PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5.1
Yatra – Travel Booking & Tour Operator Software v3.0.5.1
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / app / Services / EmailMergeTagRegistry.php

EmailMergeTagRegistry.php in Yatra – Travel Booking & Tour Operator Software 3.0.5.1, at app/Services/EmailMergeTagRegistry.php

936 lines 42.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Central registry of email merge tags.
4 *
5 * The single source of truth for every {{tag}} an operator can put inside
6 * an email subject or body. Defines the tag's label, description, category
7 * and sample value, plus which automation events resolve it at send time.
8 *
9 * Consumed by:
10 * - {@see \YatraPro\Modules\EmailAutomation\Services\EmailAutomationService}
11 * for the Template Editor's "Available Variables" sidebar and for
12 * preview-sample defaults — instead of maintaining a parallel catalogue.
13 * - {@see \YatraPro\Modules\EmailAutomation\Support\EmailAutomationEvents}
14 * to derive each event's variable whitelist, so adding a new tag to
15 * the registry automatically surfaces it on the events that resolve it.
16 *
17 * To add a new tag: append a row in {@see self::definitions()} with the
18 * event keys that actually inject the value, then make the renderer
19 * (`variablesFromBooking`, etc.) inject it. Both ends stay in sync.
20 *
21 * @package Yatra\Services
22 * @since 3.0.5
23 */
24
25 declare(strict_types=1);
26
27 namespace Yatra\Services;
28
29 final class EmailMergeTagRegistry
30 {
31 public const CATEGORY_GENERAL = 'general';
32 public const CATEGORY_CUSTOMER = 'customer';
33 public const CATEGORY_BOOKING = 'booking';
34 public const CATEGORY_PAYMENT = 'payment';
35 public const CATEGORY_SCHEDULED_PAYMENT = 'scheduled_payment';
36 public const CATEGORY_ENQUIRY = 'enquiry';
37 public const CATEGORY_TRIP_CONSENT = 'trip_consent';
38 public const CATEGORY_ACCOUNT = 'account';
39 public const CATEGORY_ABANDONED_RECOVERY = 'abandoned_recovery';
40 public const CATEGORY_REMINDER = 'reminder';
41
42 public const EVENT_BOOKING_CREATED = 'booking.created';
43 public const EVENT_BOOKING_CONFIRMED = 'booking.confirmed';
44 public const EVENT_BOOKING_CANCELLED = 'booking.cancelled';
45 public const EVENT_BOOKING_COMPLETED = 'booking.completed';
46 public const EVENT_BOOKING_EXPIRED = 'booking.expired';
47 public const EVENT_PAYMENT_RECEIVED = 'payment.received';
48 public const EVENT_PAYMENT_REMINDER = 'payment.reminder';
49 public const EVENT_REMINDER_TRIP = 'reminder.trip';
50 public const EVENT_ENQUIRY_CREATED = 'enquiry.created';
51 public const EVENT_ENQUIRY_RESPONDED = 'enquiry.responded';
52 public const EVENT_REVIEW_REQUEST = 'marketing.review_request';
53 public const EVENT_CONSENT_REQUESTED = 'consent.requested';
54 public const EVENT_ACCOUNT_EMAIL_VERIFICATION = 'account.email_verification';
55 public const EVENT_SCHEDULED_PAYMENT_REMINDER = 'scheduled.payment.reminder';
56 public const EVENT_SCHEDULED_PAYMENT_SUCCEEDED = 'scheduled.payment.succeeded';
57 public const EVENT_SCHEDULED_PAYMENT_FAILED = 'scheduled.payment.failed';
58 public const EVENT_BOOKING_ABANDONED_RECOVERY = 'booking.abandoned_recovery';
59
60 /**
61 * Every event that resolves a `variablesFromBooking()`-derived
62 * booking context. Booking-context tags inherit this list so
63 * the per-event whitelist stays in sync as events evolve.
64 */
65 private const BOOKING_CONTEXT_EVENTS = [
66 self::EVENT_BOOKING_CREATED,
67 self::EVENT_BOOKING_CONFIRMED,
68 self::EVENT_BOOKING_CANCELLED,
69 self::EVENT_BOOKING_COMPLETED,
70 self::EVENT_BOOKING_EXPIRED,
71 self::EVENT_PAYMENT_RECEIVED,
72 self::EVENT_PAYMENT_REMINDER,
73 self::EVENT_REMINDER_TRIP,
74 self::EVENT_REVIEW_REQUEST,
75 self::EVENT_SCHEDULED_PAYMENT_REMINDER,
76 self::EVENT_SCHEDULED_PAYMENT_SUCCEEDED,
77 self::EVENT_SCHEDULED_PAYMENT_FAILED,
78 ];
79
80 private const ENQUIRY_CONTEXT_EVENTS = [
81 self::EVENT_ENQUIRY_CREATED,
82 self::EVENT_ENQUIRY_RESPONDED,
83 ];
84
85 private const SCHEDULED_PAYMENT_EVENTS = [
86 self::EVENT_SCHEDULED_PAYMENT_REMINDER,
87 self::EVENT_SCHEDULED_PAYMENT_SUCCEEDED,
88 self::EVENT_SCHEDULED_PAYMENT_FAILED,
89 ];
90
91 /**
92 * Full merge-tag catalogue keyed by tag key.
93 *
94 * Each entry shape:
95 * - key (string) — the literal token, e.g. `booking_reference`
96 * - label (string) — human-readable label for the UI sidebar
97 * - description (string) — short hint shown under the label
98 * - category (string) — bucket for grouping in the sidebar
99 * - sample (string) — preview value when no real context is present
100 * - events (list|string) — automation events that inject this tag at
101 * send time. Use the literal '*' to mark
102 * event-independent tags (site_name, etc.).
103 *
104 * Filter `yatra_email_merge_tag_definitions` lets Pro modules / custom
105 * integrations append their own tags without editing this method.
106 *
107 * @return array<string, array{key:string,label:string,description:string,category:string,sample:string,events:list<string>|string}>
108 */
109 public static function definitions(): array
110 {
111 $previewTok = defined('YATRA_EMAIL_VERIFICATION_PREVIEW_TOKEN')
112 ? (string) YATRA_EMAIL_VERIFICATION_PREVIEW_TOKEN
113 : 'preview-verify-token';
114 $verificationSampleLink = function_exists('yatra_get_email_verification_url')
115 ? yatra_get_email_verification_url($previewTok)
116 : home_url('/?yatra_verify_email=' . rawurlencode($previewTok));
117
118 $dateFormat = function_exists('get_option') ? (string) get_option('date_format') : 'Y-m-d';
119 $sampleTravelDate = function_exists('date_i18n')
120 ? date_i18n($dateFormat, strtotime('+30 days') ?: time())
121 : date('Y-m-d', strtotime('+30 days') ?: time());
122 $sampleDueDate = function_exists('date_i18n')
123 ? date_i18n($dateFormat, strtotime('+14 days') ?: time())
124 : date('Y-m-d', strtotime('+14 days') ?: time());
125
126 $homeUrl = function_exists('home_url') ? home_url('/') : 'https://example.test/';
127 $adminUrl = function_exists('admin_url') ? admin_url('admin.php?page=yatra') : $homeUrl . 'wp-admin/admin.php?page=yatra';
128 $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'Your Site';
129 $adminEmail = function_exists('get_option') ? (string) get_option('admin_email') : '[email protected]';
130
131 $bookingContextEvents = self::BOOKING_CONTEXT_EVENTS;
132 $enquiryContextEvents = self::ENQUIRY_CONTEXT_EVENTS;
133 $scheduledPaymentEvents = self::SCHEDULED_PAYMENT_EVENTS;
134
135 $catalog = [
136 // ---------------------------------------------------------
137 // General — available to every event because parseTemplate
138 // merges getDefaultVariables() over the caller's $variables.
139 // ---------------------------------------------------------
140 'site_name' => [
141 'key' => 'site_name',
142 'label' => 'Site Name',
143 'description' => 'Your website name (from WordPress Site Title).',
144 'category' => self::CATEGORY_GENERAL,
145 'sample' => $siteName !== '' ? $siteName : 'Your Site',
146 'events' => '*',
147 ],
148 'site_url' => [
149 'key' => 'site_url',
150 'label' => 'Site URL',
151 'description' => 'Your website home URL.',
152 'category' => self::CATEGORY_GENERAL,
153 'sample' => $homeUrl,
154 'events' => '*',
155 ],
156 'admin_email' => [
157 'key' => 'admin_email',
158 'label' => 'Admin Email',
159 'description' => 'Site administrator email address.',
160 'category' => self::CATEGORY_GENERAL,
161 'sample' => $adminEmail !== '' ? $adminEmail : '[email protected]',
162 'events' => '*',
163 ],
164 'admin_url' => [
165 'key' => 'admin_url',
166 'label' => 'Admin URL',
167 'description' => 'Link to the Yatra admin dashboard.',
168 'category' => self::CATEGORY_GENERAL,
169 'sample' => $adminUrl,
170 'events' => '*',
171 ],
172 'current_date' => [
173 'key' => 'current_date',
174 'label' => 'Current Date',
175 'description' => "Today's date formatted per the site's date format.",
176 'category' => self::CATEGORY_GENERAL,
177 'sample' => function_exists('date_i18n') ? date_i18n($dateFormat) : date($dateFormat),
178 'events' => '*',
179 ],
180 'current_year' => [
181 'key' => 'current_year',
182 'label' => 'Current Year',
183 'description' => 'Current four-digit year.',
184 'category' => self::CATEGORY_GENERAL,
185 'sample' => date('Y'),
186 'events' => '*',
187 ],
188
189 // ---------------------------------------------------------
190 // Customer — produced by variablesFromBooking + variablesFromEnquiry
191 // + the verification + consent + recovery senders.
192 // ---------------------------------------------------------
193 'customer_name' => [
194 'key' => 'customer_name',
195 'label' => 'Customer Name',
196 'description' => 'Full name (first + last) of the customer / enquirer.',
197 'category' => self::CATEGORY_CUSTOMER,
198 'sample' => 'John Doe',
199 'events' => array_merge(
200 $bookingContextEvents,
201 $enquiryContextEvents,
202 [
203 self::EVENT_ACCOUNT_EMAIL_VERIFICATION,
204 self::EVENT_BOOKING_ABANDONED_RECOVERY,
205 ]
206 ),
207 ],
208 'customer_first_name' => [
209 'key' => 'customer_first_name',
210 'label' => 'First Name',
211 'description' => 'Customer first name only.',
212 'category' => self::CATEGORY_CUSTOMER,
213 'sample' => 'John',
214 'events' => array_merge(
215 $bookingContextEvents,
216 [self::EVENT_ACCOUNT_EMAIL_VERIFICATION]
217 ),
218 ],
219 'customer_last_name' => [
220 'key' => 'customer_last_name',
221 'label' => 'Last Name',
222 'description' => 'Customer last name only.',
223 'category' => self::CATEGORY_CUSTOMER,
224 'sample' => 'Doe',
225 'events' => $bookingContextEvents,
226 ],
227 'customer_email' => [
228 'key' => 'customer_email',
229 'label' => 'Customer Email',
230 'description' => 'Customer email address.',
231 'category' => self::CATEGORY_CUSTOMER,
232 'sample' => '[email protected]',
233 'events' => array_merge(
234 $bookingContextEvents,
235 $enquiryContextEvents,
236 [
237 self::EVENT_ACCOUNT_EMAIL_VERIFICATION,
238 self::EVENT_BOOKING_ABANDONED_RECOVERY,
239 ]
240 ),
241 ],
242 'customer_phone' => [
243 'key' => 'customer_phone',
244 'label' => 'Customer Phone',
245 'description' => 'Customer phone number.',
246 'category' => self::CATEGORY_CUSTOMER,
247 'sample' => '+1 234 567 8900',
248 'events' => array_merge($bookingContextEvents, $enquiryContextEvents),
249 ],
250
251 // ---------------------------------------------------------
252 // Booking — core fields from variablesFromBooking + the rich
253 // tags appended by BookingEmailRichMergeTags::forBooking.
254 // ---------------------------------------------------------
255 'booking_reference' => [
256 'key' => 'booking_reference',
257 'label' => 'Booking Reference',
258 'description' => 'Customer-visible booking code (e.g. YTR-12345).',
259 'category' => self::CATEGORY_BOOKING,
260 'sample' => 'YTR-2024-001234',
261 'events' => array_merge(
262 $bookingContextEvents,
263 [
264 self::EVENT_CONSENT_REQUESTED,
265 self::EVENT_BOOKING_ABANDONED_RECOVERY,
266 ]
267 ),
268 ],
269 'booking_id' => [
270 'key' => 'booking_id',
271 'label' => 'Booking ID',
272 'description' => 'Internal numeric booking identifier.',
273 'category' => self::CATEGORY_BOOKING,
274 'sample' => '1234',
275 'events' => $bookingContextEvents,
276 ],
277 'booking_url' => [
278 'key' => 'booking_url',
279 'label' => 'Booking URL',
280 'description' => 'Link to view the booking in My Account.',
281 'category' => self::CATEGORY_BOOKING,
282 'sample' => $homeUrl . 'my-account/bookings/1234',
283 'events' => $bookingContextEvents,
284 ],
285 'booking_status' => [
286 'key' => 'booking_status',
287 'label' => 'Booking Status',
288 'description' => 'pending / confirmed / cancelled / completed.',
289 'category' => self::CATEGORY_BOOKING,
290 'sample' => 'confirmed',
291 'events' => $bookingContextEvents,
292 ],
293 'payment_status' => [
294 'key' => 'payment_status',
295 'label' => 'Payment Status',
296 'description' => 'unpaid / partial / paid / refunded.',
297 'category' => self::CATEGORY_BOOKING,
298 'sample' => 'paid',
299 'events' => $bookingContextEvents,
300 ],
301 'trip_name' => [
302 'key' => 'trip_name',
303 'label' => 'Trip Name',
304 'description' => 'Title of the trip the booking / enquiry is for.',
305 'category' => self::CATEGORY_BOOKING,
306 'sample' => 'Amazing Mountain Adventure',
307 'events' => array_merge(
308 $bookingContextEvents,
309 $enquiryContextEvents,
310 [
311 self::EVENT_CONSENT_REQUESTED,
312 self::EVENT_BOOKING_ABANDONED_RECOVERY,
313 ]
314 ),
315 ],
316 'trip_url' => [
317 'key' => 'trip_url',
318 'label' => 'Trip URL',
319 'description' => 'Public link to the trip detail page.',
320 'category' => self::CATEGORY_BOOKING,
321 'sample' => $homeUrl . 'trips/amazing-mountain-adventure',
322 'events' => array_merge($bookingContextEvents, $enquiryContextEvents),
323 ],
324 'travel_date' => [
325 'key' => 'travel_date',
326 'label' => 'Travel Date',
327 'description' => 'Departure date formatted per site settings.',
328 'category' => self::CATEGORY_BOOKING,
329 'sample' => $sampleTravelDate,
330 'events' => array_merge($bookingContextEvents, [self::EVENT_CONSENT_REQUESTED]),
331 ],
332 'travelers_count' => [
333 'key' => 'travelers_count',
334 'label' => 'Travelers Count',
335 'description' => 'Number of travelers on the booking.',
336 'category' => self::CATEGORY_BOOKING,
337 'sample' => '4',
338 'events' => $bookingContextEvents,
339 ],
340 'travelers_list' => [
341 'key' => 'travelers_list',
342 'label' => 'Travelers List (plain)',
343 'description' => 'Plain-text list of traveler names.',
344 'category' => self::CATEGORY_BOOKING,
345 'sample' => "John Doe\nJane Doe",
346 'events' => $bookingContextEvents,
347 ],
348 'travelers_list_html' => [
349 'key' => 'travelers_list_html',
350 'label' => 'Travelers List (HTML)',
351 'description' => 'HTML-formatted list of traveler names.',
352 'category' => self::CATEGORY_BOOKING,
353 'sample' => '<ul><li>John Doe</li><li>Jane Doe</li></ul>',
354 'events' => $bookingContextEvents,
355 ],
356 'traveler_custom_fields_html' => [
357 'key' => 'traveler_custom_fields_html',
358 'label' => 'Traveler Custom Fields (HTML)',
359 'description' => 'Dynamic Form Field answers per traveler, rendered as HTML.',
360 'category' => self::CATEGORY_BOOKING,
361 'sample' => '',
362 'events' => $bookingContextEvents,
363 ],
364 'booking_custom_fields_html' => [
365 'key' => 'booking_custom_fields_html',
366 'label' => 'Booking Custom Fields (HTML)',
367 'description' => 'Booking-level Dynamic Form Field answers as HTML.',
368 'category' => self::CATEGORY_BOOKING,
369 'sample' => '',
370 'events' => $bookingContextEvents,
371 ],
372 'special_requests' => [
373 'key' => 'special_requests',
374 'label' => 'Special Requests (plain)',
375 'description' => 'Customer-entered special requests text.',
376 'category' => self::CATEGORY_BOOKING,
377 'sample' => 'Vegetarian meals please.',
378 'events' => $bookingContextEvents,
379 ],
380 'special_requests_html' => [
381 'key' => 'special_requests_html',
382 'label' => 'Special Requests (HTML)',
383 'description' => 'Special requests with line breaks preserved.',
384 'category' => self::CATEGORY_BOOKING,
385 'sample' => 'Vegetarian meals please.',
386 'events' => $bookingContextEvents,
387 ],
388 'cancellation_reason' => [
389 'key' => 'cancellation_reason',
390 'label' => 'Cancellation Reason',
391 'description' => 'Reason recorded when the booking was cancelled.',
392 'category' => self::CATEGORY_BOOKING,
393 'sample' => 'Change of plans',
394 'events' => [self::EVENT_BOOKING_CANCELLED],
395 ],
396 'completion_date' => [
397 'key' => 'completion_date',
398 'label' => 'Completion Date',
399 'description' => 'Date the trip / booking was marked completed.',
400 'category' => self::CATEGORY_BOOKING,
401 'sample' => function_exists('date_i18n') ? date_i18n($dateFormat) : date($dateFormat),
402 'events' => [self::EVENT_BOOKING_COMPLETED, self::EVENT_REVIEW_REQUEST],
403 ],
404 'expiry_policy_note' => [
405 'key' => 'expiry_policy_note',
406 'label' => 'Expiry Policy Note',
407 'description' => 'Message shown when a booking auto-expires for non-payment.',
408 'category' => self::CATEGORY_BOOKING,
409 'sample' => 'This booking was automatically cancelled after the payment window expired.',
410 'events' => [self::EVENT_BOOKING_EXPIRED],
411 ],
412
413 // ---------------------------------------------------------
414 // Payment
415 // ---------------------------------------------------------
416 'total_amount_formatted' => [
417 'key' => 'total_amount_formatted',
418 'label' => 'Total Amount (formatted)',
419 'description' => 'Total cost with currency symbol — preferred over total_amount.',
420 'category' => self::CATEGORY_PAYMENT,
421 'sample' => '$2,500.00',
422 'events' => $bookingContextEvents,
423 ],
424 // Alias of `total_amount_formatted` — exposed for templates
425 // that use the short name. Renders identically (formatted
426 // with currency) so admins can pick whichever reads
427 // naturally in their copy.
428 'total_amount' => [
429 'key' => 'total_amount',
430 'label' => 'Total Amount',
431 'description' => 'Total cost with currency symbol (alias of total_amount_formatted).',
432 'category' => self::CATEGORY_PAYMENT,
433 'sample' => '$2,500.00',
434 'events' => $bookingContextEvents,
435 ],
436 'amount_due_formatted' => [
437 'key' => 'amount_due_formatted',
438 'label' => 'Amount Due (formatted)',
439 'description' => 'Remaining balance with currency symbol.',
440 'category' => self::CATEGORY_PAYMENT,
441 'sample' => '$2,000.00',
442 'events' => $bookingContextEvents,
443 ],
444 // Aliases for the remaining balance — same value, different
445 // common spellings. `{{balance_due}}` is the most common
446 // legacy spelling in customer-edited templates; `amount_due`
447 // (unformatted-looking name but actually formatted with
448 // currency) matches the booking record column.
449 'balance_due' => [
450 'key' => 'balance_due',
451 'label' => 'Balance Due',
452 'description' => 'Remaining balance with currency symbol (alias of amount_due_formatted).',
453 'category' => self::CATEGORY_PAYMENT,
454 'sample' => '$2,000.00',
455 'events' => $bookingContextEvents,
456 ],
457 'amount_due' => [
458 'key' => 'amount_due',
459 'label' => 'Amount Due',
460 'description' => 'Remaining balance with currency symbol (alias of amount_due_formatted).',
461 'category' => self::CATEGORY_PAYMENT,
462 'sample' => '$2,000.00',
463 'events' => $bookingContextEvents,
464 ],
465 'amount_paid' => [
466 'key' => 'amount_paid',
467 'label' => 'Amount Paid',
468 'description' => 'Total paid so far with currency symbol.',
469 'category' => self::CATEGORY_PAYMENT,
470 'sample' => '$500.00',
471 'events' => $bookingContextEvents,
472 ],
473 'amount_paid_formatted' => [
474 'key' => 'amount_paid_formatted',
475 'label' => 'Amount Paid (formatted)',
476 'description' => 'Total paid so far with currency symbol (alias of amount_paid).',
477 'category' => self::CATEGORY_PAYMENT,
478 'sample' => '$500.00',
479 'events' => $bookingContextEvents,
480 ],
481 'currency' => [
482 'key' => 'currency',
483 'label' => 'Currency',
484 'description' => 'ISO 4217 currency code (e.g. USD).',
485 'category' => self::CATEGORY_PAYMENT,
486 'sample' => 'USD',
487 'events' => $bookingContextEvents,
488 ],
489 'payment_amount_formatted' => [
490 'key' => 'payment_amount_formatted',
491 'label' => 'Payment Amount (formatted)',
492 'description' => 'Amount of the specific payment with currency.',
493 'category' => self::CATEGORY_PAYMENT,
494 'sample' => '$500.00',
495 'events' => [self::EVENT_PAYMENT_RECEIVED, self::EVENT_PAYMENT_REMINDER],
496 ],
497 'payment_method' => [
498 'key' => 'payment_method',
499 'label' => 'Payment Method',
500 'description' => 'Instrument label (e.g. Card, Bank Transfer).',
501 'category' => self::CATEGORY_PAYMENT,
502 'sample' => 'Credit Card',
503 'events' => [self::EVENT_PAYMENT_RECEIVED, self::EVENT_PAYMENT_REMINDER],
504 ],
505 'transaction_id' => [
506 'key' => 'transaction_id',
507 'label' => 'Transaction ID',
508 'description' => 'Gateway transaction reference for the payment.',
509 'category' => self::CATEGORY_PAYMENT,
510 'sample' => 'ch_3O8XYZabc123',
511 'events' => [self::EVENT_PAYMENT_RECEIVED],
512 ],
513 'payment_gateway' => [
514 'key' => 'payment_gateway',
515 'label' => 'Payment Gateway (slug)',
516 'description' => 'Internal gateway slug — stripe / paypal / razorpay etc.',
517 'category' => self::CATEGORY_PAYMENT,
518 'sample' => 'stripe',
519 'events' => $bookingContextEvents,
520 ],
521 'payment_gateway_label' => [
522 'key' => 'payment_gateway_label',
523 'label' => 'Payment Gateway (label)',
524 'description' => 'Human-readable gateway name — Stripe, PayPal etc.',
525 'category' => self::CATEGORY_PAYMENT,
526 'sample' => 'Stripe',
527 'events' => $bookingContextEvents,
528 ],
529 'payment_schedule' => [
530 'key' => 'payment_schedule',
531 'label' => 'Payment Schedule (slug)',
532 'description' => 'full / deposit / partial — raw value.',
533 'category' => self::CATEGORY_PAYMENT,
534 'sample' => 'deposit',
535 'events' => $bookingContextEvents,
536 ],
537 'payment_schedule_label' => [
538 'key' => 'payment_schedule_label',
539 'label' => 'Payment Schedule (label)',
540 'description' => 'Humanised schedule (e.g. Deposit, Full Payment).',
541 'category' => self::CATEGORY_PAYMENT,
542 'sample' => 'Deposit',
543 'events' => $bookingContextEvents,
544 ],
545 'due_date' => [
546 'key' => 'due_date',
547 'label' => 'Due Date',
548 'description' => 'Payment due date for reminders.',
549 'category' => self::CATEGORY_PAYMENT,
550 'sample' => $sampleDueDate,
551 'events' => [self::EVENT_PAYMENT_REMINDER],
552 ],
553
554 // ---------------------------------------------------------
555 // Scheduled payments (installments)
556 // ---------------------------------------------------------
557 'scheduled_amount_formatted' => [
558 'key' => 'scheduled_amount_formatted',
559 'label' => 'Scheduled Amount (formatted)',
560 'description' => 'Amount of the upcoming scheduled charge with currency.',
561 'category' => self::CATEGORY_SCHEDULED_PAYMENT,
562 'sample' => '$750.00',
563 'events' => $scheduledPaymentEvents,
564 ],
565 'scheduled_date_formatted' => [
566 'key' => 'scheduled_date_formatted',
567 'label' => 'Scheduled Date (formatted)',
568 'description' => 'When the next scheduled charge will run.',
569 'category' => self::CATEGORY_SCHEDULED_PAYMENT,
570 'sample' => $sampleDueDate,
571 'events' => $scheduledPaymentEvents,
572 ],
573 'payment_type_label' => [
574 'key' => 'payment_type_label',
575 'label' => 'Payment Type Label',
576 'description' => 'Humanised type (Deposit, Final, Installment 2 of 4 ...).',
577 'category' => self::CATEGORY_SCHEDULED_PAYMENT,
578 'sample' => 'Installment 2 of 4',
579 'events' => $scheduledPaymentEvents,
580 ],
581 'balance_after_formatted' => [
582 'key' => 'balance_after_formatted',
583 'label' => 'Balance After (formatted)',
584 'description' => 'Balance remaining after this charge succeeds.',
585 'category' => self::CATEGORY_SCHEDULED_PAYMENT,
586 'sample' => '$1,250.00',
587 'events' => [self::EVENT_SCHEDULED_PAYMENT_SUCCEEDED],
588 ],
589 'failure_reason' => [
590 'key' => 'failure_reason',
591 'label' => 'Failure Reason',
592 'description' => 'Provided by the gateway when a scheduled charge fails.',
593 'category' => self::CATEGORY_SCHEDULED_PAYMENT,
594 'sample' => 'Card declined',
595 'events' => [self::EVENT_SCHEDULED_PAYMENT_FAILED],
596 ],
597 'failure_intro_html' => [
598 'key' => 'failure_intro_html',
599 'label' => 'Failure Intro (HTML)',
600 'description' => 'Intro block for the payment-failure email body.',
601 'category' => self::CATEGORY_SCHEDULED_PAYMENT,
602 'sample' => '<p>We were unable to process your scheduled payment.</p>',
603 'events' => [self::EVENT_SCHEDULED_PAYMENT_FAILED],
604 ],
605 'failure_followup_html' => [
606 'key' => 'failure_followup_html',
607 'label' => 'Failure Follow-up (HTML)',
608 'description' => 'Closing block prompting the customer to update payment.',
609 'category' => self::CATEGORY_SCHEDULED_PAYMENT,
610 'sample' => '<p>Please update your payment method to avoid cancellation.</p>',
611 'events' => [self::EVENT_SCHEDULED_PAYMENT_FAILED],
612 ],
613
614 // ---------------------------------------------------------
615 // Reminder (trip & booking reminders)
616 // ---------------------------------------------------------
617 'days_until_trip' => [
618 'key' => 'days_until_trip',
619 'label' => 'Days Until Trip',
620 'description' => 'Days remaining until departure.',
621 'category' => self::CATEGORY_REMINDER,
622 'sample' => '30',
623 'events' => [self::EVENT_REMINDER_TRIP],
624 ],
625 'reminder_days' => [
626 'key' => 'reminder_days',
627 'label' => 'Reminder Days',
628 'description' => 'Configured number of days before the trip when the reminder fires.',
629 'category' => self::CATEGORY_REMINDER,
630 'sample' => '3',
631 'events' => [self::EVENT_REMINDER_TRIP],
632 ],
633 'reminder_extra_html' => [
634 'key' => 'reminder_extra_html',
635 'label' => 'Reminder Extra (HTML)',
636 'description' => 'Optional extra block appended to reminder emails (packing list, etc.).',
637 'category' => self::CATEGORY_REMINDER,
638 'sample' => '<p>Don\'t forget your passport and travel insurance.</p>',
639 'events' => [self::EVENT_REMINDER_TRIP],
640 ],
641 'review_url' => [
642 'key' => 'review_url',
643 'label' => 'Review URL',
644 'description' => 'Public link the customer opens to leave a review.',
645 'category' => self::CATEGORY_REMINDER,
646 'sample' => $homeUrl . 'trips/amazing-mountain-adventure#reviews',
647 'events' => [self::EVENT_REVIEW_REQUEST],
648 ],
649
650 // ---------------------------------------------------------
651 // Enquiry
652 // ---------------------------------------------------------
653 'enquiry_id' => [
654 'key' => 'enquiry_id',
655 'label' => 'Enquiry ID',
656 'description' => 'Internal numeric enquiry identifier.',
657 'category' => self::CATEGORY_ENQUIRY,
658 'sample' => '4567',
659 'events' => $enquiryContextEvents,
660 ],
661 'enquiry_date' => [
662 'key' => 'enquiry_date',
663 'label' => 'Enquiry Date',
664 'description' => 'When the enquiry was submitted.',
665 'category' => self::CATEGORY_ENQUIRY,
666 'sample' => function_exists('date_i18n') ? date_i18n($dateFormat) : date($dateFormat),
667 'events' => $enquiryContextEvents,
668 ],
669 'subject' => [
670 'key' => 'subject',
671 'label' => 'Subject',
672 'description' => 'Subject line the customer provided.',
673 'category' => self::CATEGORY_ENQUIRY,
674 'sample' => 'Question about Amazing Mountain Adventure',
675 'events' => $enquiryContextEvents,
676 ],
677 'message' => [
678 'key' => 'message',
679 'label' => 'Message',
680 'description' => 'Customer message body (sanitised, line breaks preserved).',
681 'category' => self::CATEGORY_ENQUIRY,
682 'sample' => 'I would like to know more about this trip. What is included in the package?',
683 'events' => $enquiryContextEvents,
684 ],
685 'original_message' => [
686 'key' => 'original_message',
687 'label' => 'Original Message',
688 'description' => 'First message in the enquiry thread (no line-break escaping).',
689 'category' => self::CATEGORY_ENQUIRY,
690 'sample' => 'I would like to know more about this trip.',
691 'events' => $enquiryContextEvents,
692 ],
693 'response' => [
694 'key' => 'response',
695 'label' => 'Response',
696 'description' => "Operator's typed reply (alias of response_message).",
697 'category' => self::CATEGORY_ENQUIRY,
698 'sample' => 'Thank you for your interest! The package includes accommodation, meals, and guided tours.',
699 'events' => [self::EVENT_ENQUIRY_RESPONDED],
700 ],
701 'response_message' => [
702 'key' => 'response_message',
703 'label' => 'Response Message',
704 'description' => "Operator's typed reply.",
705 'category' => self::CATEGORY_ENQUIRY,
706 'sample' => 'Thank you for your interest! The package includes accommodation, meals, and guided tours.',
707 'events' => [self::EVENT_ENQUIRY_RESPONDED],
708 ],
709 'response_date' => [
710 'key' => 'response_date',
711 'label' => 'Response Date',
712 'description' => 'When the reply was sent.',
713 'category' => self::CATEGORY_ENQUIRY,
714 'sample' => function_exists('date_i18n') ? date_i18n($dateFormat) : date($dateFormat),
715 'events' => [self::EVENT_ENQUIRY_RESPONDED],
716 ],
717
718 // ---------------------------------------------------------
719 // Trip consent (Pro)
720 // ---------------------------------------------------------
721 'recipient_name' => [
722 'key' => 'recipient_name',
723 'label' => 'Recipient Name',
724 'description' => 'Traveler receiving the consent email.',
725 'category' => self::CATEGORY_TRIP_CONSENT,
726 'sample' => 'Alex Traveler',
727 'events' => [self::EVENT_CONSENT_REQUESTED],
728 ],
729 'form_name' => [
730 'key' => 'form_name',
731 'label' => 'Consent Form Name',
732 'description' => 'Title of the consent form.',
733 'category' => self::CATEGORY_TRIP_CONSENT,
734 'sample' => 'Trip liability & release',
735 'events' => [self::EVENT_CONSENT_REQUESTED],
736 ],
737 'consent_link' => [
738 'key' => 'consent_link',
739 'label' => 'Consent Link',
740 'description' => 'URL to open and sign the form.',
741 'category' => self::CATEGORY_TRIP_CONSENT,
742 'sample' => $homeUrl . 'trip-consent/preview-token/',
743 'events' => [self::EVENT_CONSENT_REQUESTED],
744 ],
745 'consent_test_notice_html' => [
746 'key' => 'consent_test_notice_html',
747 'label' => 'Test Notice (HTML)',
748 'description' => 'Shown only on admin test sends.',
749 'category' => self::CATEGORY_TRIP_CONSENT,
750 'sample' => '',
751 'events' => [self::EVENT_CONSENT_REQUESTED],
752 ],
753
754 // ---------------------------------------------------------
755 // Account verification
756 // ---------------------------------------------------------
757 'verification_link' => [
758 'key' => 'verification_link',
759 'label' => 'Verification Link',
760 'description' => 'Magic link the customer opens to verify their email.',
761 'category' => self::CATEGORY_ACCOUNT,
762 'sample' => $verificationSampleLink,
763 'events' => [self::EVENT_ACCOUNT_EMAIL_VERIFICATION],
764 ],
765 'intro_paragraph' => [
766 'key' => 'intro_paragraph',
767 'label' => 'Intro Paragraph',
768 'description' => 'Opening sentence (registration / resend variant).',
769 'category' => self::CATEGORY_ACCOUNT,
770 'sample' => 'Thank you for registering. Click the button in this email to verify your address.',
771 'events' => [self::EVENT_ACCOUNT_EMAIL_VERIFICATION],
772 ],
773 'footer_note' => [
774 'key' => 'footer_note',
775 'label' => 'Footer Note',
776 'description' => 'Disclaimer for unintended recipients.',
777 'category' => self::CATEGORY_ACCOUNT,
778 'sample' => 'If you did not create an account, you can ignore this email.',
779 'events' => [self::EVENT_ACCOUNT_EMAIL_VERIFICATION],
780 ],
781 'expiry_notice_html' => [
782 'key' => 'expiry_notice_html',
783 'label' => 'Expiry Notice (HTML)',
784 'description' => 'Link-expiry messaging block (consent / verification emails).',
785 'category' => self::CATEGORY_ACCOUNT,
786 'sample' => '<strong>Security note:</strong> This link expires in 24 hours.',
787 'events' => [
788 self::EVENT_ACCOUNT_EMAIL_VERIFICATION,
789 self::EVENT_CONSENT_REQUESTED,
790 ],
791 ],
792
793 // ---------------------------------------------------------
794 // Abandoned booking recovery (Pro)
795 // ---------------------------------------------------------
796 'recovery_link' => [
797 'key' => 'recovery_link',
798 'label' => 'Recovery Link',
799 'description' => 'Resume the abandoned checkout from the customer email.',
800 'category' => self::CATEGORY_ABANDONED_RECOVERY,
801 'sample' => $homeUrl . 'checkout/recover/sample-token',
802 'events' => [self::EVENT_BOOKING_ABANDONED_RECOVERY],
803 ],
804 'recovery_reminder_label' => [
805 'key' => 'recovery_reminder_label',
806 'label' => 'Reminder Label',
807 'description' => 'Sequence-stage label (First, Second, Final).',
808 'category' => self::CATEGORY_ABANDONED_RECOVERY,
809 'sample' => 'First reminder',
810 'events' => [self::EVENT_BOOKING_ABANDONED_RECOVERY],
811 ],
812 'recovery_intro_html' => [
813 'key' => 'recovery_intro_html',
814 'label' => 'Intro Paragraph (HTML)',
815 'description' => 'Lead paragraph specific to each recovery email.',
816 'category' => self::CATEGORY_ABANDONED_RECOVERY,
817 'sample' => '<p>You\'re just one step away from booking your dream trip.</p>',
818 'events' => [self::EVENT_BOOKING_ABANDONED_RECOVERY],
819 ],
820 ];
821
822 /**
823 * Filter the email merge-tag catalogue so integrations (Channel
824 * Manager, WhatsApp, custom modules) can append their own tags.
825 *
826 * @param array $catalog The tag definitions keyed by tag key.
827 */
828 return function_exists('apply_filters')
829 ? (array) apply_filters('yatra_email_merge_tag_definitions', $catalog)
830 : $catalog;
831 }
832
833 /**
834 * Return tag definitions grouped by category, optionally filtered to
835 * those that resolve for the given automation event.
836 *
837 * When $eventKey is empty or unknown, returns the full catalogue so
838 * the operator never sees an empty sidebar.
839 *
840 * @return array<string, list<array{key:string,label:string,description:string}>>
841 */
842 public static function groupedForEvent(string $eventKey = ''): array
843 {
844 $eventKey = trim($eventKey);
845 $defs = self::definitions();
846
847 if ($eventKey !== '' && !self::eventHasAnyDefinitions($eventKey, $defs)) {
848 // Unknown event — return the full registry rather than nothing.
849 $eventKey = '';
850 }
851
852 $grouped = [];
853 foreach ($defs as $def) {
854 if ($eventKey !== '' && !self::tagAppliesToEvent($def, $eventKey)) {
855 continue;
856 }
857 $cat = $def['category'] ?? self::CATEGORY_GENERAL;
858 $grouped[$cat] = $grouped[$cat] ?? [];
859 $grouped[$cat][] = [
860 'key' => $def['key'],
861 'label' => $def['label'],
862 'description' => $def['description'],
863 ];
864 }
865
866 return $grouped;
867 }
868
869 /**
870 * Flat list of tag keys that resolve for the given event. Used by
871 * EmailAutomationEvents::definitions to derive each event's variable
872 * whitelist instead of hand-maintaining a parallel list.
873 *
874 * @return list<string>
875 */
876 public static function keysForEvent(string $eventKey): array
877 {
878 $eventKey = trim($eventKey);
879 if ($eventKey === '') {
880 return [];
881 }
882 $keys = [];
883 foreach (self::definitions() as $def) {
884 if (self::tagAppliesToEvent($def, $eventKey)) {
885 $keys[] = $def['key'];
886 }
887 }
888 return $keys;
889 }
890
891 /**
892 * Sample variable map for the in-editor preview pipeline. Includes
893 * every tag with a non-empty sample value — operators see realistic
894 * placeholders rather than the literal {{tag}} string.
895 *
896 * @return array<string, string>
897 */
898 public static function samples(): array
899 {
900 $out = [];
901 foreach (self::definitions() as $def) {
902 $out[$def['key']] = (string) ($def['sample'] ?? '');
903 }
904 return $out;
905 }
906
907 /**
908 * @param array{events:list<string>|string} $def
909 */
910 private static function tagAppliesToEvent(array $def, string $eventKey): bool
911 {
912 $events = $def['events'] ?? [];
913 if ($events === '*' || $events === ['*']) {
914 return true;
915 }
916 return is_array($events) && in_array($eventKey, $events, true);
917 }
918
919 /**
920 * @param array<string, array{events:list<string>|string}> $defs
921 */
922 private static function eventHasAnyDefinitions(string $eventKey, array $defs): bool
923 {
924 foreach ($defs as $def) {
925 $events = $def['events'] ?? [];
926 if ($events === '*' || $events === ['*']) {
927 continue; // General tags don't qualify the event as "known".
928 }
929 if (is_array($events) && in_array($eventKey, $events, true)) {
930 return true;
931 }
932 }
933 return false;
934 }
935 }
936