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 +203 -29 3.0.9 → 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
@@ -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,28 +167,25 @@
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
158 - // Backward-compatible subject override. A caller may preserve a
159 - // context-specific subject line while still reusing another type's
160 - // body — e.g. guest checkout keeps its booking-specific "Verify your
161 - // email to complete your booking" line even though the body now comes
162 - // from the operator's customer verification template. Only applied when
163 - // the reserved key is explicitly supplied and non-empty; every existing
164 - // caller (which never sets it) is completely unaffected. The value is a
165 - // pre-rendered final string, so it is used verbatim (no re-parsing).
166 - if (isset($variables['_subject_override'])
167 - && is_string($variables['_subject_override'])
168 - && $variables['_subject_override'] !== ''
169 - ) {
170 - $subject = $variables['_subject_override'];
171 - }
172 -
173 188 if ($bodyTpl === '') {
174 189 $body = self::defaultBody($type, $variables);
175 190 } else {
176 191 $body = self::parseTemplate($bodyTpl, $variables);
@@ -197,8 +212,13 @@
197 212 'flag' => 'email_template_confirmation',
198 213 'subject' => 'email_tpl_payment_subject',
199 214 'body' => 'email_tpl_payment_body',
200 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 + ],
201 221 self::TYPE_BOOKING_CANCELLATION => [
202 222 'flag' => 'email_template_cancellation',
203 223 'subject' => 'email_tpl_cancellation_subject',
204 224 'body' => 'email_tpl_cancellation_body',
@@ -237,8 +257,18 @@
237 257 'flag' => 'email_template_guest_verification',
238 258 'subject' => 'email_tpl_guest_verification_subject',
239 259 'body' => 'email_tpl_guest_verification_body',
240 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 + ],
241 271 self::TYPE_BOOKING_COMPLETED => [
242 272 'flag' => 'email_template_booking_completed',
243 273 'subject' => 'email_tpl_booking_completed_subject',
244 274 'body' => 'email_tpl_booking_completed_body',
@@ -323,12 +353,122 @@
323 353 * and `..._default_body` to supply baseline copy for their type.
324 354 *
325 355 * @param array<string, array{flag:string,subject:string,body:string}> $defaults
326 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 +
327 378 return (array) apply_filters('yatra_transactional_email_type_to_keys', $defaults);
328 379 }
329 380
330 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 + /**
331 471 * Send if the type is enabled in settings. Pro may handle via {@see 'yatra_send_transactional_email'}.
332 472 *
333 473 * Optional string `transactional_context` (e.g. `booking_created`, `status_confirmed`) is passed through
334 474 * to the filter so Pro can choose a different template row for the same TYPE_BOOKING_CONFIRMATION.
@@ -360,25 +500,35 @@
360 500 /**
361 501 * Allow Yatra Pro (or extensions) to send instead of core templates.
362 502 * Return null to use core; true/false if handled.
363 503 */
364 - $handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables);
365 - if ($handled !== null) {
366 - return (bool) $handled;
367 - }
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;
368 509
369 - if (!SettingsService::isEnabled($flag)) {
370 - return false;
371 - }
510 + try {
511 + $handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables);
512 + if ($handled !== null) {
513 + return (bool) $handled;
514 + }
372 515
373 - $rendered = self::render($type, $variables);
516 + if (!SettingsService::isEnabled($flag)) {
517 + return false;
518 + }
374 519
375 - return EmailService::send(
376 - $to,
377 - $rendered['subject'],
378 - $rendered['body'],
379 - ['Content-Type: text/html; charset=UTF-8']
380 - );
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 + }
381 531 }
382 532
383 533 /**
384 534 * @param array<string, string|int|float> $variables
@@ -578,8 +728,12 @@
578 728 case self::TYPE_PAYMENT_CONFIRMATION:
579 729 /* translators: 1: site name, 2: booking reference. */
580 730 return sprintf(__('✅ [%1$s] Payment received · %2$s', 'yatra'), $site, $ref);
581 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 +
582 736 case self::TYPE_BOOKING_CANCELLATION:
583 737 /* translators: 1: site name, 2: booking reference. */
584 738 return sprintf(__('📋 [%1$s] Booking cancelled · %2$s', 'yatra'), $site, $ref);
585 739
@@ -614,8 +768,16 @@
614 768 // your account" from "verify to complete your booking".
615 769 /* translators: %s: site name. */
616 770 return sprintf(__('✉️ [%s] Verify your email to complete your booking', 'yatra'), $site);
617 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 +
618 780 case self::TYPE_BOOKING_COMPLETED:
619 781 /* translators: 1: site name, 2: booking reference. */
620 782 return sprintf(__('🌟 [%1$s] Trip complete · %2$s', 'yatra'), $site, $ref);
621 783
@@ -706,8 +868,11 @@
706 868
707 869 case self::TYPE_PAYMENT_CONFIRMATION:
708 870 return EmailTemplateDefaults::fallbackTransactionalPayment($v);
709 871
872 + case self::TYPE_PARTIAL_PAYMENT_RECEIVED:
873 + return EmailTemplateDefaults::fallbackTransactionalPartialPayment($v);
874 +
710 875 case self::TYPE_BOOKING_CANCELLATION:
711 876 return EmailTemplateDefaults::fallbackTransactionalCancellation($v);
712 877
713 878 case self::TYPE_BOOKING_REMINDER:
@@ -737,8 +902,14 @@
737 902 // injected at call-time via the intro_paragraph /
738 903 // footer_note merge tags by the booking handler.
739 904 return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v);
740 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 +
741 912 case self::TYPE_BOOKING_COMPLETED:
742 913 return EmailTemplateDefaults::fallbackTransactionalBookingCompleted($v);
743 914
744 915 case self::TYPE_BOOKING_EXPIRED_CUSTOMER:
@@ -833,8 +1004,11 @@
833 1004 'customer_phone' => (string) ($booking->contact_phone ?? ''),
834 1005 'booking_reference' => (string) ($booking->reference ?? ''),
835 1006 'booking_id' => (string) $bookingId,
836 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),
837 1011 'trip_name' => (string) ($booking->trip_title ?? ''),
838 1012 'trip_url' => !empty($booking->trip_slug)
839 1013 ? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode((string) $booking->trip_slug) . '/')
840 1014 : home_url('/'),