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 +253 -14 3.0.5.1 → 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
@@ -40,8 +49,14 @@
40 49 * account registration.
41 50 */
42 51 public const TYPE_GUEST_EMAIL_VERIFICATION = 'guest_email_verification';
43 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 +
44 59 public const TYPE_BOOKING_COMPLETED = 'booking_completed';
45 60
46 61 public const TYPE_BOOKING_EXPIRED_CUSTOMER = 'booking_expired_customer';
47 62
@@ -75,8 +90,9 @@
75 90 {
76 91 $map = [
77 92 'booking_confirmation' => self::TYPE_BOOKING_CONFIRMATION,
78 93 'payment_received' => self::TYPE_PAYMENT_CONFIRMATION,
94 + 'partial_payment_received' => self::TYPE_PARTIAL_PAYMENT_RECEIVED,
79 95 'booking_cancelled' => self::TYPE_BOOKING_CANCELLATION,
80 96 'trip_reminder' => self::TYPE_BOOKING_REMINDER,
81 97 'admin_new_booking' => self::TYPE_ADMIN_NEW_BOOKING,
82 98 'admin_payment_received' => self::TYPE_ADMIN_PAYMENT_RECEIVED,
@@ -83,8 +99,10 @@
83 99 'admin_booking_cancelled' => self::TYPE_ADMIN_BOOKING_CANCELLED,
84 100 'trip_consent_request' => self::TYPE_TRIP_CONSENT_REQUEST,
85 101 'customer_email_verification' => self::TYPE_CUSTOMER_EMAIL_VERIFICATION,
86 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,
87 105 'booking_completed' => self::TYPE_BOOKING_COMPLETED,
88 106 'booking_expired_customer' => self::TYPE_BOOKING_EXPIRED_CUSTOMER,
89 107 'admin_booking_expired' => self::TYPE_ADMIN_BOOKING_EXPIRED,
90 108 'scheduled_payment_reminder' => self::TYPE_SCHEDULED_PAYMENT_REMINDER,
@@ -149,9 +167,21 @@
149 167 return ['subject' => '', 'body' => ''];
150 168 }
151 169
152 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.
153 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 + }
154 184 } else {
155 185 $subject = self::parseTemplate($subjectTpl, $variables);
156 186 }
157 187
@@ -182,8 +212,13 @@
182 212 'flag' => 'email_template_confirmation',
183 213 'subject' => 'email_tpl_payment_subject',
184 214 'body' => 'email_tpl_payment_body',
185 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 + ],
186 221 self::TYPE_BOOKING_CANCELLATION => [
187 222 'flag' => 'email_template_cancellation',
188 223 'subject' => 'email_tpl_cancellation_subject',
189 224 'body' => 'email_tpl_cancellation_body',
@@ -222,8 +257,18 @@
222 257 'flag' => 'email_template_guest_verification',
223 258 'subject' => 'email_tpl_guest_verification_subject',
224 259 'body' => 'email_tpl_guest_verification_body',
225 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 + ],
226 271 self::TYPE_BOOKING_COMPLETED => [
227 272 'flag' => 'email_template_booking_completed',
228 273 'subject' => 'email_tpl_booking_completed_subject',
229 274 'body' => 'email_tpl_booking_completed_body',
@@ -308,12 +353,122 @@
308 353 * and `..._default_body` to supply baseline copy for their type.
309 354 *
310 355 * @param array<string, array{flag:string,subject:string,body:string}> $defaults
311 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 +
312 378 return (array) apply_filters('yatra_transactional_email_type_to_keys', $defaults);
313 379 }
314 380
315 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 + /**
316 471 * Send if the type is enabled in settings. Pro may handle via {@see 'yatra_send_transactional_email'}.
317 472 *
318 473 * Optional string `transactional_context` (e.g. `booking_created`, `status_confirmed`) is passed through
319 474 * to the filter so Pro can choose a different template row for the same TYPE_BOOKING_CONFIRMATION.
@@ -345,25 +500,55 @@
345 500 /**
346 501 * Allow Yatra Pro (or extensions) to send instead of core templates.
347 502 * Return null to use core; true/false if handled.
348 503 */
349 - $handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables);
350 - if ($handled !== null) {
351 - return (bool) $handled;
352 - }
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;
353 509
354 - if (!SettingsService::isEnabled($flag)) {
355 - return false;
356 - }
510 + try {
511 + $handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables);
512 + if ($handled !== null) {
513 + return (bool) $handled;
514 + }
357 515
358 - $rendered = self::render($type, $variables);
516 + if (!SettingsService::isEnabled($flag)) {
517 + return false;
518 + }
359 519
360 - return EmailService::send(
361 - $to,
362 - $rendered['subject'],
363 - $rendered['body'],
364 - ['Content-Type: text/html; charset=UTF-8']
365 - );
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 + }
366 551 }
367 552
368 553 /**
369 554 * @param array<string, string|int|float> $variables
@@ -385,8 +570,38 @@
385 570 return self::renderWithStringTemplates($type, $subjectTpl, $bodyTpl, $variables);
386 571 }
387 572
388 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 + /**
389 604 * @param array<string, string|int|float> $variables
390 605 * @return array<string, string>
391 606 */
392 607 private static function mergeDefaultVariables(array $variables): array
@@ -533,8 +748,12 @@
533 748 case self::TYPE_PAYMENT_CONFIRMATION:
534 749 /* translators: 1: site name, 2: booking reference. */
535 750 return sprintf(__('✅ [%1$s] Payment received · %2$s', 'yatra'), $site, $ref);
536 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 +
537 756 case self::TYPE_BOOKING_CANCELLATION:
538 757 /* translators: 1: site name, 2: booking reference. */
539 758 return sprintf(__('📋 [%1$s] Booking cancelled · %2$s', 'yatra'), $site, $ref);
540 759
@@ -569,8 +788,16 @@
569 788 // your account" from "verify to complete your booking".
570 789 /* translators: %s: site name. */
571 790 return sprintf(__('✉️ [%s] Verify your email to complete your booking', 'yatra'), $site);
572 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 +
573 800 case self::TYPE_BOOKING_COMPLETED:
574 801 /* translators: 1: site name, 2: booking reference. */
575 802 return sprintf(__('🌟 [%1$s] Trip complete · %2$s', 'yatra'), $site, $ref);
576 803
@@ -661,8 +888,11 @@
661 888
662 889 case self::TYPE_PAYMENT_CONFIRMATION:
663 890 return EmailTemplateDefaults::fallbackTransactionalPayment($v);
664 891
892 + case self::TYPE_PARTIAL_PAYMENT_RECEIVED:
893 + return EmailTemplateDefaults::fallbackTransactionalPartialPayment($v);
894 +
665 895 case self::TYPE_BOOKING_CANCELLATION:
666 896 return EmailTemplateDefaults::fallbackTransactionalCancellation($v);
667 897
668 898 case self::TYPE_BOOKING_REMINDER:
@@ -692,8 +922,14 @@
692 922 // injected at call-time via the intro_paragraph /
693 923 // footer_note merge tags by the booking handler.
694 924 return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v);
695 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 +
696 932 case self::TYPE_BOOKING_COMPLETED:
697 933 return EmailTemplateDefaults::fallbackTransactionalBookingCompleted($v);
698 934
699 935 case self::TYPE_BOOKING_EXPIRED_CUSTOMER:
@@ -788,8 +1024,11 @@
788 1024 'customer_phone' => (string) ($booking->contact_phone ?? ''),
789 1025 'booking_reference' => (string) ($booking->reference ?? ''),
790 1026 'booking_id' => (string) $bookingId,
791 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),
792 1031 'trip_name' => (string) ($booking->trip_title ?? ''),
793 1032 'trip_url' => !empty($booking->trip_slug)
794 1033 ? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode((string) $booking->trip_slug) . '/')
795 1034 : home_url('/'),