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/TransactionalEmailTemplateService.php +392 -36 3.0.4 → 3.0.16 View file →
@@ -13,8 +13,17 @@
13 13 public const TYPE_BOOKING_CONFIRMATION = 'booking_confirmation';
14 14
15 15 public const TYPE_PAYMENT_CONFIRMATION = 'payment_confirmation';
16 16
17 + /**
18 + * Partial payment received (deposit / instalment), where a balance remains.
19 + *
20 + * Opt-in: until an operator enables it, every payment keeps using
21 + * TYPE_PAYMENT_CONFIRMATION exactly as before, so existing sites see no
22 + * change. Only relevant when partial payments or deposits are switched on.
23 + */
24 + public const TYPE_PARTIAL_PAYMENT_RECEIVED = 'partial_payment_received';
25 +
17 26 public const TYPE_BOOKING_CANCELLATION = 'booking_cancellation';
18 27
19 28 public const TYPE_BOOKING_REMINDER = 'booking_reminder';
20 29
@@ -29,8 +38,25 @@
29 38
30 39 /** Customer account email verification (e.g. checkout registration). */
31 40 public const TYPE_CUSTOMER_EMAIL_VERIFICATION = 'customer_email_verification';
32 41
42 + /**
43 + * Guest-checkout email verification — sent BEFORE payment when
44 + * `require_guest_email_verification` is on. The booking is held
45 + * in `pending_verification` status until the customer clicks the
46 + * magic link. Distinct from `customer_email_verification` because
47 + * (a) the recipient is not a registered user, and (b) the link
48 + * resumes the in-flight booking flow rather than completing
49 + * account registration.
50 + */
51 + public const TYPE_GUEST_EMAIL_VERIFICATION = 'guest_email_verification';
52 +
53 + /** Confirmation link sent to the NEW address when a customer changes their account email. */
54 + public const TYPE_ACCOUNT_EMAIL_CHANGE_REQUEST = 'account_email_change_request';
55 +
56 + /** Security notice sent to the OLD address once an account email change is confirmed. */
57 + public const TYPE_ACCOUNT_EMAIL_CHANGED = 'account_email_changed';
58 +
33 59 public const TYPE_BOOKING_COMPLETED = 'booking_completed';
34 60
35 61 public const TYPE_BOOKING_EXPIRED_CUSTOMER = 'booking_expired_customer';
36 62
@@ -64,8 +90,9 @@
64 90 {
65 91 $map = [
66 92 'booking_confirmation' => self::TYPE_BOOKING_CONFIRMATION,
67 93 'payment_received' => self::TYPE_PAYMENT_CONFIRMATION,
94 + 'partial_payment_received' => self::TYPE_PARTIAL_PAYMENT_RECEIVED,
68 95 'booking_cancelled' => self::TYPE_BOOKING_CANCELLATION,
69 96 'trip_reminder' => self::TYPE_BOOKING_REMINDER,
70 97 'admin_new_booking' => self::TYPE_ADMIN_NEW_BOOKING,
71 98 'admin_payment_received' => self::TYPE_ADMIN_PAYMENT_RECEIVED,
@@ -71,8 +98,11 @@
71 98 'admin_payment_received' => self::TYPE_ADMIN_PAYMENT_RECEIVED,
72 99 'admin_booking_cancelled' => self::TYPE_ADMIN_BOOKING_CANCELLED,
73 100 'trip_consent_request' => self::TYPE_TRIP_CONSENT_REQUEST,
74 101 'customer_email_verification' => self::TYPE_CUSTOMER_EMAIL_VERIFICATION,
102 + 'guest_email_verification' => self::TYPE_GUEST_EMAIL_VERIFICATION,
103 + 'account_email_change_request' => self::TYPE_ACCOUNT_EMAIL_CHANGE_REQUEST,
104 + 'account_email_changed' => self::TYPE_ACCOUNT_EMAIL_CHANGED,
75 105 'booking_completed' => self::TYPE_BOOKING_COMPLETED,
76 106 'booking_expired_customer' => self::TYPE_BOOKING_EXPIRED_CUSTOMER,
77 107 'admin_booking_expired' => self::TYPE_ADMIN_BOOKING_EXPIRED,
78 108 'scheduled_payment_reminder' => self::TYPE_SCHEDULED_PAYMENT_REMINDER,
@@ -137,9 +167,21 @@
137 167 return ['subject' => '', 'body' => ''];
138 168 }
139 169
140 170 if ($subjectTpl === '') {
171 + // No operator-configured subject → use the built-in default, unless
172 + // a caller supplied a context-specific override (e.g. guest checkout
173 + // substitutes its booking-oriented subject for the account default).
174 + // This is a FALLBACK only: when the operator HAS configured a subject
175 + // (the `else` branch) it always wins — otherwise `_subject_override`
176 + // would clobber a configured subject with the generic default.
141 177 $subject = self::defaultSubject($type, $variables);
178 + if (isset($variables['_subject_override'])
179 + && is_string($variables['_subject_override'])
180 + && $variables['_subject_override'] !== ''
181 + ) {
182 + $subject = $variables['_subject_override'];
183 + }
142 184 } else {
143 185 $subject = self::parseTemplate($subjectTpl, $variables);
144 186 }
145 187
@@ -159,9 +201,9 @@
159 201 * @return array<string, string>
160 202 */
161 203 private static function typeToSettingsKeys(): array
162 204 {
163 - return [
205 + $defaults = [
164 206 self::TYPE_BOOKING_CONFIRMATION => [
165 207 'flag' => 'email_template_booking',
166 208 'subject' => 'email_tpl_booking_subject',
167 209 'body' => 'email_tpl_booking_body',
@@ -170,8 +212,13 @@
170 212 'flag' => 'email_template_confirmation',
171 213 'subject' => 'email_tpl_payment_subject',
172 214 'body' => 'email_tpl_payment_body',
173 215 ],
216 + self::TYPE_PARTIAL_PAYMENT_RECEIVED => [
217 + 'flag' => 'email_template_partial_payment',
218 + 'subject' => 'email_tpl_partial_payment_subject',
219 + 'body' => 'email_tpl_partial_payment_body',
220 + ],
174 221 self::TYPE_BOOKING_CANCELLATION => [
175 222 'flag' => 'email_template_cancellation',
176 223 'subject' => 'email_tpl_cancellation_subject',
177 224 'body' => 'email_tpl_cancellation_body',
@@ -205,8 +252,23 @@
205 252 'flag' => 'email_template_customer_verification',
206 253 'subject' => 'email_tpl_customer_verification_subject',
207 254 'body' => 'email_tpl_customer_verification_body',
208 255 ],
256 + self::TYPE_GUEST_EMAIL_VERIFICATION => [
257 + 'flag' => 'email_template_guest_verification',
258 + 'subject' => 'email_tpl_guest_verification_subject',
259 + 'body' => 'email_tpl_guest_verification_body',
260 + ],
261 + self::TYPE_ACCOUNT_EMAIL_CHANGE_REQUEST => [
262 + 'flag' => 'email_template_account_email_change',
263 + 'subject' => 'email_tpl_account_email_change_subject',
264 + 'body' => 'email_tpl_account_email_change_body',
265 + ],
266 + self::TYPE_ACCOUNT_EMAIL_CHANGED => [
267 + 'flag' => 'email_template_account_email_changed',
268 + 'subject' => 'email_tpl_account_email_changed_subject',
269 + 'body' => 'email_tpl_account_email_changed_body',
270 + ],
209 271 self::TYPE_BOOKING_COMPLETED => [
210 272 'flag' => 'email_template_booking_completed',
211 273 'subject' => 'email_tpl_booking_completed_subject',
212 274 'body' => 'email_tpl_booking_completed_body',
@@ -276,11 +338,137 @@
276 338 'subject' => 'email_tpl_abandoned_booking_recovery_final_subject',
277 339 'body' => 'email_tpl_abandoned_booking_recovery_final_body',
278 340 ],
279 341 ];
342 +
343 + /**
344 + * Allow Pro modules (Team & Access, etc.) to register additional
345 + * transactional template types — each entry must be an array with
346 + * `flag`, `subject`, `body` keys matching the option-name pattern
347 + * used above. Once registered, the type participates in:
348 + * - sendIfEnabled() (flag gate + send)
349 + * - render() / renderWithStringTemplates() (templated subject/body)
350 + * - the Email → Templates UI (auto-discovered via the same map)
351 + *
352 + * Modules also need to hook `yatra_transactional_email_default_subject`
353 + * and `..._default_body` to supply baseline copy for their type.
354 + *
355 + * @param array<string, array{flag:string,subject:string,body:string}> $defaults
356 + */
357 + // Per-template BCC / CC keys are DERIVED from each type's subject key
358 + // (email_tpl_booking_subject -> email_tpl_booking_bcc / _cc) rather than
359 + // written out 26 times. A hand-maintained parallel list is exactly how
360 + // `admin_payment_received` ended up missing from the Pro override map, so
361 + // a new template type now gets its BCC/CC keys automatically — including
362 + // types added by modules through the filter below.
363 + foreach ($defaults as $type => $keys) {
364 + if (empty($keys['subject']) || !is_string($keys['subject'])) {
365 + continue;
366 + }
367 +
368 + $base = preg_replace('/_subject$/', '', $keys['subject']);
369 +
370 + if (!isset($defaults[$type]['bcc'])) {
371 + $defaults[$type]['bcc'] = $base . '_bcc';
372 + }
373 + if (!isset($defaults[$type]['cc'])) {
374 + $defaults[$type]['cc'] = $base . '_cc';
375 + }
376 + }
377 +
378 + return (array) apply_filters('yatra_transactional_email_type_to_keys', $defaults);
280 379 }
281 380
282 381 /**
382 + * Build Cc/Bcc headers for a transactional type from its own settings.
383 + *
384 + * Both are opt-in: an empty setting adds no header, so nothing changes for
385 + * an operator who never fills them in. Multiple comma-separated addresses are
386 + * supported, and anything that is not a valid address is dropped rather than
387 + * handed to the mailer.
388 + *
389 + * @return string[]
390 + */
391 + /**
392 + * The transactional type currently being dispatched, if any.
393 + *
394 + * Pro can take over a send through `yatra_send_transactional_email` and mails
395 + * it through its own service, which means header building here would be
396 + * skipped entirely. Both paths funnel through EmailService::send, so the type
397 + * is recorded for the duration of the dispatch and the Cc/Bcc for that
398 + * template is applied there — one injection point that works whether core or
399 + * Pro actually sends.
400 + *
401 + * @var string
402 + */
403 + private static $dispatchingType = '';
404 +
405 + /**
406 + * Cc/Bcc headers for the send currently in flight, for EmailService.
407 + *
408 + * @return string[]
409 + */
410 + public static function headersForCurrentDispatch(): array
411 + {
412 + if (self::$dispatchingType === '') {
413 + return [];
414 + }
415 +
416 + return self::recipientHeadersForType(self::$dispatchingType);
417 + }
418 +
419 + private static function recipientHeadersForType(string $type): array
420 + {
421 + $map = self::typeToSettingsKeys();
422 +
423 + if (!isset($map[$type])) {
424 + return [];
425 + }
426 +
427 + $headers = [];
428 +
429 + foreach (['Cc' => $map[$type]['cc'] ?? '', 'Bcc' => $map[$type]['bcc'] ?? ''] as $label => $settingKey) {
430 + if ($settingKey === '') {
431 + continue;
432 + }
433 +
434 + $addresses = self::sanitizeAddressList((string) SettingsService::get($settingKey, ''));
435 +
436 + if ($addresses !== []) {
437 + $headers[] = $label . ': ' . implode(', ', $addresses);
438 + }
439 + }
440 +
441 + return $headers;
442 + }
443 +
444 + /**
445 + * Split a comma/semicolon separated address list into valid addresses.
446 + *
447 + * @return string[]
448 + */
449 + public static function sanitizeAddressList(string $raw): array
450 + {
451 + $raw = trim($raw);
452 +
453 + if ($raw === '') {
454 + return [];
455 + }
456 +
457 + $addresses = [];
458 +
459 + foreach (preg_split('/[,;]+/', $raw) as $candidate) {
460 + $candidate = sanitize_email(trim((string) $candidate));
461 +
462 + if ($candidate !== '' && is_email($candidate)) {
463 + $addresses[strtolower($candidate)] = $candidate;
464 + }
465 + }
466 +
467 + return array_values($addresses);
468 + }
469 +
470 + /**
283 471 * Send if the type is enabled in settings. Pro may handle via {@see 'yatra_send_transactional_email'}.
284 472 *
285 473 * Optional string `transactional_context` (e.g. `booking_created`, `status_confirmed`) is passed through
286 474 * to the filter so Pro can choose a different template row for the same TYPE_BOOKING_CONFIRMATION.
@@ -312,25 +500,35 @@
312 500 /**
313 501 * Allow Yatra Pro (or extensions) to send instead of core templates.
314 502 * Return null to use core; true/false if handled.
315 503 */
316 - $handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables);
317 - if ($handled !== null) {
318 - return (bool) $handled;
319 - }
504 + // Mark the type for the whole dispatch — including a Pro takeover — so
505 + // EmailService can apply this template's own Cc/Bcc whichever service
506 + // ends up doing the sending.
507 + $previousType = self::$dispatchingType;
508 + self::$dispatchingType = $type;
320 509
321 - if (!SettingsService::isEnabled($flag)) {
322 - return false;
323 - }
510 + try {
511 + $handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables);
512 + if ($handled !== null) {
513 + return (bool) $handled;
514 + }
324 515
325 - $rendered = self::render($type, $variables);
516 + if (!SettingsService::isEnabled($flag)) {
517 + return false;
518 + }
326 519
327 - return EmailService::send(
328 - $to,
329 - $rendered['subject'],
330 - $rendered['body'],
331 - ['Content-Type: text/html; charset=UTF-8']
332 - );
520 + $rendered = self::render($type, $variables);
521 +
522 + return EmailService::send(
523 + $to,
524 + $rendered['subject'],
525 + $rendered['body'],
526 + ['Content-Type: text/html; charset=UTF-8']
527 + );
528 + } finally {
529 + self::$dispatchingType = $previousType;
530 + }
333 531 }
334 532
335 533 /**
336 534 * @param array<string, string|int|float> $variables
@@ -352,8 +550,38 @@
352 550 return self::renderWithStringTemplates($type, $subjectTpl, $bodyTpl, $variables);
353 551 }
354 552
355 553 /**
554 + * Would the template that actually gets sent for $type render the
555 + * verification link ({{verification_link}})? Guest checkout can't complete
556 + * without it, so the checkout controller uses this to decide whether an
557 + * operator's customised verification template is safe to use, or whether to
558 + * fall back to the built-in default. Respects Pro ownership: a Pro DB
559 + * template reports its raw body via `yatra_transactional_email_effective_body`;
560 + * otherwise the core option body is checked, and an empty option means the
561 + * built-in default (which always includes the link) is used.
562 + */
563 + public static function templateRendersVerificationLink(string $type): bool
564 + {
565 + $effective = apply_filters('yatra_transactional_email_effective_body', null, $type);
566 + if (is_string($effective) && $effective !== '') {
567 + return strpos($effective, 'verification_link') !== false;
568 + }
569 +
570 + $map = self::typeToSettingsKeys();
571 + if (!isset($map[$type])) {
572 + return false;
573 + }
574 +
575 + $body = SettingsService::getString($map[$type]['body'], '');
576 + if (trim($body) === '') {
577 + return true; // no custom body → built-in default is used, which always carries the link
578 + }
579 +
580 + return strpos($body, 'verification_link') !== false;
581 + }
582 +
583 + /**
356 584 * @param array<string, string|int|float> $variables
357 585 * @return array<string, string>
358 586 */
359 587 private static function mergeDefaultVariables(array $variables): array
@@ -393,8 +621,9 @@
393 621 if (!isset($variables['details_html']) || trim($variables['details_html']) === '') {
394 622 $variables['details_html'] = self::fallbackBookingDetailsHtml($variables);
395 623 }
396 624 if (!isset($variables['footer_note']) || trim($variables['footer_note']) === '') {
625 + /* translators: %s: site name. */
397 626 $variables['footer_note'] = sprintf(__('— %s', 'yatra'), get_bloginfo('name'));
398 627 }
399 628 }
400 629
@@ -492,83 +721,139 @@
492 721 $ref = $v['booking_reference'] ?? $v['booking_id'] ?? '';
493 722
494 723 switch ($type) {
495 724 case self::TYPE_BOOKING_CONFIRMATION:
496 - return sprintf(__('✈️ [%s] Booking update · %s', 'yatra'), $site, $ref);
725 + /* translators: 1: site name, 2: booking reference. */
726 + return sprintf(__('✈️ [%1$s] Booking update · %2$s', 'yatra'), $site, $ref);
497 727
498 728 case self::TYPE_PAYMENT_CONFIRMATION:
499 - return sprintf(__('✅ [%s] Payment received · %s', 'yatra'), $site, $ref);
729 + /* translators: 1: site name, 2: booking reference. */
730 + return sprintf(__('✅ [%1$s] Payment received · %2$s', 'yatra'), $site, $ref);
500 731
732 + case self::TYPE_PARTIAL_PAYMENT_RECEIVED:
733 + /* translators: 1: site name, 2: booking reference. */
734 + return sprintf(__('💳 [%1$s] Part payment received · %2$s', 'yatra'), $site, $ref);
735 +
501 736 case self::TYPE_BOOKING_CANCELLATION:
502 - return sprintf(__('📋 [%s] Booking cancelled · %s', 'yatra'), $site, $ref);
737 + /* translators: 1: site name, 2: booking reference. */
738 + return sprintf(__('📋 [%1$s] Booking cancelled · %2$s', 'yatra'), $site, $ref);
503 739
504 740 case self::TYPE_BOOKING_REMINDER:
505 - return sprintf(__('🗓️ [%s] Your trip is coming up · %s', 'yatra'), $site, $ref);
741 + /* translators: 1: site name, 2: booking reference. */
742 + return sprintf(__('🗓️ [%1$s] Your trip is coming up · %2$s', 'yatra'), $site, $ref);
506 743
507 744 case self::TYPE_ADMIN_NEW_BOOKING:
508 - return sprintf(__('🔔 [%s] New booking · %s (#%s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
745 + /* translators: 1: site name, 2: booking reference, 3: booking ID. */
746 + return sprintf(__('🔔 [%1$s] New booking · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
509 747
510 748 case self::TYPE_ADMIN_PAYMENT_RECEIVED:
511 - return sprintf(__('✅ [%s] Payment received · %s (#%s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
749 + /* translators: 1: site name, 2: booking reference, 3: booking ID. */
750 + return sprintf(__('✅ [%1$s] Payment received · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
512 751
513 752 case self::TYPE_ADMIN_BOOKING_CANCELLED:
514 - return sprintf(__('📋 [%s] Booking cancelled · %s (#%s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
753 + /* translators: 1: site name, 2: booking reference, 3: booking ID. */
754 + return sprintf(__('📋 [%1$s] Booking cancelled · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
515 755
516 756 case self::TYPE_TRIP_CONSENT_REQUEST:
517 757 $formName = $v['form_name'] ?? __('consent form', 'yatra');
518 758
519 - return sprintf(__('📝 [%s] Action required · %s', 'yatra'), $site, $formName);
759 + /* translators: 1: site name, 2: consent form name. */
760 + return sprintf(__('📝 [%1$s] Action required · %2$s', 'yatra'), $site, $formName);
520 761
521 762 case self::TYPE_CUSTOMER_EMAIL_VERIFICATION:
763 + /* translators: %s: site name. */
522 764 return sprintf(__('✉️ [%s] Verify your email address', 'yatra'), $site);
523 765
766 + case self::TYPE_GUEST_EMAIL_VERIFICATION:
767 + // Distinct subject so customers can tell apart "verify
768 + // your account" from "verify to complete your booking".
769 + /* translators: %s: site name. */
770 + return sprintf(__('✉️ [%s] Verify your email to complete your booking', 'yatra'), $site);
771 +
772 + case self::TYPE_ACCOUNT_EMAIL_CHANGE_REQUEST:
773 + /* translators: %s: site name. */
774 + return sprintf(__('✉️ [%s] Confirm your new email address', 'yatra'), $site);
775 +
776 + case self::TYPE_ACCOUNT_EMAIL_CHANGED:
777 + /* translators: %s: site name. */
778 + return sprintf(__('🔔 [%s] Your email address was changed', 'yatra'), $site);
779 +
524 780 case self::TYPE_BOOKING_COMPLETED:
525 - return sprintf(__('🌟 [%s] Trip complete · %s', 'yatra'), $site, $ref);
781 + /* translators: 1: site name, 2: booking reference. */
782 + return sprintf(__('🌟 [%1$s] Trip complete · %2$s', 'yatra'), $site, $ref);
526 783
527 784 case self::TYPE_BOOKING_EXPIRED_CUSTOMER:
528 - return sprintf(__('⏱️ [%s] Booking expired · %s', 'yatra'), $site, $ref);
785 + /* translators: 1: site name, 2: booking reference. */
786 + return sprintf(__('⏱️ [%1$s] Booking expired · %2$s', 'yatra'), $site, $ref);
529 787
530 788 case self::TYPE_ADMIN_BOOKING_EXPIRED:
531 - return sprintf(__('⏱️ [%s] Booking expired · %s (#%s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
789 + /* translators: 1: site name, 2: booking reference, 3: booking ID. */
790 + return sprintf(__('⏱️ [%1$s] Booking expired · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
532 791
533 792 case self::TYPE_SCHEDULED_PAYMENT_REMINDER:
534 - return sprintf(__('💳 [%s] Upcoming payment · %s', 'yatra'), $site, $ref);
793 + /* translators: 1: site name, 2: booking reference. */
794 + return sprintf(__('💳 [%1$s] Upcoming payment · %2$s', 'yatra'), $site, $ref);
535 795
536 796 case self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED:
537 - return sprintf(__('✅ [%s] Scheduled payment received · %s', 'yatra'), $site, $ref);
797 + /* translators: 1: site name, 2: booking reference. */
798 + return sprintf(__('✅ [%1$s] Scheduled payment received · %2$s', 'yatra'), $site, $ref);
538 799
539 800 case self::TYPE_SCHEDULED_PAYMENT_FAILED:
540 - return sprintf(__('⚠️ [%s] Payment issue · %s', 'yatra'), $site, $ref);
801 + /* translators: 1: site name, 2: booking reference. */
802 + return sprintf(__('⚠️ [%1$s] Payment issue · %2$s', 'yatra'), $site, $ref);
541 803
542 804 case self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED:
543 - return sprintf(__('⚠️ [%s] Scheduled payment failed · %s', 'yatra'), $site, $ref);
805 + /* translators: 1: site name, 2: booking reference. */
806 + return sprintf(__('⚠️ [%1$s] Scheduled payment failed · %2$s', 'yatra'), $site, $ref);
544 807
545 808 case self::TYPE_ENQUIRY_ADMIN:
546 809 $who = $v['customer_name'] ?? __('Customer', 'yatra');
547 810
548 - return sprintf(__('💬 [%s] New enquiry · %s', 'yatra'), $site, $who);
811 + /* translators: 1: site name, 2: customer name. */
812 + return sprintf(__('💬 [%1$s] New enquiry · %2$s', 'yatra'), $site, $who);
549 813
550 814 case self::TYPE_ENQUIRY_CUSTOMER_RECEIVED:
815 + /* translators: %s: site name. */
551 816 return sprintf(__('✉️ [%s] We received your message', 'yatra'), $site);
552 817
553 818 case self::TYPE_ENQUIRY_CUSTOMER_RESPONSE:
819 + /* translators: %s: site name. */
554 820 return sprintf(__('💬 [%s] Re: your enquiry', 'yatra'), $site);
555 821
556 822 case self::TYPE_REVIEW_REQUEST:
557 823 $trip = $v['trip_name'] ?? __('your trip', 'yatra');
558 824
559 - return sprintf(__('⭐ [%s] How was %s?', 'yatra'), $site, $trip);
825 + /* translators: 1: site name, 2: trip name. */
826 + return sprintf(__('⭐ [%1$s] How was %2$s?', 'yatra'), $site, $trip);
560 827
561 828 case self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST:
829 + /* translators: %s: site name. */
562 830 return sprintf(__('🛒 [%s] Complete your booking', 'yatra'), $site);
563 831
564 832 case self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND:
833 + /* translators: %s: site name. */
565 834 return sprintf(__('⏳ [%s] Still interested? Your booking is waiting', 'yatra'), $site);
566 835
567 836 case self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL:
837 + /* translators: %s: site name. */
568 838 return sprintf(__('⚠️ [%s] Final reminder: complete your booking', 'yatra'), $site);
569 839
570 840 default:
841 + // Pro modules register their own types via
842 + // `yatra_transactional_email_type_to_keys` — they
843 + // supply default copy through this filter. Returning
844 + // empty string means "no extension claimed this type"
845 + // and we fall back to the generic notification line.
846 + $custom = (string) apply_filters(
847 + 'yatra_transactional_email_default_subject',
848 + '',
849 + $type,
850 + $v
851 + );
852 + if ($custom !== '') {
853 + return $custom;
854 + }
855 + /* translators: %s: site name. */
571 856 return sprintf(__('✉️ [%s] Notification', 'yatra'), $site);
572 857 }
573 858 }
574 859
@@ -583,8 +868,11 @@
583 868
584 869 case self::TYPE_PAYMENT_CONFIRMATION:
585 870 return EmailTemplateDefaults::fallbackTransactionalPayment($v);
586 871
872 + case self::TYPE_PARTIAL_PAYMENT_RECEIVED:
873 + return EmailTemplateDefaults::fallbackTransactionalPartialPayment($v);
874 +
587 875 case self::TYPE_BOOKING_CANCELLATION:
588 876 return EmailTemplateDefaults::fallbackTransactionalCancellation($v);
589 877
590 878 case self::TYPE_BOOKING_REMINDER:
@@ -604,8 +892,24 @@
604 892
605 893 case self::TYPE_CUSTOMER_EMAIL_VERIFICATION:
606 894 return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v);
607 895
896 + case self::TYPE_GUEST_EMAIL_VERIFICATION:
897 + // Reuse the customer-verification body. The flow is
898 + // similar — click a magic link to prove ownership of
899 + // the address — and operators that have already
900 + // customised the customer-verification copy get a
901 + // consistent look across both. Differentiating copy is
902 + // injected at call-time via the intro_paragraph /
903 + // footer_note merge tags by the booking handler.
904 + return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v);
905 +
906 + case self::TYPE_ACCOUNT_EMAIL_CHANGE_REQUEST:
907 + return EmailTemplateDefaults::fallbackTransactionalAccountEmailChangeRequest($v);
908 +
909 + case self::TYPE_ACCOUNT_EMAIL_CHANGED:
910 + return EmailTemplateDefaults::fallbackTransactionalAccountEmailChanged($v);
911 +
608 912 case self::TYPE_BOOKING_COMPLETED:
609 913 return EmailTemplateDefaults::fallbackTransactionalBookingCompleted($v);
610 914
611 915 case self::TYPE_BOOKING_EXPIRED_CUSTOMER:
@@ -647,8 +951,22 @@
647 951 case self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL:
648 952 return EmailTemplateDefaults::fallbackTransactionalAbandonedBookingRecoveryFinal($v);
649 953
650 954 default:
955 + // Pro modules register their own types via
956 + // `yatra_transactional_email_type_to_keys` — they
957 + // supply default body markup through this filter.
958 + // Returning empty string falls back to the generic
959 + // notification block.
960 + $custom = (string) apply_filters(
961 + 'yatra_transactional_email_default_body',
962 + '',
963 + $type,
964 + $v
965 + );
966 + if ($custom !== '') {
967 + return $custom;
968 + }
651 969 return EmailTemplateLayout::customer(
652 970 '✉️',
653 971 __('Notification', 'yatra'),
654 972 '<p style="margin:0;color:#475569;">' . esc_html__('This is an automated message from your travel site.', 'yatra') . '</p>',
@@ -676,8 +994,9 @@
676 994 $travelDate = !empty($booking->travel_date)
677 995 ? date_i18n(get_option('date_format'), strtotime((string) $booking->travel_date))
678 996 : '';
679 997
998 + $bookingId = (int) ($booking->id ?? 0);
680 999 $base = [
681 1000 'customer_name' => trim((string) (($booking->contact_first_name ?? '') . ' ' . ($booking->contact_last_name ?? ''))),
682 1001 'customer_first_name' => (string) ($booking->contact_first_name ?? ''),
683 1002 'customer_last_name' => (string) ($booking->contact_last_name ?? ''),
@@ -683,9 +1002,13 @@
683 1002 'customer_last_name' => (string) ($booking->contact_last_name ?? ''),
684 1003 'customer_email' => (string) ($booking->contact_email ?? ''),
685 1004 'customer_phone' => (string) ($booking->contact_phone ?? ''),
686 1005 'booking_reference' => (string) ($booking->reference ?? ''),
687 - 'booking_id' => (string) (int) ($booking->id ?? 0),
1006 + 'booking_id' => (string) $bookingId,
1007 + 'booking_url' => $bookingId > 0 ? home_url('/my-account/bookings/' . $bookingId) : home_url('/'),
1008 + // Trip context for per-trip template selection (Pro overrides) and
1009 + // for {{trip_id}}; "0" when the booking has no trip.
1010 + 'trip_id' => (string) (int) ($booking->trip_id ?? 0),
688 1011 'trip_name' => (string) ($booking->trip_title ?? ''),
689 1012 'trip_url' => !empty($booking->trip_slug)
690 1013 ? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode((string) $booking->trip_slug) . '/')
691 1014 : home_url('/'),
@@ -692,8 +1015,22 @@
692 1015 'travel_date' => $travelDate,
693 1016 'travelers_count' => (string) (int) ($booking->travelers_count ?? 0),
694 1017 'total_amount_formatted' => yatra_format_price((float) ($booking->total_amount ?? 0)),
695 1018 'amount_due_formatted' => yatra_format_price((float) ($booking->amount_due ?? 0)),
1019 + // Aliases for the legacy / customer-customised template
1020 + // syntax: many templates (including ones edited via Settings
1021 + // → Email Templates) reference `{{total_amount}}` and
1022 + // `{{balance_due}}` directly rather than the
1023 + // `_formatted` variants. Without these aliases the
1024 + // placeholders survived unsubstituted into the rendered
1025 + // email body. Aliases use the same formatted-with-currency
1026 + // value as the canonical keys above so templates remain
1027 + // visually consistent regardless of which name is used.
1028 + 'total_amount' => yatra_format_price((float) ($booking->total_amount ?? 0)),
1029 + 'balance_due' => yatra_format_price((float) ($booking->amount_due ?? 0)),
1030 + 'amount_due' => yatra_format_price((float) ($booking->amount_due ?? 0)),
1031 + 'amount_paid' => yatra_format_price((float) ($booking->amount_paid ?? 0)),
1032 + 'amount_paid_formatted' => yatra_format_price((float) ($booking->amount_paid ?? 0)),
696 1033 'currency' => $currency,
697 1034 'booking_status' => (string) ($booking->status ?? ''),
698 1035 'payment_status' => (string) ($booking->payment_status ?? ''),
699 1036 'admin_url' => admin_url('admin.php?page=yatra'),
@@ -741,18 +1078,37 @@
741 1078 $tripUrl = $tripSlug !== ''
742 1079 ? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode($tripSlug) . '/')
743 1080 : home_url('/');
744 1081
745 - return [
1082 + $created = (string) ($enquiry->created_at ?? '');
1083 + $enquiryDate = $created !== ''
1084 + ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($created) ?: time())
1085 + : '';
1086 +
1087 + $vars = [
746 1088 'customer_name' => (string) ($enquiry->name ?? ''),
747 1089 'customer_email' => (string) ($enquiry->email ?? ''),
748 1090 'customer_phone' => (string) ($enquiry->phone ?? ''),
1091 + 'enquiry_id' => (string) (int) ($enquiry->id ?? 0),
1092 + 'enquiry_date' => $enquiryDate,
1093 + 'subject' => (string) ($enquiry->subject ?? ''),
749 1094 'trip_name' => $trip !== '' ? $trip : __('General enquiry', 'yatra'),
750 1095 'trip_url' => $tripUrl,
751 1096 'message' => nl2br(esc_html((string) ($enquiry->message ?? ''))),
752 - 'response' => $responsePlain !== '' ? nl2br(esc_html($responsePlain)) : '',
753 - 'response_message' => $responsePlain !== '' ? nl2br(esc_html($responsePlain)) : '',
1097 + 'original_message' => (string) ($enquiry->message ?? ''),
754 1098 ];
1099 +
1100 + // Response-only tags are injected solely on the response email so the
1101 + // sidebar for `enquiry.created` doesn't surface tags that would render
1102 + // empty in that context.
1103 + if ($responsePlain !== '') {
1104 + $responseHtml = nl2br(esc_html($responsePlain));
1105 + $vars['response'] = $responseHtml;
1106 + $vars['response_message'] = $responseHtml;
1107 + $vars['response_date'] = date_i18n(get_option('date_format') . ' ' . get_option('time_format'));
1108 + }
1109 +
1110 + return $vars;
755 1111 }
756 1112
757 1113 /**
758 1114 * @param object $booking Booking row (contact_*, reference, …)