PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
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
← All changes | app/Services/EmailMergeTagRegistry.php +122 -16 3.0.5.1 → 3.0.16 View file →
@@ -44,8 +44,10 @@
44 44 public const EVENT_BOOKING_CANCELLED = 'booking.cancelled';
45 45 public const EVENT_BOOKING_COMPLETED = 'booking.completed';
46 46 public const EVENT_BOOKING_EXPIRED = 'booking.expired';
47 47 public const EVENT_PAYMENT_RECEIVED = 'payment.received';
48 + /** A payment landed but a balance is still outstanding (deposit / instalment). */
49 + public const EVENT_PAYMENT_PARTIAL_RECEIVED = 'payment.partial_received';
48 50 public const EVENT_PAYMENT_REMINDER = 'payment.reminder';
49 51 public const EVENT_REMINDER_TRIP = 'reminder.trip';
50 52 public const EVENT_ENQUIRY_CREATED = 'enquiry.created';
51 53 public const EVENT_ENQUIRY_RESPONDED = 'enquiry.responded';
@@ -51,8 +53,14 @@
51 53 public const EVENT_ENQUIRY_RESPONDED = 'enquiry.responded';
52 54 public const EVENT_REVIEW_REQUEST = 'marketing.review_request';
53 55 public const EVENT_CONSENT_REQUESTED = 'consent.requested';
54 56 public const EVENT_ACCOUNT_EMAIL_VERIFICATION = 'account.email_verification';
57 + // Account email change (CustomerService): the request goes to the NEW
58 + // address with a confirmation link; the "changed" security notice goes to
59 + // the OLD address. Pro seeds templates on these keys, so they must be
60 + // registered here or the template editor rejects them ("Invalid event key").
61 + public const EVENT_ACCOUNT_EMAIL_CHANGE_REQUEST = 'account.email_change_request';
62 + public const EVENT_ACCOUNT_EMAIL_CHANGED = 'account.email_changed';
55 63 public const EVENT_SCHEDULED_PAYMENT_REMINDER = 'scheduled.payment.reminder';
56 64 public const EVENT_SCHEDULED_PAYMENT_SUCCEEDED = 'scheduled.payment.succeeded';
57 65 public const EVENT_SCHEDULED_PAYMENT_FAILED = 'scheduled.payment.failed';
58 66 public const EVENT_BOOKING_ABANDONED_RECOVERY = 'booking.abandoned_recovery';
@@ -61,8 +69,15 @@
61 69 * Every event that resolves a `variablesFromBooking()`-derived
62 70 * booking context. Booking-context tags inherit this list so
63 71 * the per-event whitelist stays in sync as events evolve.
64 72 */
73 + /** Customer-account emails: share the customer + intro/footer tags. */
74 + private const ACCOUNT_CONTEXT_EVENTS = [
75 + self::EVENT_ACCOUNT_EMAIL_VERIFICATION,
76 + self::EVENT_ACCOUNT_EMAIL_CHANGE_REQUEST,
77 + self::EVENT_ACCOUNT_EMAIL_CHANGED,
78 + ];
79 +
65 80 private const BOOKING_CONTEXT_EVENTS = [
66 81 self::EVENT_BOOKING_CREATED,
67 82 self::EVENT_BOOKING_CONFIRMED,
68 83 self::EVENT_BOOKING_CANCELLED,
@@ -68,8 +83,9 @@
68 83 self::EVENT_BOOKING_CANCELLED,
69 84 self::EVENT_BOOKING_COMPLETED,
70 85 self::EVENT_BOOKING_EXPIRED,
71 86 self::EVENT_PAYMENT_RECEIVED,
87 + self::EVENT_PAYMENT_PARTIAL_RECEIVED,
72 88 self::EVENT_PAYMENT_REMINDER,
73 89 self::EVENT_REMINDER_TRIP,
74 90 self::EVENT_REVIEW_REQUEST,
75 91 self::EVENT_SCHEDULED_PAYMENT_REMINDER,
@@ -198,12 +214,10 @@
198 214 'sample' => 'John Doe',
199 215 'events' => array_merge(
200 216 $bookingContextEvents,
201 217 $enquiryContextEvents,
202 - [
203 - self::EVENT_ACCOUNT_EMAIL_VERIFICATION,
204 - self::EVENT_BOOKING_ABANDONED_RECOVERY,
205 - ]
218 + self::ACCOUNT_CONTEXT_EVENTS,
219 + [self::EVENT_BOOKING_ABANDONED_RECOVERY]
206 220 ),
207 221 ],
208 222 'customer_first_name' => [
209 223 'key' => 'customer_first_name',
@@ -212,9 +226,9 @@
212 226 'category' => self::CATEGORY_CUSTOMER,
213 227 'sample' => 'John',
214 228 'events' => array_merge(
215 229 $bookingContextEvents,
216 - [self::EVENT_ACCOUNT_EMAIL_VERIFICATION]
230 + self::ACCOUNT_CONTEXT_EVENTS
217 231 ),
218 232 ],
219 233 'customer_last_name' => [
220 234 'key' => 'customer_last_name',
@@ -232,12 +246,10 @@
232 246 'sample' => '[email protected]',
233 247 'events' => array_merge(
234 248 $bookingContextEvents,
235 249 $enquiryContextEvents,
236 - [
237 - self::EVENT_ACCOUNT_EMAIL_VERIFICATION,
238 - self::EVENT_BOOKING_ABANDONED_RECOVERY,
239 - ]
250 + self::ACCOUNT_CONTEXT_EVENTS,
251 + [self::EVENT_BOOKING_ABANDONED_RECOVERY]
240 252 ),
241 253 ],
242 254 'customer_phone' => [
243 255 'key' => 'customer_phone',
@@ -491,9 +503,9 @@
491 503 'label' => 'Payment Amount (formatted)',
492 504 'description' => 'Amount of the specific payment with currency.',
493 505 'category' => self::CATEGORY_PAYMENT,
494 506 'sample' => '$500.00',
495 - 'events' => [self::EVENT_PAYMENT_RECEIVED, self::EVENT_PAYMENT_REMINDER],
507 + 'events' => [self::EVENT_PAYMENT_RECEIVED, self::EVENT_PAYMENT_PARTIAL_RECEIVED, self::EVENT_PAYMENT_REMINDER],
496 508 ],
497 509 'payment_method' => [
498 510 'key' => 'payment_method',
499 511 'label' => 'Payment Method',
@@ -499,9 +511,9 @@
499 511 'label' => 'Payment Method',
500 512 'description' => 'Instrument label (e.g. Card, Bank Transfer).',
501 513 'category' => self::CATEGORY_PAYMENT,
502 514 'sample' => 'Credit Card',
503 - 'events' => [self::EVENT_PAYMENT_RECEIVED, self::EVENT_PAYMENT_REMINDER],
515 + 'events' => [self::EVENT_PAYMENT_RECEIVED, self::EVENT_PAYMENT_PARTIAL_RECEIVED, self::EVENT_PAYMENT_REMINDER],
504 516 ],
505 517 'transaction_id' => [
506 518 'key' => 'transaction_id',
507 519 'label' => 'Transaction ID',
@@ -756,20 +768,29 @@
756 768 // ---------------------------------------------------------
757 769 'verification_link' => [
758 770 'key' => 'verification_link',
759 771 'label' => 'Verification Link',
760 - 'description' => 'Magic link the customer opens to verify their email.',
772 + 'description' => 'Magic link the customer opens to verify their email, or to confirm a requested new address.',
761 773 'category' => self::CATEGORY_ACCOUNT,
762 774 'sample' => $verificationSampleLink,
763 - 'events' => [self::EVENT_ACCOUNT_EMAIL_VERIFICATION],
775 + // Not offered for the "changed" notice — that email carries no link.
776 + 'events' => [self::EVENT_ACCOUNT_EMAIL_VERIFICATION, self::EVENT_ACCOUNT_EMAIL_CHANGE_REQUEST],
764 777 ],
778 + 'new_email' => [
779 + 'key' => 'new_email',
780 + 'label' => 'New Email Address',
781 + 'description' => 'The address the customer asked to switch their account to.',
782 + 'category' => self::CATEGORY_ACCOUNT,
783 + 'sample' => '[email protected]',
784 + 'events' => [self::EVENT_ACCOUNT_EMAIL_CHANGE_REQUEST, self::EVENT_ACCOUNT_EMAIL_CHANGED],
785 + ],
765 786 'intro_paragraph' => [
766 787 'key' => 'intro_paragraph',
767 788 'label' => 'Intro Paragraph',
768 - 'description' => 'Opening sentence (registration / resend variant).',
789 + 'description' => 'Opening sentence, set by the sender for each account email (verification, change request, changed notice).',
769 790 'category' => self::CATEGORY_ACCOUNT,
770 791 'sample' => 'Thank you for registering. Click the button in this email to verify your address.',
771 - 'events' => [self::EVENT_ACCOUNT_EMAIL_VERIFICATION],
792 + 'events' => self::ACCOUNT_CONTEXT_EVENTS,
772 793 ],
773 794 'footer_note' => [
774 795 'key' => 'footer_note',
775 796 'label' => 'Footer Note',
@@ -775,9 +796,9 @@
775 796 'label' => 'Footer Note',
776 797 'description' => 'Disclaimer for unintended recipients.',
777 798 'category' => self::CATEGORY_ACCOUNT,
778 799 'sample' => 'If you did not create an account, you can ignore this email.',
779 - 'events' => [self::EVENT_ACCOUNT_EMAIL_VERIFICATION],
800 + 'events' => self::ACCOUNT_CONTEXT_EVENTS,
780 801 ],
781 802 'expiry_notice_html' => [
782 803 'key' => 'expiry_notice_html',
783 804 'label' => 'Expiry Notice (HTML)',
@@ -818,8 +839,14 @@
818 839 'events' => [self::EVENT_BOOKING_ABANDONED_RECOVERY],
819 840 ],
820 841 ];
821 842
843 + // Dynamically expose every enabled Contact/Emergency booking-form field —
844 + // including custom fields an operator adds — so they're discoverable and
845 + // usable as email variables. Values are resolved at send time by
846 + // BookingEmailRichMergeTags (contact_/emergency_ prefixes).
847 + $catalog = array_merge($catalog, self::bookingFormFieldDefinitions($bookingContextEvents));
848 +
822 849 /**
823 850 * Filter the email merge-tag catalogue so integrations (Channel
824 851 * Manager, WhatsApp, custom modules) can append their own tags.
825 852 *
@@ -827,8 +854,87 @@
827 854 */
828 855 return function_exists('apply_filters')
829 856 ? (array) apply_filters('yatra_email_merge_tag_definitions', $catalog)
830 857 : $catalog;
858 + }
859 +
860 + /**
861 + * Build merge-tag definitions from the live booking-form config so dynamic
862 + * (and custom) Contact/Emergency fields surface in the email editor. Only
863 + * enabled fields in enabled sections are included; existing canonical tags
864 + * are never overwritten.
865 + *
866 + * @param array<int,string> $events
867 + * @return array<string, array<string,mixed>>
868 + */
869 + private static function bookingFormFieldDefinitions(array $events): array
870 + {
871 + // Custom/dynamic booking-form fields are a Pro-module feature. When the
872 + // Dynamic Form Field module is off the form is fixed, so we don't surface
873 + // these extra tags — free installs keep their existing tag list unchanged.
874 + if (!function_exists('apply_filters') || !apply_filters('yatra_dynamic_form_field_enabled', false)) {
875 + return [];
876 + }
877 + if (!function_exists('yatra_get_booking_form_config')) {
878 + return [];
879 + }
880 +
881 + $config = yatra_get_booking_form_config();
882 + if (!is_array($config)) {
883 + return [];
884 + }
885 +
886 + $sections = [
887 + 'contact_form' => ['prefix' => 'contact_', 'category' => self::CATEGORY_CUSTOMER],
888 + 'emergency_contact_form' => ['prefix' => 'emergency_', 'category' => self::CATEGORY_BOOKING],
889 + ];
890 +
891 + $defs = [];
892 + foreach ($sections as $sectionKey => $meta) {
893 + $section = $config[$sectionKey] ?? null;
894 + if (!is_array($section) || (isset($section['enabled']) && !$section['enabled'])) {
895 + continue;
896 + }
897 + // Fields the section can ask on ANY trip: the global list plus every
898 + // per-trip condition's list (Pro) — a field that only a "Trekking"
899 + // version of the form asks still needs its merge tag.
900 + $fields = is_array($section['fields'] ?? null) ? $section['fields'] : [];
901 + foreach ((array) ($section['conditions'] ?? []) as $condition) {
902 + if (is_array($condition) && is_array($condition['fields'] ?? null)) {
903 + $fields = array_merge($fields, $condition['fields']);
904 + }
905 + }
906 + foreach ($fields as $field) {
907 + if (!is_array($field) || empty($field['enabled']) || empty($field['id'])) {
908 + continue;
909 + }
910 + // Text blocks are display-only content, not inputs — they hold no
911 + // booking value, so they must not become email merge tags.
912 + if (($field['type'] ?? '') === 'text_block') {
913 + continue;
914 + }
915 + $id = sanitize_key((string) $field['id']);
916 + if ($id === '') {
917 + continue;
918 + }
919 + $tagKey = $meta['prefix'] . $id;
920 + if (isset($defs[$tagKey])) {
921 + continue;
922 + }
923 + $label = (string) ($field['label'] ?? ucwords(str_replace('_', ' ', $id)));
924 + $defs[$tagKey] = [
925 + 'key' => $tagKey,
926 + 'label' => $label,
927 + /* translators: %s: booking form field label. */
928 + 'description' => sprintf(__('Booking form field: %s', 'yatra'), $label),
929 + 'category' => $meta['category'],
930 + 'sample' => '',
931 + 'events' => $events,
932 + ];
933 + }
934 + }
935 +
936 + return $defs;
831 937 }
832 938
833 939 /**
834 940 * Return tag definitions grouped by category, optionally filtered to