PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | app/Services/TransactionalEmailTemplateService.php +412 -36 3.0.4 → 3.0.17 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,55 @@
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 + $sent = EmailService::send(
523 + $to,
524 + $rendered['subject'],
525 + $rendered['body'],
526 + ['Content-Type: text/html; charset=UTF-8']
527 + );
528 +
529 + /**
530 + * A transactional email core just sent.
531 + *
532 + * Yatra Pro writes the Email Logs, but only from its own modules —
533 + * nothing records what core sends, so a booking confirmation or a
534 + * review request left no trace and an operator looking for "which
535 + * email went to this customer" found a log that quietly covered
536 + * only part of the picture. Pro listens for this and logs it.
537 + *
538 + * @param string $type Template type, e.g. review_request.
539 + * @param string $to Recipient address.
540 + * @param string $subject Rendered subject.
541 + * @param string $body Rendered body.
542 + * @param bool $sent Whether wp_mail() accepted it.
543 + * @param array<string, mixed> $variables Merge variables used to render.
544 + */
545 + do_action('yatra_transactional_email_sent', $type, $to, $rendered['subject'], $rendered['body'], $sent, $variables);
546 +
547 + return $sent;
548 + } finally {
549 + self::$dispatchingType = $previousType;
550 + }
333 551 }
334 552
335 553 /**
336 554 * @param array<string, string|int|float> $variables
@@ -352,8 +570,38 @@
352 570 return self::renderWithStringTemplates($type, $subjectTpl, $bodyTpl, $variables);
353 571 }
354 572
355 573 /**
574 + * Would the template that actually gets sent for $type render the
575 + * verification link ({{verification_link}})? Guest checkout can't complete
576 + * without it, so the checkout controller uses this to decide whether an
577 + * operator's customised verification template is safe to use, or whether to
578 + * fall back to the built-in default. Respects Pro ownership: a Pro DB
579 + * template reports its raw body via `yatra_transactional_email_effective_body`;
580 + * otherwise the core option body is checked, and an empty option means the
581 + * built-in default (which always includes the link) is used.
582 + */
583 + public static function templateRendersVerificationLink(string $type): bool
584 + {
585 + $effective = apply_filters('yatra_transactional_email_effective_body', null, $type);
586 + if (is_string($effective) && $effective !== '') {
587 + return strpos($effective, 'verification_link') !== false;
588 + }
589 +
590 + $map = self::typeToSettingsKeys();
591 + if (!isset($map[$type])) {
592 + return false;
593 + }
594 +
595 + $body = SettingsService::getString($map[$type]['body'], '');
596 + if (trim($body) === '') {
597 + return true; // no custom body → built-in default is used, which always carries the link
598 + }
599 +
600 + return strpos($body, 'verification_link') !== false;
601 + }
602 +
603 + /**
356 604 * @param array<string, string|int|float> $variables
357 605 * @return array<string, string>
358 606 */
359 607 private static function mergeDefaultVariables(array $variables): array
@@ -393,8 +641,9 @@
393 641 if (!isset($variables['details_html']) || trim($variables['details_html']) === '') {
394 642 $variables['details_html'] = self::fallbackBookingDetailsHtml($variables);
395 643 }
396 644 if (!isset($variables['footer_note']) || trim($variables['footer_note']) === '') {
645 + /* translators: %s: site name. */
397 646 $variables['footer_note'] = sprintf(__('— %s', 'yatra'), get_bloginfo('name'));
398 647 }
399 648 }
400 649
@@ -492,83 +741,139 @@
492 741 $ref = $v['booking_reference'] ?? $v['booking_id'] ?? '';
493 742
494 743 switch ($type) {
495 744 case self::TYPE_BOOKING_CONFIRMATION:
496 - return sprintf(__('✈️ [%s] Booking update · %s', 'yatra'), $site, $ref);
745 + /* translators: 1: site name, 2: booking reference. */
746 + return sprintf(__('✈️ [%1$s] Booking update · %2$s', 'yatra'), $site, $ref);
497 747
498 748 case self::TYPE_PAYMENT_CONFIRMATION:
499 - return sprintf(__('✅ [%s] Payment received · %s', 'yatra'), $site, $ref);
749 + /* translators: 1: site name, 2: booking reference. */
750 + return sprintf(__('✅ [%1$s] Payment received · %2$s', 'yatra'), $site, $ref);
500 751
752 + case self::TYPE_PARTIAL_PAYMENT_RECEIVED:
753 + /* translators: 1: site name, 2: booking reference. */
754 + return sprintf(__('💳 [%1$s] Part payment received · %2$s', 'yatra'), $site, $ref);
755 +
501 756 case self::TYPE_BOOKING_CANCELLATION:
502 - return sprintf(__('📋 [%s] Booking cancelled · %s', 'yatra'), $site, $ref);
757 + /* translators: 1: site name, 2: booking reference. */
758 + return sprintf(__('📋 [%1$s] Booking cancelled · %2$s', 'yatra'), $site, $ref);
503 759
504 760 case self::TYPE_BOOKING_REMINDER:
505 - return sprintf(__('🗓️ [%s] Your trip is coming up · %s', 'yatra'), $site, $ref);
761 + /* translators: 1: site name, 2: booking reference. */
762 + return sprintf(__('🗓️ [%1$s] Your trip is coming up · %2$s', 'yatra'), $site, $ref);
506 763
507 764 case self::TYPE_ADMIN_NEW_BOOKING:
508 - return sprintf(__('🔔 [%s] New booking · %s (#%s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
765 + /* translators: 1: site name, 2: booking reference, 3: booking ID. */
766 + return sprintf(__('🔔 [%1$s] New booking · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
509 767
510 768 case self::TYPE_ADMIN_PAYMENT_RECEIVED:
511 - return sprintf(__('✅ [%s] Payment received · %s (#%s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
769 + /* translators: 1: site name, 2: booking reference, 3: booking ID. */
770 + return sprintf(__('✅ [%1$s] Payment received · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
512 771
513 772 case self::TYPE_ADMIN_BOOKING_CANCELLED:
514 - return sprintf(__('📋 [%s] Booking cancelled · %s (#%s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
773 + /* translators: 1: site name, 2: booking reference, 3: booking ID. */
774 + return sprintf(__('📋 [%1$s] Booking cancelled · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
515 775
516 776 case self::TYPE_TRIP_CONSENT_REQUEST:
517 777 $formName = $v['form_name'] ?? __('consent form', 'yatra');
518 778
519 - return sprintf(__('📝 [%s] Action required · %s', 'yatra'), $site, $formName);
779 + /* translators: 1: site name, 2: consent form name. */
780 + return sprintf(__('📝 [%1$s] Action required · %2$s', 'yatra'), $site, $formName);
520 781
521 782 case self::TYPE_CUSTOMER_EMAIL_VERIFICATION:
783 + /* translators: %s: site name. */
522 784 return sprintf(__('✉️ [%s] Verify your email address', 'yatra'), $site);
523 785
786 + case self::TYPE_GUEST_EMAIL_VERIFICATION:
787 + // Distinct subject so customers can tell apart "verify
788 + // your account" from "verify to complete your booking".
789 + /* translators: %s: site name. */
790 + return sprintf(__('✉️ [%s] Verify your email to complete your booking', 'yatra'), $site);
791 +
792 + case self::TYPE_ACCOUNT_EMAIL_CHANGE_REQUEST:
793 + /* translators: %s: site name. */
794 + return sprintf(__('✉️ [%s] Confirm your new email address', 'yatra'), $site);
795 +
796 + case self::TYPE_ACCOUNT_EMAIL_CHANGED:
797 + /* translators: %s: site name. */
798 + return sprintf(__('🔔 [%s] Your email address was changed', 'yatra'), $site);
799 +
524 800 case self::TYPE_BOOKING_COMPLETED:
525 - return sprintf(__('🌟 [%s] Trip complete · %s', 'yatra'), $site, $ref);
801 + /* translators: 1: site name, 2: booking reference. */
802 + return sprintf(__('🌟 [%1$s] Trip complete · %2$s', 'yatra'), $site, $ref);
526 803
527 804 case self::TYPE_BOOKING_EXPIRED_CUSTOMER:
528 - return sprintf(__('⏱️ [%s] Booking expired · %s', 'yatra'), $site, $ref);
805 + /* translators: 1: site name, 2: booking reference. */
806 + return sprintf(__('⏱️ [%1$s] Booking expired · %2$s', 'yatra'), $site, $ref);
529 807
530 808 case self::TYPE_ADMIN_BOOKING_EXPIRED:
531 - return sprintf(__('⏱️ [%s] Booking expired · %s (#%s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
809 + /* translators: 1: site name, 2: booking reference, 3: booking ID. */
810 + return sprintf(__('⏱️ [%1$s] Booking expired · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? '');
532 811
533 812 case self::TYPE_SCHEDULED_PAYMENT_REMINDER:
534 - return sprintf(__('💳 [%s] Upcoming payment · %s', 'yatra'), $site, $ref);
813 + /* translators: 1: site name, 2: booking reference. */
814 + return sprintf(__('💳 [%1$s] Upcoming payment · %2$s', 'yatra'), $site, $ref);
535 815
536 816 case self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED:
537 - return sprintf(__('✅ [%s] Scheduled payment received · %s', 'yatra'), $site, $ref);
817 + /* translators: 1: site name, 2: booking reference. */
818 + return sprintf(__('✅ [%1$s] Scheduled payment received · %2$s', 'yatra'), $site, $ref);
538 819
539 820 case self::TYPE_SCHEDULED_PAYMENT_FAILED:
540 - return sprintf(__('⚠️ [%s] Payment issue · %s', 'yatra'), $site, $ref);
821 + /* translators: 1: site name, 2: booking reference. */
822 + return sprintf(__('⚠️ [%1$s] Payment issue · %2$s', 'yatra'), $site, $ref);
541 823
542 824 case self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED:
543 - return sprintf(__('⚠️ [%s] Scheduled payment failed · %s', 'yatra'), $site, $ref);
825 + /* translators: 1: site name, 2: booking reference. */
826 + return sprintf(__('⚠️ [%1$s] Scheduled payment failed · %2$s', 'yatra'), $site, $ref);
544 827
545 828 case self::TYPE_ENQUIRY_ADMIN:
546 829 $who = $v['customer_name'] ?? __('Customer', 'yatra');
547 830
548 - return sprintf(__('💬 [%s] New enquiry · %s', 'yatra'), $site, $who);
831 + /* translators: 1: site name, 2: customer name. */
832 + return sprintf(__('💬 [%1$s] New enquiry · %2$s', 'yatra'), $site, $who);
549 833
550 834 case self::TYPE_ENQUIRY_CUSTOMER_RECEIVED:
835 + /* translators: %s: site name. */
551 836 return sprintf(__('✉️ [%s] We received your message', 'yatra'), $site);
552 837
553 838 case self::TYPE_ENQUIRY_CUSTOMER_RESPONSE:
839 + /* translators: %s: site name. */
554 840 return sprintf(__('💬 [%s] Re: your enquiry', 'yatra'), $site);
555 841
556 842 case self::TYPE_REVIEW_REQUEST:
557 843 $trip = $v['trip_name'] ?? __('your trip', 'yatra');
558 844
559 - return sprintf(__('⭐ [%s] How was %s?', 'yatra'), $site, $trip);
845 + /* translators: 1: site name, 2: trip name. */
846 + return sprintf(__('⭐ [%1$s] How was %2$s?', 'yatra'), $site, $trip);
560 847
561 848 case self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST:
849 + /* translators: %s: site name. */
562 850 return sprintf(__('🛒 [%s] Complete your booking', 'yatra'), $site);
563 851
564 852 case self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND:
853 + /* translators: %s: site name. */
565 854 return sprintf(__('⏳ [%s] Still interested? Your booking is waiting', 'yatra'), $site);
566 855
567 856 case self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL:
857 + /* translators: %s: site name. */
568 858 return sprintf(__('⚠️ [%s] Final reminder: complete your booking', 'yatra'), $site);
569 859
570 860 default:
861 + // Pro modules register their own types via
862 + // `yatra_transactional_email_type_to_keys` — they
863 + // supply default copy through this filter. Returning
864 + // empty string means "no extension claimed this type"
865 + // and we fall back to the generic notification line.
866 + $custom = (string) apply_filters(
867 + 'yatra_transactional_email_default_subject',
868 + '',
869 + $type,
870 + $v
871 + );
872 + if ($custom !== '') {
873 + return $custom;
874 + }
875 + /* translators: %s: site name. */
571 876 return sprintf(__('✉️ [%s] Notification', 'yatra'), $site);
572 877 }
573 878 }
574 879
@@ -583,8 +888,11 @@
583 888
584 889 case self::TYPE_PAYMENT_CONFIRMATION:
585 890 return EmailTemplateDefaults::fallbackTransactionalPayment($v);
586 891
892 + case self::TYPE_PARTIAL_PAYMENT_RECEIVED:
893 + return EmailTemplateDefaults::fallbackTransactionalPartialPayment($v);
894 +
587 895 case self::TYPE_BOOKING_CANCELLATION:
588 896 return EmailTemplateDefaults::fallbackTransactionalCancellation($v);
589 897
590 898 case self::TYPE_BOOKING_REMINDER:
@@ -604,8 +912,24 @@
604 912
605 913 case self::TYPE_CUSTOMER_EMAIL_VERIFICATION:
606 914 return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v);
607 915
916 + case self::TYPE_GUEST_EMAIL_VERIFICATION:
917 + // Reuse the customer-verification body. The flow is
918 + // similar — click a magic link to prove ownership of
919 + // the address — and operators that have already
920 + // customised the customer-verification copy get a
921 + // consistent look across both. Differentiating copy is
922 + // injected at call-time via the intro_paragraph /
923 + // footer_note merge tags by the booking handler.
924 + return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v);
925 +
926 + case self::TYPE_ACCOUNT_EMAIL_CHANGE_REQUEST:
927 + return EmailTemplateDefaults::fallbackTransactionalAccountEmailChangeRequest($v);
928 +
929 + case self::TYPE_ACCOUNT_EMAIL_CHANGED:
930 + return EmailTemplateDefaults::fallbackTransactionalAccountEmailChanged($v);
931 +
608 932 case self::TYPE_BOOKING_COMPLETED:
609 933 return EmailTemplateDefaults::fallbackTransactionalBookingCompleted($v);
610 934
611 935 case self::TYPE_BOOKING_EXPIRED_CUSTOMER:
@@ -647,8 +971,22 @@
647 971 case self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL:
648 972 return EmailTemplateDefaults::fallbackTransactionalAbandonedBookingRecoveryFinal($v);
649 973
650 974 default:
975 + // Pro modules register their own types via
976 + // `yatra_transactional_email_type_to_keys` — they
977 + // supply default body markup through this filter.
978 + // Returning empty string falls back to the generic
979 + // notification block.
980 + $custom = (string) apply_filters(
981 + 'yatra_transactional_email_default_body',
982 + '',
983 + $type,
984 + $v
985 + );
986 + if ($custom !== '') {
987 + return $custom;
988 + }
651 989 return EmailTemplateLayout::customer(
652 990 '✉️',
653 991 __('Notification', 'yatra'),
654 992 '<p style="margin:0;color:#475569;">' . esc_html__('This is an automated message from your travel site.', 'yatra') . '</p>',
@@ -676,8 +1014,9 @@
676 1014 $travelDate = !empty($booking->travel_date)
677 1015 ? date_i18n(get_option('date_format'), strtotime((string) $booking->travel_date))
678 1016 : '';
679 1017
1018 + $bookingId = (int) ($booking->id ?? 0);
680 1019 $base = [
681 1020 'customer_name' => trim((string) (($booking->contact_first_name ?? '') . ' ' . ($booking->contact_last_name ?? ''))),
682 1021 'customer_first_name' => (string) ($booking->contact_first_name ?? ''),
683 1022 'customer_last_name' => (string) ($booking->contact_last_name ?? ''),
@@ -683,9 +1022,13 @@
683 1022 'customer_last_name' => (string) ($booking->contact_last_name ?? ''),
684 1023 'customer_email' => (string) ($booking->contact_email ?? ''),
685 1024 'customer_phone' => (string) ($booking->contact_phone ?? ''),
686 1025 'booking_reference' => (string) ($booking->reference ?? ''),
687 - 'booking_id' => (string) (int) ($booking->id ?? 0),
1026 + 'booking_id' => (string) $bookingId,
1027 + 'booking_url' => $bookingId > 0 ? home_url('/my-account/bookings/' . $bookingId) : home_url('/'),
1028 + // Trip context for per-trip template selection (Pro overrides) and
1029 + // for {{trip_id}}; "0" when the booking has no trip.
1030 + 'trip_id' => (string) (int) ($booking->trip_id ?? 0),
688 1031 'trip_name' => (string) ($booking->trip_title ?? ''),
689 1032 'trip_url' => !empty($booking->trip_slug)
690 1033 ? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode((string) $booking->trip_slug) . '/')
691 1034 : home_url('/'),
@@ -692,8 +1035,22 @@
692 1035 'travel_date' => $travelDate,
693 1036 'travelers_count' => (string) (int) ($booking->travelers_count ?? 0),
694 1037 'total_amount_formatted' => yatra_format_price((float) ($booking->total_amount ?? 0)),
695 1038 'amount_due_formatted' => yatra_format_price((float) ($booking->amount_due ?? 0)),
1039 + // Aliases for the legacy / customer-customised template
1040 + // syntax: many templates (including ones edited via Settings
1041 + // → Email Templates) reference `{{total_amount}}` and
1042 + // `{{balance_due}}` directly rather than the
1043 + // `_formatted` variants. Without these aliases the
1044 + // placeholders survived unsubstituted into the rendered
1045 + // email body. Aliases use the same formatted-with-currency
1046 + // value as the canonical keys above so templates remain
1047 + // visually consistent regardless of which name is used.
1048 + 'total_amount' => yatra_format_price((float) ($booking->total_amount ?? 0)),
1049 + 'balance_due' => yatra_format_price((float) ($booking->amount_due ?? 0)),
1050 + 'amount_due' => yatra_format_price((float) ($booking->amount_due ?? 0)),
1051 + 'amount_paid' => yatra_format_price((float) ($booking->amount_paid ?? 0)),
1052 + 'amount_paid_formatted' => yatra_format_price((float) ($booking->amount_paid ?? 0)),
696 1053 'currency' => $currency,
697 1054 'booking_status' => (string) ($booking->status ?? ''),
698 1055 'payment_status' => (string) ($booking->payment_status ?? ''),
699 1056 'admin_url' => admin_url('admin.php?page=yatra'),
@@ -741,18 +1098,37 @@
741 1098 $tripUrl = $tripSlug !== ''
742 1099 ? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode($tripSlug) . '/')
743 1100 : home_url('/');
744 1101
745 - return [
1102 + $created = (string) ($enquiry->created_at ?? '');
1103 + $enquiryDate = $created !== ''
1104 + ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($created) ?: time())
1105 + : '';
1106 +
1107 + $vars = [
746 1108 'customer_name' => (string) ($enquiry->name ?? ''),
747 1109 'customer_email' => (string) ($enquiry->email ?? ''),
748 1110 'customer_phone' => (string) ($enquiry->phone ?? ''),
1111 + 'enquiry_id' => (string) (int) ($enquiry->id ?? 0),
1112 + 'enquiry_date' => $enquiryDate,
1113 + 'subject' => (string) ($enquiry->subject ?? ''),
749 1114 'trip_name' => $trip !== '' ? $trip : __('General enquiry', 'yatra'),
750 1115 'trip_url' => $tripUrl,
751 1116 'message' => nl2br(esc_html((string) ($enquiry->message ?? ''))),
752 - 'response' => $responsePlain !== '' ? nl2br(esc_html($responsePlain)) : '',
753 - 'response_message' => $responsePlain !== '' ? nl2br(esc_html($responsePlain)) : '',
1117 + 'original_message' => (string) ($enquiry->message ?? ''),
754 1118 ];
1119 +
1120 + // Response-only tags are injected solely on the response email so the
1121 + // sidebar for `enquiry.created` doesn't surface tags that would render
1122 + // empty in that context.
1123 + if ($responsePlain !== '') {
1124 + $responseHtml = nl2br(esc_html($responsePlain));
1125 + $vars['response'] = $responseHtml;
1126 + $vars['response_message'] = $responseHtml;
1127 + $vars['response_date'] = date_i18n(get_option('date_format') . ' ' . get_option('time_format'));
1128 + }
1129 +
1130 + return $vars;
755 1131 }
756 1132
757 1133 /**
758 1134 * @param object $booking Booking row (contact_*, reference, …)