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 +179 -14 3.0.10 → 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
@@ -81,8 +90,9 @@
81 90 {
82 91 $map = [
83 92 'booking_confirmation' => self::TYPE_BOOKING_CONFIRMATION,
84 93 'payment_received' => self::TYPE_PAYMENT_CONFIRMATION,
94 + 'partial_payment_received' => self::TYPE_PARTIAL_PAYMENT_RECEIVED,
85 95 'booking_cancelled' => self::TYPE_BOOKING_CANCELLATION,
86 96 'trip_reminder' => self::TYPE_BOOKING_REMINDER,
87 97 'admin_new_booking' => self::TYPE_ADMIN_NEW_BOOKING,
88 98 'admin_payment_received' => self::TYPE_ADMIN_PAYMENT_RECEIVED,
@@ -202,8 +212,13 @@
202 212 'flag' => 'email_template_confirmation',
203 213 'subject' => 'email_tpl_payment_subject',
204 214 'body' => 'email_tpl_payment_body',
205 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 + ],
206 221 self::TYPE_BOOKING_CANCELLATION => [
207 222 'flag' => 'email_template_cancellation',
208 223 'subject' => 'email_tpl_cancellation_subject',
209 224 'body' => 'email_tpl_cancellation_body',
@@ -338,12 +353,122 @@
338 353 * and `..._default_body` to supply baseline copy for their type.
339 354 *
340 355 * @param array<string, array{flag:string,subject:string,body:string}> $defaults
341 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 +
342 378 return (array) apply_filters('yatra_transactional_email_type_to_keys', $defaults);
343 379 }
344 380
345 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 + /**
346 471 * Send if the type is enabled in settings. Pro may handle via {@see 'yatra_send_transactional_email'}.
347 472 *
348 473 * Optional string `transactional_context` (e.g. `booking_created`, `status_confirmed`) is passed through
349 474 * to the filter so Pro can choose a different template row for the same TYPE_BOOKING_CONFIRMATION.
@@ -375,25 +500,55 @@
375 500 /**
376 501 * Allow Yatra Pro (or extensions) to send instead of core templates.
377 502 * Return null to use core; true/false if handled.
378 503 */
379 - $handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables);
380 - if ($handled !== null) {
381 - return (bool) $handled;
382 - }
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;
383 509
384 - if (!SettingsService::isEnabled($flag)) {
385 - return false;
386 - }
510 + try {
511 + $handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables);
512 + if ($handled !== null) {
513 + return (bool) $handled;
514 + }
387 515
388 - $rendered = self::render($type, $variables);
516 + if (!SettingsService::isEnabled($flag)) {
517 + return false;
518 + }
389 519
390 - return EmailService::send(
391 - $to,
392 - $rendered['subject'],
393 - $rendered['body'],
394 - ['Content-Type: text/html; charset=UTF-8']
395 - );
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 + }
396 551 }
397 552
398 553 /**
399 554 * @param array<string, string|int|float> $variables
@@ -593,8 +748,12 @@
593 748 case self::TYPE_PAYMENT_CONFIRMATION:
594 749 /* translators: 1: site name, 2: booking reference. */
595 750 return sprintf(__('✅ [%1$s] Payment received · %2$s', 'yatra'), $site, $ref);
596 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 +
597 756 case self::TYPE_BOOKING_CANCELLATION:
598 757 /* translators: 1: site name, 2: booking reference. */
599 758 return sprintf(__('📋 [%1$s] Booking cancelled · %2$s', 'yatra'), $site, $ref);
600 759
@@ -729,8 +888,11 @@
729 888
730 889 case self::TYPE_PAYMENT_CONFIRMATION:
731 890 return EmailTemplateDefaults::fallbackTransactionalPayment($v);
732 891
892 + case self::TYPE_PARTIAL_PAYMENT_RECEIVED:
893 + return EmailTemplateDefaults::fallbackTransactionalPartialPayment($v);
894 +
733 895 case self::TYPE_BOOKING_CANCELLATION:
734 896 return EmailTemplateDefaults::fallbackTransactionalCancellation($v);
735 897
736 898 case self::TYPE_BOOKING_REMINDER:
@@ -862,8 +1024,11 @@
862 1024 'customer_phone' => (string) ($booking->contact_phone ?? ''),
863 1025 'booking_reference' => (string) ($booking->reference ?? ''),
864 1026 'booking_id' => (string) $bookingId,
865 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),
866 1031 'trip_name' => (string) ($booking->trip_title ?? ''),
867 1032 'trip_url' => !empty($booking->trip_slug)
868 1033 ? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode((string) $booking->trip_slug) . '/')
869 1034 : home_url('/'),