self::TYPE_BOOKING_CONFIRMATION, 'payment_received' => self::TYPE_PAYMENT_CONFIRMATION, 'booking_cancelled' => self::TYPE_BOOKING_CANCELLATION, 'trip_reminder' => self::TYPE_BOOKING_REMINDER, 'admin_new_booking' => self::TYPE_ADMIN_NEW_BOOKING, 'admin_payment_received' => self::TYPE_ADMIN_PAYMENT_RECEIVED, 'admin_booking_cancelled' => self::TYPE_ADMIN_BOOKING_CANCELLED, 'trip_consent_request' => self::TYPE_TRIP_CONSENT_REQUEST, 'customer_email_verification' => self::TYPE_CUSTOMER_EMAIL_VERIFICATION, 'guest_email_verification' => self::TYPE_GUEST_EMAIL_VERIFICATION, 'booking_completed' => self::TYPE_BOOKING_COMPLETED, 'booking_expired_customer' => self::TYPE_BOOKING_EXPIRED_CUSTOMER, 'admin_booking_expired' => self::TYPE_ADMIN_BOOKING_EXPIRED, 'scheduled_payment_reminder' => self::TYPE_SCHEDULED_PAYMENT_REMINDER, 'scheduled_payment_succeeded' => self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED, 'scheduled_payment_failed' => self::TYPE_SCHEDULED_PAYMENT_FAILED, 'admin_scheduled_payment_failed' => self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED, 'enquiry_admin' => self::TYPE_ENQUIRY_ADMIN, 'enquiry_received' => self::TYPE_ENQUIRY_CUSTOMER_RECEIVED, 'enquiry_response' => self::TYPE_ENQUIRY_CUSTOMER_RESPONSE, 'review_request' => self::TYPE_REVIEW_REQUEST, 'abandoned_booking_recovery_first' => self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST, 'abandoned_booking_recovery_second' => self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND, 'abandoned_booking_recovery_final' => self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL, ]; return $map[$templateKey] ?? null; } /** * Sample merge-tag values for admin preview (core templates). * * @return array */ public static function sampleVariablesForCoreTemplateKey(string $templateKey): array { return EmailTemplateSampleData::forTemplateKey($templateKey); } /** * @param array $variables * @return array */ public static function mergeTemplateVariables(array $variables): array { return self::mergeDefaultVariables($variables); } /** * Replace {{word}} placeholders (used by previews and extensions). * * @param array $variables */ public static function parseMergeTags(string $template, array $variables): string { $variables = self::mergeDefaultVariables($variables); return self::parseTemplate($template, $variables); } /** * Render using explicit subject/body templates (e.g. unsaved editor content). Empty strings use built-in defaults. * * @param array $variables * @return array{subject: string, body: string} */ public static function renderWithStringTemplates(string $type, string $subjectTpl, string $bodyTpl, array $variables): array { $variables = self::mergeDefaultVariables($variables); $variables = self::normalizeVariablesForType($type, $variables); $map = self::typeToSettingsKeys(); if (!isset($map[$type])) { return ['subject' => '', 'body' => '']; } if ($subjectTpl === '') { $subject = self::defaultSubject($type, $variables); } else { $subject = self::parseTemplate($subjectTpl, $variables); } // Backward-compatible subject override. A caller may preserve a // context-specific subject line while still reusing another type's // body — e.g. guest checkout keeps its booking-specific "Verify your // email to complete your booking" line even though the body now comes // from the operator's customer verification template. Only applied when // the reserved key is explicitly supplied and non-empty; every existing // caller (which never sets it) is completely unaffected. The value is a // pre-rendered final string, so it is used verbatim (no re-parsing). if (isset($variables['_subject_override']) && is_string($variables['_subject_override']) && $variables['_subject_override'] !== '' ) { $subject = $variables['_subject_override']; } if ($bodyTpl === '') { $body = self::defaultBody($type, $variables); } else { $body = self::parseTemplate($bodyTpl, $variables); } return [ 'subject' => $subject, 'body' => $body, ]; } /** * @return array */ private static function typeToSettingsKeys(): array { $defaults = [ self::TYPE_BOOKING_CONFIRMATION => [ 'flag' => 'email_template_booking', 'subject' => 'email_tpl_booking_subject', 'body' => 'email_tpl_booking_body', ], self::TYPE_PAYMENT_CONFIRMATION => [ 'flag' => 'email_template_confirmation', 'subject' => 'email_tpl_payment_subject', 'body' => 'email_tpl_payment_body', ], self::TYPE_BOOKING_CANCELLATION => [ 'flag' => 'email_template_cancellation', 'subject' => 'email_tpl_cancellation_subject', 'body' => 'email_tpl_cancellation_body', ], self::TYPE_BOOKING_REMINDER => [ 'flag' => 'email_template_reminder', 'subject' => 'email_tpl_reminder_subject', 'body' => 'email_tpl_reminder_body', ], self::TYPE_ADMIN_NEW_BOOKING => [ 'flag' => 'email_template_admin_new_booking', 'subject' => 'email_tpl_admin_booking_subject', 'body' => 'email_tpl_admin_booking_body', ], self::TYPE_ADMIN_PAYMENT_RECEIVED => [ 'flag' => 'email_template_admin_payment', 'subject' => 'email_tpl_admin_payment_subject', 'body' => 'email_tpl_admin_payment_body', ], self::TYPE_ADMIN_BOOKING_CANCELLED => [ 'flag' => 'email_template_admin_cancellation', 'subject' => 'email_tpl_admin_cancellation_subject', 'body' => 'email_tpl_admin_cancellation_body', ], self::TYPE_TRIP_CONSENT_REQUEST => [ 'flag' => 'email_template_trip_consent', 'subject' => 'email_tpl_trip_consent_subject', 'body' => 'email_tpl_trip_consent_body', ], self::TYPE_CUSTOMER_EMAIL_VERIFICATION => [ 'flag' => 'email_template_customer_verification', 'subject' => 'email_tpl_customer_verification_subject', 'body' => 'email_tpl_customer_verification_body', ], self::TYPE_GUEST_EMAIL_VERIFICATION => [ 'flag' => 'email_template_guest_verification', 'subject' => 'email_tpl_guest_verification_subject', 'body' => 'email_tpl_guest_verification_body', ], self::TYPE_BOOKING_COMPLETED => [ 'flag' => 'email_template_booking_completed', 'subject' => 'email_tpl_booking_completed_subject', 'body' => 'email_tpl_booking_completed_body', ], self::TYPE_BOOKING_EXPIRED_CUSTOMER => [ 'flag' => 'email_template_booking_expired_customer', 'subject' => 'email_tpl_booking_expired_customer_subject', 'body' => 'email_tpl_booking_expired_customer_body', ], self::TYPE_ADMIN_BOOKING_EXPIRED => [ 'flag' => 'email_template_admin_booking_expired', 'subject' => 'email_tpl_admin_booking_expired_subject', 'body' => 'email_tpl_admin_booking_expired_body', ], self::TYPE_SCHEDULED_PAYMENT_REMINDER => [ 'flag' => 'email_template_scheduled_payment_reminder', 'subject' => 'email_tpl_scheduled_payment_reminder_subject', 'body' => 'email_tpl_scheduled_payment_reminder_body', ], self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED => [ 'flag' => 'email_template_scheduled_payment_succeeded', 'subject' => 'email_tpl_scheduled_payment_succeeded_subject', 'body' => 'email_tpl_scheduled_payment_succeeded_body', ], self::TYPE_SCHEDULED_PAYMENT_FAILED => [ 'flag' => 'email_template_scheduled_payment_failed', 'subject' => 'email_tpl_scheduled_payment_failed_subject', 'body' => 'email_tpl_scheduled_payment_failed_body', ], self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED => [ 'flag' => 'email_template_admin_scheduled_payment_failed', 'subject' => 'email_tpl_admin_scheduled_payment_failed_subject', 'body' => 'email_tpl_admin_scheduled_payment_failed_body', ], self::TYPE_ENQUIRY_ADMIN => [ 'flag' => 'email_template_enquiry_admin', 'subject' => 'email_tpl_enquiry_admin_subject', 'body' => 'email_tpl_enquiry_admin_body', ], self::TYPE_ENQUIRY_CUSTOMER_RECEIVED => [ 'flag' => 'email_template_enquiry_received', 'subject' => 'email_tpl_enquiry_received_subject', 'body' => 'email_tpl_enquiry_received_body', ], self::TYPE_ENQUIRY_CUSTOMER_RESPONSE => [ 'flag' => 'email_template_enquiry_response', 'subject' => 'email_tpl_enquiry_response_subject', 'body' => 'email_tpl_enquiry_response_body', ], self::TYPE_REVIEW_REQUEST => [ 'flag' => 'email_template_review_request', 'subject' => 'email_tpl_review_request_subject', 'body' => 'email_tpl_review_request_body', ], self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST => [ 'flag' => 'email_template_abandoned_booking_recovery_first', 'subject' => 'email_tpl_abandoned_booking_recovery_first_subject', 'body' => 'email_tpl_abandoned_booking_recovery_first_body', ], self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND => [ 'flag' => 'email_template_abandoned_booking_recovery_second', 'subject' => 'email_tpl_abandoned_booking_recovery_second_subject', 'body' => 'email_tpl_abandoned_booking_recovery_second_body', ], self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL => [ 'flag' => 'email_template_abandoned_booking_recovery_final', 'subject' => 'email_tpl_abandoned_booking_recovery_final_subject', 'body' => 'email_tpl_abandoned_booking_recovery_final_body', ], ]; /** * Allow Pro modules (Team & Access, etc.) to register additional * transactional template types — each entry must be an array with * `flag`, `subject`, `body` keys matching the option-name pattern * used above. Once registered, the type participates in: * - sendIfEnabled() (flag gate + send) * - render() / renderWithStringTemplates() (templated subject/body) * - the Email → Templates UI (auto-discovered via the same map) * * Modules also need to hook `yatra_transactional_email_default_subject` * and `..._default_body` to supply baseline copy for their type. * * @param array $defaults */ return (array) apply_filters('yatra_transactional_email_type_to_keys', $defaults); } /** * Send if the type is enabled in settings. Pro may handle via {@see 'yatra_send_transactional_email'}. * * Optional string `transactional_context` (e.g. `booking_created`, `status_confirmed`) is passed through * to the filter so Pro can choose a different template row for the same TYPE_BOOKING_CONFIRMATION. * * @param array $variables Merge tags: {{key}} */ public static function sendIfEnabled(string $type, string $to, array $variables = []): bool { $to = sanitize_email($to); if ($to === '' || !is_email($to)) { return false; } $map = self::typeToSettingsKeys(); if (!isset($map[$type])) { return false; } $flag = $map[$type]['flag']; $proOwnsType = (bool) apply_filters('yatra_pro_email_automation_owns_transactional_type', false, $type); if (!$proOwnsType && !SettingsService::isEnabled($flag)) { return false; } $variables = self::mergeDefaultVariables($variables); $variables = self::normalizeVariablesForType($type, $variables); /** * Allow Yatra Pro (or extensions) to send instead of core templates. * Return null to use core; true/false if handled. */ $handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables); if ($handled !== null) { return (bool) $handled; } if (!SettingsService::isEnabled($flag)) { return false; } $rendered = self::render($type, $variables); return EmailService::send( $to, $rendered['subject'], $rendered['body'], ['Content-Type: text/html; charset=UTF-8'] ); } /** * @param array $variables * @return array{subject: string, body: string} */ public static function render(string $type, array $variables): array { $map = self::typeToSettingsKeys(); if (!isset($map[$type])) { return ['subject' => '', 'body' => '']; } $subjectKey = $map[$type]['subject']; $bodyKey = $map[$type]['body']; $subjectTpl = SettingsService::getString($subjectKey, ''); $bodyTpl = SettingsService::getString($bodyKey, ''); return self::renderWithStringTemplates($type, $subjectTpl, $bodyTpl, $variables); } /** * Would the template that actually gets sent for $type render the * verification link ({{verification_link}})? Guest checkout can't complete * without it, so the checkout controller uses this to decide whether an * operator's customised verification template is safe to use, or whether to * fall back to the built-in default. Respects Pro ownership: a Pro DB * template reports its raw body via `yatra_transactional_email_effective_body`; * otherwise the core option body is checked, and an empty option means the * built-in default (which always includes the link) is used. */ public static function templateRendersVerificationLink(string $type): bool { $effective = apply_filters('yatra_transactional_email_effective_body', null, $type); if (is_string($effective) && $effective !== '') { return strpos($effective, 'verification_link') !== false; } $map = self::typeToSettingsKeys(); if (!isset($map[$type])) { return false; } $body = SettingsService::getString($map[$type]['body'], ''); if (trim($body) === '') { return true; // no custom body → built-in default is used, which always carries the link } return strpos($body, 'verification_link') !== false; } /** * @param array $variables * @return array */ private static function mergeDefaultVariables(array $variables): array { $defaults = [ 'site_name' => get_bloginfo('name'), 'site_url' => home_url('/'), 'admin_email' => SettingsService::getString('admin_email', get_option('admin_email')), ]; $out = []; foreach (array_merge($defaults, $variables) as $k => $v) { $out[(string) $k] = is_scalar($v) ? (string) $v : ''; } return $out; } /** * Ensure templates always have safe, meaningful defaults for commonly-used tags. * * This prevents "blank sections" when a caller supplies only the core booking variables * (e.g. status-change emails) while the template contains richer optional sections. * * @param array $variables * @return array */ private static function normalizeVariablesForType(string $type, array $variables): array { // Booking confirmation is sent from multiple contexts (checkout + admin status changes). // If the caller didn't include the rich "intro/details/footer" blocks, provide a minimal, // data-driven fallback so the email still looks correct. if ($type === self::TYPE_BOOKING_CONFIRMATION) { if (!isset($variables['intro_paragraph']) || trim($variables['intro_paragraph']) === '') { $variables['intro_paragraph'] = __('Thank you for your booking.', 'yatra'); } if (!isset($variables['details_html']) || trim($variables['details_html']) === '') { $variables['details_html'] = self::fallbackBookingDetailsHtml($variables); } if (!isset($variables['footer_note']) || trim($variables['footer_note']) === '') { /* translators: %s: site name. */ $variables['footer_note'] = sprintf(__('— %s', 'yatra'), get_bloginfo('name')); } } // Shared defaults that are safe for most templates if included. if (!isset($variables['intro_paragraph'])) { $variables['intro_paragraph'] = ''; } if (!isset($variables['footer_note'])) { $variables['footer_note'] = ''; } if (!isset($variables['details_html'])) { $variables['details_html'] = ''; } return $variables; } /** * Minimal booking details block for confirmation emails when caller doesn't provide `details_html`. * * @param array $v */ private static function fallbackBookingDetailsHtml(array $v): string { $trip = $v['trip_name'] ?? ''; $date = $v['travel_date'] ?? ''; $pax = $v['travelers_count'] ?? ''; $total = $v['total_amount_formatted'] ?? ''; $due = $v['amount_due_formatted'] ?? ''; $rows = []; if ($trip !== '') { $rows[] = ['label' => __('Trip', 'yatra'), 'value' => esc_html($trip)]; } if ($date !== '') { $rows[] = ['label' => __('Departure', 'yatra'), 'value' => esc_html($date)]; } if ($pax !== '') { $rows[] = ['label' => __('Travelers', 'yatra'), 'value' => esc_html($pax)]; } if ($total !== '') { $rows[] = ['label' => __('Total', 'yatra'), 'value' => esc_html($total)]; } if ($due !== '') { $rows[] = ['label' => __('Amount due', 'yatra'), 'value' => esc_html($due)]; } if (empty($rows)) { return ''; } return EmailTemplateLayout::detailCard($rows); } /** * @param array $variables */ private static function parseTemplate(string $template, array $variables): string { $rendered = (string) preg_replace_callback( // Allow optional whitespace: {{trip_name}} and {{ trip_name }} both work. '/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/', static function (array $m) use ($variables): string { $key = $m[1]; // Never leak raw merge-tags into real emails. If a variable is // missing, replace it with an empty string rather than // returning the original {{tag}} token. return $variables[$key] ?? ''; }, $template ); // Hard-strip any remaining merge-tags (defense-in-depth). $rendered = (string) preg_replace('/\{\{\s*[a-zA-Z0-9_]+\s*\}\}/', '', $rendered); // Users sometimes paste helper text from the editor into the template. // If that happens, strip common helper headings so they don't appear in // production emails. $rendered = (string) preg_replace( '/^.*(Available Variables|Available placeholders|Available Placeholders|Merge tags).*$/mi', '', $rendered ); return $rendered; } /** * @param array $v */ private static function defaultSubject(string $type, array $v): string { $site = $v['site_name'] ?? get_bloginfo('name'); $ref = $v['booking_reference'] ?? $v['booking_id'] ?? ''; switch ($type) { case self::TYPE_BOOKING_CONFIRMATION: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('✈️ [%1$s] Booking update · %2$s', 'yatra'), $site, $ref); case self::TYPE_PAYMENT_CONFIRMATION: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('✅ [%1$s] Payment received · %2$s', 'yatra'), $site, $ref); case self::TYPE_BOOKING_CANCELLATION: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('📋 [%1$s] Booking cancelled · %2$s', 'yatra'), $site, $ref); case self::TYPE_BOOKING_REMINDER: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('🗓️ [%1$s] Your trip is coming up · %2$s', 'yatra'), $site, $ref); case self::TYPE_ADMIN_NEW_BOOKING: /* translators: 1: site name, 2: booking reference, 3: booking ID. */ return sprintf(__('🔔 [%1$s] New booking · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? ''); case self::TYPE_ADMIN_PAYMENT_RECEIVED: /* translators: 1: site name, 2: booking reference, 3: booking ID. */ return sprintf(__('✅ [%1$s] Payment received · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? ''); case self::TYPE_ADMIN_BOOKING_CANCELLED: /* translators: 1: site name, 2: booking reference, 3: booking ID. */ return sprintf(__('📋 [%1$s] Booking cancelled · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? ''); case self::TYPE_TRIP_CONSENT_REQUEST: $formName = $v['form_name'] ?? __('consent form', 'yatra'); /* translators: 1: site name, 2: consent form name. */ return sprintf(__('📝 [%1$s] Action required · %2$s', 'yatra'), $site, $formName); case self::TYPE_CUSTOMER_EMAIL_VERIFICATION: /* translators: %s: site name. */ return sprintf(__('✉️ [%s] Verify your email address', 'yatra'), $site); case self::TYPE_GUEST_EMAIL_VERIFICATION: // Distinct subject so customers can tell apart "verify // your account" from "verify to complete your booking". /* translators: %s: site name. */ return sprintf(__('✉️ [%s] Verify your email to complete your booking', 'yatra'), $site); case self::TYPE_BOOKING_COMPLETED: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('🌟 [%1$s] Trip complete · %2$s', 'yatra'), $site, $ref); case self::TYPE_BOOKING_EXPIRED_CUSTOMER: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('⏱️ [%1$s] Booking expired · %2$s', 'yatra'), $site, $ref); case self::TYPE_ADMIN_BOOKING_EXPIRED: /* translators: 1: site name, 2: booking reference, 3: booking ID. */ return sprintf(__('⏱️ [%1$s] Booking expired · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? ''); case self::TYPE_SCHEDULED_PAYMENT_REMINDER: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('💳 [%1$s] Upcoming payment · %2$s', 'yatra'), $site, $ref); case self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('✅ [%1$s] Scheduled payment received · %2$s', 'yatra'), $site, $ref); case self::TYPE_SCHEDULED_PAYMENT_FAILED: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('⚠️ [%1$s] Payment issue · %2$s', 'yatra'), $site, $ref); case self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED: /* translators: 1: site name, 2: booking reference. */ return sprintf(__('⚠️ [%1$s] Scheduled payment failed · %2$s', 'yatra'), $site, $ref); case self::TYPE_ENQUIRY_ADMIN: $who = $v['customer_name'] ?? __('Customer', 'yatra'); /* translators: 1: site name, 2: customer name. */ return sprintf(__('💬 [%1$s] New enquiry · %2$s', 'yatra'), $site, $who); case self::TYPE_ENQUIRY_CUSTOMER_RECEIVED: /* translators: %s: site name. */ return sprintf(__('✉️ [%s] We received your message', 'yatra'), $site); case self::TYPE_ENQUIRY_CUSTOMER_RESPONSE: /* translators: %s: site name. */ return sprintf(__('💬 [%s] Re: your enquiry', 'yatra'), $site); case self::TYPE_REVIEW_REQUEST: $trip = $v['trip_name'] ?? __('your trip', 'yatra'); /* translators: 1: site name, 2: trip name. */ return sprintf(__('⭐ [%1$s] How was %2$s?', 'yatra'), $site, $trip); case self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST: /* translators: %s: site name. */ return sprintf(__('🛒 [%s] Complete your booking', 'yatra'), $site); case self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND: /* translators: %s: site name. */ return sprintf(__('⏳ [%s] Still interested? Your booking is waiting', 'yatra'), $site); case self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL: /* translators: %s: site name. */ return sprintf(__('⚠️ [%s] Final reminder: complete your booking', 'yatra'), $site); default: // Pro modules register their own types via // `yatra_transactional_email_type_to_keys` — they // supply default copy through this filter. Returning // empty string means "no extension claimed this type" // and we fall back to the generic notification line. $custom = (string) apply_filters( 'yatra_transactional_email_default_subject', '', $type, $v ); if ($custom !== '') { return $custom; } /* translators: %s: site name. */ return sprintf(__('✉️ [%s] Notification', 'yatra'), $site); } } /** * @param array $v */ private static function defaultBody(string $type, array $v): string { switch ($type) { case self::TYPE_BOOKING_CONFIRMATION: return EmailTemplateDefaults::fallbackTransactionalBooking($v); case self::TYPE_PAYMENT_CONFIRMATION: return EmailTemplateDefaults::fallbackTransactionalPayment($v); case self::TYPE_BOOKING_CANCELLATION: return EmailTemplateDefaults::fallbackTransactionalCancellation($v); case self::TYPE_BOOKING_REMINDER: return EmailTemplateDefaults::fallbackTransactionalReminder($v); case self::TYPE_ADMIN_NEW_BOOKING: return EmailTemplateDefaults::fallbackAdminNewBooking($v); case self::TYPE_ADMIN_PAYMENT_RECEIVED: return EmailTemplateDefaults::fallbackAdminPaymentReceived($v); case self::TYPE_ADMIN_BOOKING_CANCELLED: return EmailTemplateDefaults::fallbackAdminBookingCancelled($v); case self::TYPE_TRIP_CONSENT_REQUEST: return EmailTemplateDefaults::fallbackTransactionalTripConsent($v); case self::TYPE_CUSTOMER_EMAIL_VERIFICATION: return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v); case self::TYPE_GUEST_EMAIL_VERIFICATION: // Reuse the customer-verification body. The flow is // similar — click a magic link to prove ownership of // the address — and operators that have already // customised the customer-verification copy get a // consistent look across both. Differentiating copy is // injected at call-time via the intro_paragraph / // footer_note merge tags by the booking handler. return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v); case self::TYPE_BOOKING_COMPLETED: return EmailTemplateDefaults::fallbackTransactionalBookingCompleted($v); case self::TYPE_BOOKING_EXPIRED_CUSTOMER: return EmailTemplateDefaults::fallbackTransactionalBookingExpiredCustomer($v); case self::TYPE_ADMIN_BOOKING_EXPIRED: return EmailTemplateDefaults::fallbackAdminBookingExpired($v); case self::TYPE_SCHEDULED_PAYMENT_REMINDER: return EmailTemplateDefaults::fallbackTransactionalScheduledPaymentReminder($v); case self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED: return EmailTemplateDefaults::fallbackTransactionalScheduledPaymentSucceeded($v); case self::TYPE_SCHEDULED_PAYMENT_FAILED: return EmailTemplateDefaults::fallbackTransactionalScheduledPaymentFailed($v); case self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED: return EmailTemplateDefaults::fallbackAdminScheduledPaymentFailed($v); case self::TYPE_ENQUIRY_ADMIN: return EmailTemplateDefaults::fallbackTransactionalEnquiryAdmin($v); case self::TYPE_ENQUIRY_CUSTOMER_RECEIVED: return EmailTemplateDefaults::fallbackTransactionalEnquiryReceived($v); case self::TYPE_ENQUIRY_CUSTOMER_RESPONSE: return EmailTemplateDefaults::fallbackTransactionalEnquiryResponse($v); case self::TYPE_REVIEW_REQUEST: return EmailTemplateDefaults::fallbackTransactionalReviewRequest($v); case self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST: return EmailTemplateDefaults::fallbackTransactionalAbandonedBookingRecoveryFirst($v); case self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND: return EmailTemplateDefaults::fallbackTransactionalAbandonedBookingRecoverySecond($v); case self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL: return EmailTemplateDefaults::fallbackTransactionalAbandonedBookingRecoveryFinal($v); default: // Pro modules register their own types via // `yatra_transactional_email_type_to_keys` — they // supply default body markup through this filter. // Returning empty string falls back to the generic // notification block. $custom = (string) apply_filters( 'yatra_transactional_email_default_body', '', $type, $v ); if ($custom !== '') { return $custom; } return EmailTemplateLayout::customer( '✉️', __('Notification', 'yatra'), '

' . esc_html__('This is an automated message from your travel site.', 'yatra') . '

', esc_html($v['site_name'] ?? get_bloginfo('name')) ); } } /** * Build variables from a booking row (admin / cron). * * Includes rich tags from {@see BookingEmailRichMergeTags::forBooking()}: * `payment_gateway`, `payment_gateway_label`, `payment_schedule`, `payment_schedule_label`, * `travelers_list`, `travelers_list_html`, `traveler_custom_fields_html`, `booking_custom_fields_html`, * `special_requests`, `special_requests_html`. * * Note: `{{payment_method}}` on payment emails is the instrument label (e.g. Card) merged by callers; * gateway/slug labels use `payment_gateway` / `payment_gateway_label`. Deposit vs full uses `payment_schedule*`. * * @return array Filter: `yatra_booking_email_variables`. */ public static function variablesFromBooking(object $booking): array { $currency = $booking->currency ?? SettingsService::getCurrency(); $travelDate = !empty($booking->travel_date) ? date_i18n(get_option('date_format'), strtotime((string) $booking->travel_date)) : ''; $bookingId = (int) ($booking->id ?? 0); $base = [ 'customer_name' => trim((string) (($booking->contact_first_name ?? '') . ' ' . ($booking->contact_last_name ?? ''))), 'customer_first_name' => (string) ($booking->contact_first_name ?? ''), 'customer_last_name' => (string) ($booking->contact_last_name ?? ''), 'customer_email' => (string) ($booking->contact_email ?? ''), 'customer_phone' => (string) ($booking->contact_phone ?? ''), 'booking_reference' => (string) ($booking->reference ?? ''), 'booking_id' => (string) $bookingId, 'booking_url' => $bookingId > 0 ? home_url('/my-account/bookings/' . $bookingId) : home_url('/'), 'trip_name' => (string) ($booking->trip_title ?? ''), 'trip_url' => !empty($booking->trip_slug) ? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode((string) $booking->trip_slug) . '/') : home_url('/'), 'travel_date' => $travelDate, 'travelers_count' => (string) (int) ($booking->travelers_count ?? 0), 'total_amount_formatted' => yatra_format_price((float) ($booking->total_amount ?? 0)), 'amount_due_formatted' => yatra_format_price((float) ($booking->amount_due ?? 0)), // Aliases for the legacy / customer-customised template // syntax: many templates (including ones edited via Settings // → Email Templates) reference `{{total_amount}}` and // `{{balance_due}}` directly rather than the // `_formatted` variants. Without these aliases the // placeholders survived unsubstituted into the rendered // email body. Aliases use the same formatted-with-currency // value as the canonical keys above so templates remain // visually consistent regardless of which name is used. 'total_amount' => yatra_format_price((float) ($booking->total_amount ?? 0)), 'balance_due' => yatra_format_price((float) ($booking->amount_due ?? 0)), 'amount_due' => yatra_format_price((float) ($booking->amount_due ?? 0)), 'amount_paid' => yatra_format_price((float) ($booking->amount_paid ?? 0)), 'amount_paid_formatted' => yatra_format_price((float) ($booking->amount_paid ?? 0)), 'currency' => $currency, 'booking_status' => (string) ($booking->status ?? ''), 'payment_status' => (string) ($booking->payment_status ?? ''), 'admin_url' => admin_url('admin.php?page=yatra'), ]; $rich = BookingEmailRichMergeTags::forBooking($booking); /** @var array $merged */ $merged = array_merge($base, $rich); return apply_filters('yatra_booking_email_variables', $merged, $booking); } /** * Merge tags for enquiry emails (row from EnquiryRepository::findWithTrip()). * * @param object $enquiry Row with name, email, phone, message, trip_title, etc. * @return array */ public static function variablesFromEnquiry(object $enquiry, string $responsePlain = ''): array { $trip = trim((string) ($enquiry->trip_title ?? '')); $tripSlug = (string) ($enquiry->trip_slug ?? ''); $tripId = isset($enquiry->trip_id) ? (int) $enquiry->trip_id : 0; // Defense-in-depth: if repository join didn't provide trip_title/slug but we do have a trip_id, // resolve the trip directly so {{trip_name}} doesn't fall back to "General enquiry". if (($trip === '' || $tripSlug === '') && $tripId > 0) { try { $repo = new \Yatra\Repositories\TripRepository(); $tripRow = $repo->find($tripId); if ($tripRow) { if ($trip === '' && !empty($tripRow->title)) { $trip = trim((string) $tripRow->title); } if ($tripSlug === '' && !empty($tripRow->slug)) { $tripSlug = (string) $tripRow->slug; } } } catch (\Throwable $e) { // Ignore: keep existing values/fallback. } } $tripUrl = $tripSlug !== '' ? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode($tripSlug) . '/') : home_url('/'); $created = (string) ($enquiry->created_at ?? ''); $enquiryDate = $created !== '' ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($created) ?: time()) : ''; $vars = [ 'customer_name' => (string) ($enquiry->name ?? ''), 'customer_email' => (string) ($enquiry->email ?? ''), 'customer_phone' => (string) ($enquiry->phone ?? ''), 'enquiry_id' => (string) (int) ($enquiry->id ?? 0), 'enquiry_date' => $enquiryDate, 'subject' => (string) ($enquiry->subject ?? ''), 'trip_name' => $trip !== '' ? $trip : __('General enquiry', 'yatra'), 'trip_url' => $tripUrl, 'message' => nl2br(esc_html((string) ($enquiry->message ?? ''))), 'original_message' => (string) ($enquiry->message ?? ''), ]; // Response-only tags are injected solely on the response email so the // sidebar for `enquiry.created` doesn't surface tags that would render // empty in that context. if ($responsePlain !== '') { $responseHtml = nl2br(esc_html($responsePlain)); $vars['response'] = $responseHtml; $vars['response_message'] = $responseHtml; $vars['response_date'] = date_i18n(get_option('date_format') . ' ' . get_option('time_format')); } return $vars; } /** * @param object $booking Booking row (contact_*, reference, …) * @param array $extra e.g. expiry_hours, expiry_notice_html * @return array */ public static function variablesFromBookingWithExtras(object $booking, array $extra = []): array { return array_merge(self::variablesFromBooking($booking), $extra); } /** * @param object $booking Booking row * @param object $scheduledRow Scheduled payment row (amount, currency, scheduled_date, payment_type, …) * @param array $extra failure_reason, balance_after_formatted, permanent_failure, … * @return array */ public static function variablesFromScheduledPayment(object $booking, object $scheduledRow, array $extra = []): array { $currency = (string) ($scheduledRow->currency ?? $booking->currency ?? SettingsService::getCurrency()); $amount = (float) ($scheduledRow->amount ?? 0); $schedDate = !empty($scheduledRow->scheduled_date) ? date_i18n(get_option('date_format'), strtotime((string) $scheduledRow->scheduled_date)) : ''; $base = self::variablesFromBooking($booking); $base['scheduled_amount_formatted'] = yatra_format_price($amount, $currency); $base['scheduled_date_formatted'] = $schedDate; $base['payment_type_label'] = (string) ($scheduledRow->payment_type ?? ''); $base['scheduled_payment_id'] = (string) (int) ($scheduledRow->id ?? 0); return array_merge($base, $extra); } }