| 1 |
<?php |
| 2 |
|
| 3 |
declare(strict_types=1); |
| 4 |
|
| 5 |
namespace Yatra\Services; |
| 6 |
|
| 7 |
/** |
| 8 |
* Central transactional emails (booking, payment, cancellation, reminder). |
| 9 |
* Content is editable under Email → Templates (settings-backed) or overridden by Pro Email Automation DB templates via filter. |
| 10 |
*/ |
| 11 |
class TransactionalEmailTemplateService |
| 12 |
{ |
| 13 |
public const TYPE_BOOKING_CONFIRMATION = 'booking_confirmation'; |
| 14 |
|
| 15 |
public const TYPE_PAYMENT_CONFIRMATION = 'payment_confirmation'; |
| 16 |
|
| 17 |
public const TYPE_BOOKING_CANCELLATION = 'booking_cancellation'; |
| 18 |
|
| 19 |
public const TYPE_BOOKING_REMINDER = 'booking_reminder'; |
| 20 |
|
| 21 |
public const TYPE_ADMIN_NEW_BOOKING = 'admin_new_booking'; |
| 22 |
|
| 23 |
public const TYPE_ADMIN_PAYMENT_RECEIVED = 'admin_payment_received'; |
| 24 |
|
| 25 |
public const TYPE_ADMIN_BOOKING_CANCELLED = 'admin_booking_cancelled_notice'; |
| 26 |
|
| 27 |
/** Trip consent request (Yatra Pro Trip Consent module). */ |
| 28 |
public const TYPE_TRIP_CONSENT_REQUEST = 'trip_consent_request'; |
| 29 |
|
| 30 |
/** Customer account email verification (e.g. checkout registration). */ |
| 31 |
public const TYPE_CUSTOMER_EMAIL_VERIFICATION = 'customer_email_verification'; |
| 32 |
|
| 33 |
/** |
| 34 |
* Guest-checkout email verification — sent BEFORE payment when |
| 35 |
* `require_guest_email_verification` is on. The booking is held |
| 36 |
* in `pending_verification` status until the customer clicks the |
| 37 |
* magic link. Distinct from `customer_email_verification` because |
| 38 |
* (a) the recipient is not a registered user, and (b) the link |
| 39 |
* resumes the in-flight booking flow rather than completing |
| 40 |
* account registration. |
| 41 |
*/ |
| 42 |
public const TYPE_GUEST_EMAIL_VERIFICATION = 'guest_email_verification'; |
| 43 |
|
| 44 |
public const TYPE_BOOKING_COMPLETED = 'booking_completed'; |
| 45 |
|
| 46 |
public const TYPE_BOOKING_EXPIRED_CUSTOMER = 'booking_expired_customer'; |
| 47 |
|
| 48 |
public const TYPE_ADMIN_BOOKING_EXPIRED = 'admin_booking_expired'; |
| 49 |
|
| 50 |
public const TYPE_SCHEDULED_PAYMENT_REMINDER = 'scheduled_payment_reminder'; |
| 51 |
|
| 52 |
public const TYPE_SCHEDULED_PAYMENT_SUCCEEDED = 'scheduled_payment_succeeded'; |
| 53 |
|
| 54 |
public const TYPE_SCHEDULED_PAYMENT_FAILED = 'scheduled_payment_failed'; |
| 55 |
|
| 56 |
public const TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED = 'admin_scheduled_payment_failed'; |
| 57 |
|
| 58 |
public const TYPE_ENQUIRY_ADMIN = 'enquiry_admin'; |
| 59 |
|
| 60 |
public const TYPE_ENQUIRY_CUSTOMER_RECEIVED = 'enquiry_received'; |
| 61 |
|
| 62 |
public const TYPE_ENQUIRY_CUSTOMER_RESPONSE = 'enquiry_response'; |
| 63 |
|
| 64 |
public const TYPE_REVIEW_REQUEST = 'review_request'; |
| 65 |
|
| 66 |
/** Abandoned checkout recovery (Yatra Pro); 3-stage sequence. */ |
| 67 |
public const TYPE_ABANDONED_BOOKING_RECOVERY_FIRST = 'abandoned_booking_recovery_first'; |
| 68 |
public const TYPE_ABANDONED_BOOKING_RECOVERY_SECOND = 'abandoned_booking_recovery_second'; |
| 69 |
public const TYPE_ABANDONED_BOOKING_RECOVERY_FINAL = 'abandoned_booking_recovery_final'; |
| 70 |
|
| 71 |
/** |
| 72 |
* Map catalog / settings UI keys to internal render types. |
| 73 |
*/ |
| 74 |
public static function coreTemplateKeyToType(string $templateKey): ?string |
| 75 |
{ |
| 76 |
$map = [ |
| 77 |
'booking_confirmation' => self::TYPE_BOOKING_CONFIRMATION, |
| 78 |
'payment_received' => self::TYPE_PAYMENT_CONFIRMATION, |
| 79 |
'booking_cancelled' => self::TYPE_BOOKING_CANCELLATION, |
| 80 |
'trip_reminder' => self::TYPE_BOOKING_REMINDER, |
| 81 |
'admin_new_booking' => self::TYPE_ADMIN_NEW_BOOKING, |
| 82 |
'admin_payment_received' => self::TYPE_ADMIN_PAYMENT_RECEIVED, |
| 83 |
'admin_booking_cancelled' => self::TYPE_ADMIN_BOOKING_CANCELLED, |
| 84 |
'trip_consent_request' => self::TYPE_TRIP_CONSENT_REQUEST, |
| 85 |
'customer_email_verification' => self::TYPE_CUSTOMER_EMAIL_VERIFICATION, |
| 86 |
'guest_email_verification' => self::TYPE_GUEST_EMAIL_VERIFICATION, |
| 87 |
'booking_completed' => self::TYPE_BOOKING_COMPLETED, |
| 88 |
'booking_expired_customer' => self::TYPE_BOOKING_EXPIRED_CUSTOMER, |
| 89 |
'admin_booking_expired' => self::TYPE_ADMIN_BOOKING_EXPIRED, |
| 90 |
'scheduled_payment_reminder' => self::TYPE_SCHEDULED_PAYMENT_REMINDER, |
| 91 |
'scheduled_payment_succeeded' => self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED, |
| 92 |
'scheduled_payment_failed' => self::TYPE_SCHEDULED_PAYMENT_FAILED, |
| 93 |
'admin_scheduled_payment_failed' => self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED, |
| 94 |
'enquiry_admin' => self::TYPE_ENQUIRY_ADMIN, |
| 95 |
'enquiry_received' => self::TYPE_ENQUIRY_CUSTOMER_RECEIVED, |
| 96 |
'enquiry_response' => self::TYPE_ENQUIRY_CUSTOMER_RESPONSE, |
| 97 |
'review_request' => self::TYPE_REVIEW_REQUEST, |
| 98 |
'abandoned_booking_recovery_first' => self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST, |
| 99 |
'abandoned_booking_recovery_second' => self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND, |
| 100 |
'abandoned_booking_recovery_final' => self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL, |
| 101 |
]; |
| 102 |
|
| 103 |
return $map[$templateKey] ?? null; |
| 104 |
} |
| 105 |
|
| 106 |
/** |
| 107 |
* Sample merge-tag values for admin preview (core templates). |
| 108 |
* |
| 109 |
* @return array<string, string> |
| 110 |
*/ |
| 111 |
public static function sampleVariablesForCoreTemplateKey(string $templateKey): array |
| 112 |
{ |
| 113 |
return EmailTemplateSampleData::forTemplateKey($templateKey); |
| 114 |
} |
| 115 |
|
| 116 |
/** |
| 117 |
* @param array<string, string|int|float> $variables |
| 118 |
* @return array<string, string> |
| 119 |
*/ |
| 120 |
public static function mergeTemplateVariables(array $variables): array |
| 121 |
{ |
| 122 |
return self::mergeDefaultVariables($variables); |
| 123 |
} |
| 124 |
|
| 125 |
/** |
| 126 |
* Replace {{word}} placeholders (used by previews and extensions). |
| 127 |
* |
| 128 |
* @param array<string, string|int|float> $variables |
| 129 |
*/ |
| 130 |
public static function parseMergeTags(string $template, array $variables): string |
| 131 |
{ |
| 132 |
$variables = self::mergeDefaultVariables($variables); |
| 133 |
|
| 134 |
return self::parseTemplate($template, $variables); |
| 135 |
} |
| 136 |
|
| 137 |
/** |
| 138 |
* Render using explicit subject/body templates (e.g. unsaved editor content). Empty strings use built-in defaults. |
| 139 |
* |
| 140 |
* @param array<string, string|int|float> $variables |
| 141 |
* @return array{subject: string, body: string} |
| 142 |
*/ |
| 143 |
public static function renderWithStringTemplates(string $type, string $subjectTpl, string $bodyTpl, array $variables): array |
| 144 |
{ |
| 145 |
$variables = self::mergeDefaultVariables($variables); |
| 146 |
$variables = self::normalizeVariablesForType($type, $variables); |
| 147 |
$map = self::typeToSettingsKeys(); |
| 148 |
if (!isset($map[$type])) { |
| 149 |
return ['subject' => '', 'body' => '']; |
| 150 |
} |
| 151 |
|
| 152 |
if ($subjectTpl === '') { |
| 153 |
$subject = self::defaultSubject($type, $variables); |
| 154 |
} else { |
| 155 |
$subject = self::parseTemplate($subjectTpl, $variables); |
| 156 |
} |
| 157 |
|
| 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 |
if ($bodyTpl === '') { |
| 174 |
$body = self::defaultBody($type, $variables); |
| 175 |
} else { |
| 176 |
$body = self::parseTemplate($bodyTpl, $variables); |
| 177 |
} |
| 178 |
|
| 179 |
return [ |
| 180 |
'subject' => $subject, |
| 181 |
'body' => $body, |
| 182 |
]; |
| 183 |
} |
| 184 |
|
| 185 |
/** |
| 186 |
* @return array<string, string> |
| 187 |
*/ |
| 188 |
private static function typeToSettingsKeys(): array |
| 189 |
{ |
| 190 |
$defaults = [ |
| 191 |
self::TYPE_BOOKING_CONFIRMATION => [ |
| 192 |
'flag' => 'email_template_booking', |
| 193 |
'subject' => 'email_tpl_booking_subject', |
| 194 |
'body' => 'email_tpl_booking_body', |
| 195 |
], |
| 196 |
self::TYPE_PAYMENT_CONFIRMATION => [ |
| 197 |
'flag' => 'email_template_confirmation', |
| 198 |
'subject' => 'email_tpl_payment_subject', |
| 199 |
'body' => 'email_tpl_payment_body', |
| 200 |
], |
| 201 |
self::TYPE_BOOKING_CANCELLATION => [ |
| 202 |
'flag' => 'email_template_cancellation', |
| 203 |
'subject' => 'email_tpl_cancellation_subject', |
| 204 |
'body' => 'email_tpl_cancellation_body', |
| 205 |
], |
| 206 |
self::TYPE_BOOKING_REMINDER => [ |
| 207 |
'flag' => 'email_template_reminder', |
| 208 |
'subject' => 'email_tpl_reminder_subject', |
| 209 |
'body' => 'email_tpl_reminder_body', |
| 210 |
], |
| 211 |
self::TYPE_ADMIN_NEW_BOOKING => [ |
| 212 |
'flag' => 'email_template_admin_new_booking', |
| 213 |
'subject' => 'email_tpl_admin_booking_subject', |
| 214 |
'body' => 'email_tpl_admin_booking_body', |
| 215 |
], |
| 216 |
self::TYPE_ADMIN_PAYMENT_RECEIVED => [ |
| 217 |
'flag' => 'email_template_admin_payment', |
| 218 |
'subject' => 'email_tpl_admin_payment_subject', |
| 219 |
'body' => 'email_tpl_admin_payment_body', |
| 220 |
], |
| 221 |
self::TYPE_ADMIN_BOOKING_CANCELLED => [ |
| 222 |
'flag' => 'email_template_admin_cancellation', |
| 223 |
'subject' => 'email_tpl_admin_cancellation_subject', |
| 224 |
'body' => 'email_tpl_admin_cancellation_body', |
| 225 |
], |
| 226 |
self::TYPE_TRIP_CONSENT_REQUEST => [ |
| 227 |
'flag' => 'email_template_trip_consent', |
| 228 |
'subject' => 'email_tpl_trip_consent_subject', |
| 229 |
'body' => 'email_tpl_trip_consent_body', |
| 230 |
], |
| 231 |
self::TYPE_CUSTOMER_EMAIL_VERIFICATION => [ |
| 232 |
'flag' => 'email_template_customer_verification', |
| 233 |
'subject' => 'email_tpl_customer_verification_subject', |
| 234 |
'body' => 'email_tpl_customer_verification_body', |
| 235 |
], |
| 236 |
self::TYPE_GUEST_EMAIL_VERIFICATION => [ |
| 237 |
'flag' => 'email_template_guest_verification', |
| 238 |
'subject' => 'email_tpl_guest_verification_subject', |
| 239 |
'body' => 'email_tpl_guest_verification_body', |
| 240 |
], |
| 241 |
self::TYPE_BOOKING_COMPLETED => [ |
| 242 |
'flag' => 'email_template_booking_completed', |
| 243 |
'subject' => 'email_tpl_booking_completed_subject', |
| 244 |
'body' => 'email_tpl_booking_completed_body', |
| 245 |
], |
| 246 |
self::TYPE_BOOKING_EXPIRED_CUSTOMER => [ |
| 247 |
'flag' => 'email_template_booking_expired_customer', |
| 248 |
'subject' => 'email_tpl_booking_expired_customer_subject', |
| 249 |
'body' => 'email_tpl_booking_expired_customer_body', |
| 250 |
], |
| 251 |
self::TYPE_ADMIN_BOOKING_EXPIRED => [ |
| 252 |
'flag' => 'email_template_admin_booking_expired', |
| 253 |
'subject' => 'email_tpl_admin_booking_expired_subject', |
| 254 |
'body' => 'email_tpl_admin_booking_expired_body', |
| 255 |
], |
| 256 |
self::TYPE_SCHEDULED_PAYMENT_REMINDER => [ |
| 257 |
'flag' => 'email_template_scheduled_payment_reminder', |
| 258 |
'subject' => 'email_tpl_scheduled_payment_reminder_subject', |
| 259 |
'body' => 'email_tpl_scheduled_payment_reminder_body', |
| 260 |
], |
| 261 |
self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED => [ |
| 262 |
'flag' => 'email_template_scheduled_payment_succeeded', |
| 263 |
'subject' => 'email_tpl_scheduled_payment_succeeded_subject', |
| 264 |
'body' => 'email_tpl_scheduled_payment_succeeded_body', |
| 265 |
], |
| 266 |
self::TYPE_SCHEDULED_PAYMENT_FAILED => [ |
| 267 |
'flag' => 'email_template_scheduled_payment_failed', |
| 268 |
'subject' => 'email_tpl_scheduled_payment_failed_subject', |
| 269 |
'body' => 'email_tpl_scheduled_payment_failed_body', |
| 270 |
], |
| 271 |
self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED => [ |
| 272 |
'flag' => 'email_template_admin_scheduled_payment_failed', |
| 273 |
'subject' => 'email_tpl_admin_scheduled_payment_failed_subject', |
| 274 |
'body' => 'email_tpl_admin_scheduled_payment_failed_body', |
| 275 |
], |
| 276 |
self::TYPE_ENQUIRY_ADMIN => [ |
| 277 |
'flag' => 'email_template_enquiry_admin', |
| 278 |
'subject' => 'email_tpl_enquiry_admin_subject', |
| 279 |
'body' => 'email_tpl_enquiry_admin_body', |
| 280 |
], |
| 281 |
self::TYPE_ENQUIRY_CUSTOMER_RECEIVED => [ |
| 282 |
'flag' => 'email_template_enquiry_received', |
| 283 |
'subject' => 'email_tpl_enquiry_received_subject', |
| 284 |
'body' => 'email_tpl_enquiry_received_body', |
| 285 |
], |
| 286 |
self::TYPE_ENQUIRY_CUSTOMER_RESPONSE => [ |
| 287 |
'flag' => 'email_template_enquiry_response', |
| 288 |
'subject' => 'email_tpl_enquiry_response_subject', |
| 289 |
'body' => 'email_tpl_enquiry_response_body', |
| 290 |
], |
| 291 |
self::TYPE_REVIEW_REQUEST => [ |
| 292 |
'flag' => 'email_template_review_request', |
| 293 |
'subject' => 'email_tpl_review_request_subject', |
| 294 |
'body' => 'email_tpl_review_request_body', |
| 295 |
], |
| 296 |
self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST => [ |
| 297 |
'flag' => 'email_template_abandoned_booking_recovery_first', |
| 298 |
'subject' => 'email_tpl_abandoned_booking_recovery_first_subject', |
| 299 |
'body' => 'email_tpl_abandoned_booking_recovery_first_body', |
| 300 |
], |
| 301 |
self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND => [ |
| 302 |
'flag' => 'email_template_abandoned_booking_recovery_second', |
| 303 |
'subject' => 'email_tpl_abandoned_booking_recovery_second_subject', |
| 304 |
'body' => 'email_tpl_abandoned_booking_recovery_second_body', |
| 305 |
], |
| 306 |
self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL => [ |
| 307 |
'flag' => 'email_template_abandoned_booking_recovery_final', |
| 308 |
'subject' => 'email_tpl_abandoned_booking_recovery_final_subject', |
| 309 |
'body' => 'email_tpl_abandoned_booking_recovery_final_body', |
| 310 |
], |
| 311 |
]; |
| 312 |
|
| 313 |
/** |
| 314 |
* Allow Pro modules (Team & Access, etc.) to register additional |
| 315 |
* transactional template types — each entry must be an array with |
| 316 |
* `flag`, `subject`, `body` keys matching the option-name pattern |
| 317 |
* used above. Once registered, the type participates in: |
| 318 |
* - sendIfEnabled() (flag gate + send) |
| 319 |
* - render() / renderWithStringTemplates() (templated subject/body) |
| 320 |
* - the Email → Templates UI (auto-discovered via the same map) |
| 321 |
* |
| 322 |
* Modules also need to hook `yatra_transactional_email_default_subject` |
| 323 |
* and `..._default_body` to supply baseline copy for their type. |
| 324 |
* |
| 325 |
* @param array<string, array{flag:string,subject:string,body:string}> $defaults |
| 326 |
*/ |
| 327 |
return (array) apply_filters('yatra_transactional_email_type_to_keys', $defaults); |
| 328 |
} |
| 329 |
|
| 330 |
/** |
| 331 |
* Send if the type is enabled in settings. Pro may handle via {@see 'yatra_send_transactional_email'}. |
| 332 |
* |
| 333 |
* Optional string `transactional_context` (e.g. `booking_created`, `status_confirmed`) is passed through |
| 334 |
* to the filter so Pro can choose a different template row for the same TYPE_BOOKING_CONFIRMATION. |
| 335 |
* |
| 336 |
* @param array<string, string|int|float> $variables Merge tags: {{key}} |
| 337 |
*/ |
| 338 |
public static function sendIfEnabled(string $type, string $to, array $variables = []): bool |
| 339 |
{ |
| 340 |
$to = sanitize_email($to); |
| 341 |
if ($to === '' || !is_email($to)) { |
| 342 |
return false; |
| 343 |
} |
| 344 |
|
| 345 |
$map = self::typeToSettingsKeys(); |
| 346 |
if (!isset($map[$type])) { |
| 347 |
return false; |
| 348 |
} |
| 349 |
|
| 350 |
$flag = $map[$type]['flag']; |
| 351 |
$proOwnsType = (bool) apply_filters('yatra_pro_email_automation_owns_transactional_type', false, $type); |
| 352 |
|
| 353 |
if (!$proOwnsType && !SettingsService::isEnabled($flag)) { |
| 354 |
return false; |
| 355 |
} |
| 356 |
|
| 357 |
$variables = self::mergeDefaultVariables($variables); |
| 358 |
$variables = self::normalizeVariablesForType($type, $variables); |
| 359 |
|
| 360 |
/** |
| 361 |
* Allow Yatra Pro (or extensions) to send instead of core templates. |
| 362 |
* Return null to use core; true/false if handled. |
| 363 |
*/ |
| 364 |
$handled = apply_filters('yatra_send_transactional_email', null, $type, $to, $variables); |
| 365 |
if ($handled !== null) { |
| 366 |
return (bool) $handled; |
| 367 |
} |
| 368 |
|
| 369 |
if (!SettingsService::isEnabled($flag)) { |
| 370 |
return false; |
| 371 |
} |
| 372 |
|
| 373 |
$rendered = self::render($type, $variables); |
| 374 |
|
| 375 |
return EmailService::send( |
| 376 |
$to, |
| 377 |
$rendered['subject'], |
| 378 |
$rendered['body'], |
| 379 |
['Content-Type: text/html; charset=UTF-8'] |
| 380 |
); |
| 381 |
} |
| 382 |
|
| 383 |
/** |
| 384 |
* @param array<string, string|int|float> $variables |
| 385 |
* @return array{subject: string, body: string} |
| 386 |
*/ |
| 387 |
public static function render(string $type, array $variables): array |
| 388 |
{ |
| 389 |
$map = self::typeToSettingsKeys(); |
| 390 |
if (!isset($map[$type])) { |
| 391 |
return ['subject' => '', 'body' => '']; |
| 392 |
} |
| 393 |
|
| 394 |
$subjectKey = $map[$type]['subject']; |
| 395 |
$bodyKey = $map[$type]['body']; |
| 396 |
|
| 397 |
$subjectTpl = SettingsService::getString($subjectKey, ''); |
| 398 |
$bodyTpl = SettingsService::getString($bodyKey, ''); |
| 399 |
|
| 400 |
return self::renderWithStringTemplates($type, $subjectTpl, $bodyTpl, $variables); |
| 401 |
} |
| 402 |
|
| 403 |
/** |
| 404 |
* Would the template that actually gets sent for $type render the |
| 405 |
* verification link ({{verification_link}})? Guest checkout can't complete |
| 406 |
* without it, so the checkout controller uses this to decide whether an |
| 407 |
* operator's customised verification template is safe to use, or whether to |
| 408 |
* fall back to the built-in default. Respects Pro ownership: a Pro DB |
| 409 |
* template reports its raw body via `yatra_transactional_email_effective_body`; |
| 410 |
* otherwise the core option body is checked, and an empty option means the |
| 411 |
* built-in default (which always includes the link) is used. |
| 412 |
*/ |
| 413 |
public static function templateRendersVerificationLink(string $type): bool |
| 414 |
{ |
| 415 |
$effective = apply_filters('yatra_transactional_email_effective_body', null, $type); |
| 416 |
if (is_string($effective) && $effective !== '') { |
| 417 |
return strpos($effective, 'verification_link') !== false; |
| 418 |
} |
| 419 |
|
| 420 |
$map = self::typeToSettingsKeys(); |
| 421 |
if (!isset($map[$type])) { |
| 422 |
return false; |
| 423 |
} |
| 424 |
|
| 425 |
$body = SettingsService::getString($map[$type]['body'], ''); |
| 426 |
if (trim($body) === '') { |
| 427 |
return true; // no custom body → built-in default is used, which always carries the link |
| 428 |
} |
| 429 |
|
| 430 |
return strpos($body, 'verification_link') !== false; |
| 431 |
} |
| 432 |
|
| 433 |
/** |
| 434 |
* @param array<string, string|int|float> $variables |
| 435 |
* @return array<string, string> |
| 436 |
*/ |
| 437 |
private static function mergeDefaultVariables(array $variables): array |
| 438 |
{ |
| 439 |
$defaults = [ |
| 440 |
'site_name' => get_bloginfo('name'), |
| 441 |
'site_url' => home_url('/'), |
| 442 |
'admin_email' => SettingsService::getString('admin_email', get_option('admin_email')), |
| 443 |
]; |
| 444 |
|
| 445 |
$out = []; |
| 446 |
foreach (array_merge($defaults, $variables) as $k => $v) { |
| 447 |
$out[(string) $k] = is_scalar($v) ? (string) $v : ''; |
| 448 |
} |
| 449 |
|
| 450 |
return $out; |
| 451 |
} |
| 452 |
|
| 453 |
/** |
| 454 |
* Ensure templates always have safe, meaningful defaults for commonly-used tags. |
| 455 |
* |
| 456 |
* This prevents "blank sections" when a caller supplies only the core booking variables |
| 457 |
* (e.g. status-change emails) while the template contains richer optional sections. |
| 458 |
* |
| 459 |
* @param array<string, string> $variables |
| 460 |
* @return array<string, string> |
| 461 |
*/ |
| 462 |
private static function normalizeVariablesForType(string $type, array $variables): array |
| 463 |
{ |
| 464 |
// Booking confirmation is sent from multiple contexts (checkout + admin status changes). |
| 465 |
// If the caller didn't include the rich "intro/details/footer" blocks, provide a minimal, |
| 466 |
// data-driven fallback so the email still looks correct. |
| 467 |
if ($type === self::TYPE_BOOKING_CONFIRMATION) { |
| 468 |
if (!isset($variables['intro_paragraph']) || trim($variables['intro_paragraph']) === '') { |
| 469 |
$variables['intro_paragraph'] = __('Thank you for your booking.', 'yatra'); |
| 470 |
} |
| 471 |
if (!isset($variables['details_html']) || trim($variables['details_html']) === '') { |
| 472 |
$variables['details_html'] = self::fallbackBookingDetailsHtml($variables); |
| 473 |
} |
| 474 |
if (!isset($variables['footer_note']) || trim($variables['footer_note']) === '') { |
| 475 |
/* translators: %s: site name. */ |
| 476 |
$variables['footer_note'] = sprintf(__('— %s', 'yatra'), get_bloginfo('name')); |
| 477 |
} |
| 478 |
} |
| 479 |
|
| 480 |
// Shared defaults that are safe for most templates if included. |
| 481 |
if (!isset($variables['intro_paragraph'])) { |
| 482 |
$variables['intro_paragraph'] = ''; |
| 483 |
} |
| 484 |
if (!isset($variables['footer_note'])) { |
| 485 |
$variables['footer_note'] = ''; |
| 486 |
} |
| 487 |
if (!isset($variables['details_html'])) { |
| 488 |
$variables['details_html'] = ''; |
| 489 |
} |
| 490 |
|
| 491 |
return $variables; |
| 492 |
} |
| 493 |
|
| 494 |
/** |
| 495 |
* Minimal booking details block for confirmation emails when caller doesn't provide `details_html`. |
| 496 |
* |
| 497 |
* @param array<string, string> $v |
| 498 |
*/ |
| 499 |
private static function fallbackBookingDetailsHtml(array $v): string |
| 500 |
{ |
| 501 |
$trip = $v['trip_name'] ?? ''; |
| 502 |
$date = $v['travel_date'] ?? ''; |
| 503 |
$pax = $v['travelers_count'] ?? ''; |
| 504 |
$total = $v['total_amount_formatted'] ?? ''; |
| 505 |
$due = $v['amount_due_formatted'] ?? ''; |
| 506 |
|
| 507 |
$rows = []; |
| 508 |
if ($trip !== '') { |
| 509 |
$rows[] = ['label' => __('Trip', 'yatra'), 'value' => esc_html($trip)]; |
| 510 |
} |
| 511 |
if ($date !== '') { |
| 512 |
$rows[] = ['label' => __('Departure', 'yatra'), 'value' => esc_html($date)]; |
| 513 |
} |
| 514 |
if ($pax !== '') { |
| 515 |
$rows[] = ['label' => __('Travelers', 'yatra'), 'value' => esc_html($pax)]; |
| 516 |
} |
| 517 |
if ($total !== '') { |
| 518 |
$rows[] = ['label' => __('Total', 'yatra'), 'value' => esc_html($total)]; |
| 519 |
} |
| 520 |
if ($due !== '') { |
| 521 |
$rows[] = ['label' => __('Amount due', 'yatra'), 'value' => esc_html($due)]; |
| 522 |
} |
| 523 |
|
| 524 |
if (empty($rows)) { |
| 525 |
return ''; |
| 526 |
} |
| 527 |
|
| 528 |
return EmailTemplateLayout::detailCard($rows); |
| 529 |
} |
| 530 |
|
| 531 |
/** |
| 532 |
* @param array<string, string> $variables |
| 533 |
*/ |
| 534 |
private static function parseTemplate(string $template, array $variables): string |
| 535 |
{ |
| 536 |
$rendered = (string) preg_replace_callback( |
| 537 |
// Allow optional whitespace: {{trip_name}} and {{ trip_name }} both work. |
| 538 |
'/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/', |
| 539 |
static function (array $m) use ($variables): string { |
| 540 |
$key = $m[1]; |
| 541 |
|
| 542 |
// Never leak raw merge-tags into real emails. If a variable is |
| 543 |
// missing, replace it with an empty string rather than |
| 544 |
// returning the original {{tag}} token. |
| 545 |
return $variables[$key] ?? ''; |
| 546 |
}, |
| 547 |
$template |
| 548 |
); |
| 549 |
|
| 550 |
// Hard-strip any remaining merge-tags (defense-in-depth). |
| 551 |
$rendered = (string) preg_replace('/\{\{\s*[a-zA-Z0-9_]+\s*\}\}/', '', $rendered); |
| 552 |
|
| 553 |
// Users sometimes paste helper text from the editor into the template. |
| 554 |
// If that happens, strip common helper headings so they don't appear in |
| 555 |
// production emails. |
| 556 |
$rendered = (string) preg_replace( |
| 557 |
'/^.*(Available Variables|Available placeholders|Available Placeholders|Merge tags).*$/mi', |
| 558 |
'', |
| 559 |
$rendered |
| 560 |
); |
| 561 |
|
| 562 |
return $rendered; |
| 563 |
} |
| 564 |
|
| 565 |
/** |
| 566 |
* @param array<string, string> $v |
| 567 |
*/ |
| 568 |
private static function defaultSubject(string $type, array $v): string |
| 569 |
{ |
| 570 |
$site = $v['site_name'] ?? get_bloginfo('name'); |
| 571 |
$ref = $v['booking_reference'] ?? $v['booking_id'] ?? ''; |
| 572 |
|
| 573 |
switch ($type) { |
| 574 |
case self::TYPE_BOOKING_CONFIRMATION: |
| 575 |
/* translators: 1: site name, 2: booking reference. */ |
| 576 |
return sprintf(__('✈️ [%1$s] Booking update · %2$s', 'yatra'), $site, $ref); |
| 577 |
|
| 578 |
case self::TYPE_PAYMENT_CONFIRMATION: |
| 579 |
/* translators: 1: site name, 2: booking reference. */ |
| 580 |
return sprintf(__('� |
| 581 |
[%1$s] Payment received · %2$s', 'yatra'), $site, $ref); |
| 582 |
|
| 583 |
case self::TYPE_BOOKING_CANCELLATION: |
| 584 |
/* translators: 1: site name, 2: booking reference. */ |
| 585 |
return sprintf(__('📋 [%1$s] Booking cancelled · %2$s', 'yatra'), $site, $ref); |
| 586 |
|
| 587 |
case self::TYPE_BOOKING_REMINDER: |
| 588 |
/* translators: 1: site name, 2: booking reference. */ |
| 589 |
return sprintf(__('🗓️ [%1$s] Your trip is coming up · %2$s', 'yatra'), $site, $ref); |
| 590 |
|
| 591 |
case self::TYPE_ADMIN_NEW_BOOKING: |
| 592 |
/* translators: 1: site name, 2: booking reference, 3: booking ID. */ |
| 593 |
return sprintf(__('🔔 [%1$s] New booking · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? ''); |
| 594 |
|
| 595 |
case self::TYPE_ADMIN_PAYMENT_RECEIVED: |
| 596 |
/* translators: 1: site name, 2: booking reference, 3: booking ID. */ |
| 597 |
return sprintf(__('� |
| 598 |
[%1$s] Payment received · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? ''); |
| 599 |
|
| 600 |
case self::TYPE_ADMIN_BOOKING_CANCELLED: |
| 601 |
/* translators: 1: site name, 2: booking reference, 3: booking ID. */ |
| 602 |
return sprintf(__('📋 [%1$s] Booking cancelled · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? ''); |
| 603 |
|
| 604 |
case self::TYPE_TRIP_CONSENT_REQUEST: |
| 605 |
$formName = $v['form_name'] ?? __('consent form', 'yatra'); |
| 606 |
|
| 607 |
/* translators: 1: site name, 2: consent form name. */ |
| 608 |
return sprintf(__('📝 [%1$s] Action required · %2$s', 'yatra'), $site, $formName); |
| 609 |
|
| 610 |
case self::TYPE_CUSTOMER_EMAIL_VERIFICATION: |
| 611 |
/* translators: %s: site name. */ |
| 612 |
return sprintf(__('✉️ [%s] Verify your email address', 'yatra'), $site); |
| 613 |
|
| 614 |
case self::TYPE_GUEST_EMAIL_VERIFICATION: |
| 615 |
// Distinct subject so customers can tell apart "verify |
| 616 |
// your account" from "verify to complete your booking". |
| 617 |
/* translators: %s: site name. */ |
| 618 |
return sprintf(__('✉️ [%s] Verify your email to complete your booking', 'yatra'), $site); |
| 619 |
|
| 620 |
case self::TYPE_BOOKING_COMPLETED: |
| 621 |
/* translators: 1: site name, 2: booking reference. */ |
| 622 |
return sprintf(__('🌟 [%1$s] Trip complete · %2$s', 'yatra'), $site, $ref); |
| 623 |
|
| 624 |
case self::TYPE_BOOKING_EXPIRED_CUSTOMER: |
| 625 |
/* translators: 1: site name, 2: booking reference. */ |
| 626 |
return sprintf(__('⏱️ [%1$s] Booking expired · %2$s', 'yatra'), $site, $ref); |
| 627 |
|
| 628 |
case self::TYPE_ADMIN_BOOKING_EXPIRED: |
| 629 |
/* translators: 1: site name, 2: booking reference, 3: booking ID. */ |
| 630 |
return sprintf(__('⏱️ [%1$s] Booking expired · %2$s (#%3$s)', 'yatra'), $site, $ref, $v['booking_id'] ?? ''); |
| 631 |
|
| 632 |
case self::TYPE_SCHEDULED_PAYMENT_REMINDER: |
| 633 |
/* translators: 1: site name, 2: booking reference. */ |
| 634 |
return sprintf(__('💳 [%1$s] Upcoming payment · %2$s', 'yatra'), $site, $ref); |
| 635 |
|
| 636 |
case self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED: |
| 637 |
/* translators: 1: site name, 2: booking reference. */ |
| 638 |
return sprintf(__('� |
| 639 |
[%1$s] Scheduled payment received · %2$s', 'yatra'), $site, $ref); |
| 640 |
|
| 641 |
case self::TYPE_SCHEDULED_PAYMENT_FAILED: |
| 642 |
/* translators: 1: site name, 2: booking reference. */ |
| 643 |
return sprintf(__('⚠️ [%1$s] Payment issue · %2$s', 'yatra'), $site, $ref); |
| 644 |
|
| 645 |
case self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED: |
| 646 |
/* translators: 1: site name, 2: booking reference. */ |
| 647 |
return sprintf(__('⚠️ [%1$s] Scheduled payment failed · %2$s', 'yatra'), $site, $ref); |
| 648 |
|
| 649 |
case self::TYPE_ENQUIRY_ADMIN: |
| 650 |
$who = $v['customer_name'] ?? __('Customer', 'yatra'); |
| 651 |
|
| 652 |
/* translators: 1: site name, 2: customer name. */ |
| 653 |
return sprintf(__('💬 [%1$s] New enquiry · %2$s', 'yatra'), $site, $who); |
| 654 |
|
| 655 |
case self::TYPE_ENQUIRY_CUSTOMER_RECEIVED: |
| 656 |
/* translators: %s: site name. */ |
| 657 |
return sprintf(__('✉️ [%s] We received your message', 'yatra'), $site); |
| 658 |
|
| 659 |
case self::TYPE_ENQUIRY_CUSTOMER_RESPONSE: |
| 660 |
/* translators: %s: site name. */ |
| 661 |
return sprintf(__('💬 [%s] Re: your enquiry', 'yatra'), $site); |
| 662 |
|
| 663 |
case self::TYPE_REVIEW_REQUEST: |
| 664 |
$trip = $v['trip_name'] ?? __('your trip', 'yatra'); |
| 665 |
|
| 666 |
/* translators: 1: site name, 2: trip name. */ |
| 667 |
return sprintf(__('⭐ [%1$s] How was %2$s?', 'yatra'), $site, $trip); |
| 668 |
|
| 669 |
case self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST: |
| 670 |
/* translators: %s: site name. */ |
| 671 |
return sprintf(__('🛒 [%s] Complete your booking', 'yatra'), $site); |
| 672 |
|
| 673 |
case self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND: |
| 674 |
/* translators: %s: site name. */ |
| 675 |
return sprintf(__('⏳ [%s] Still interested? Your booking is waiting', 'yatra'), $site); |
| 676 |
|
| 677 |
case self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL: |
| 678 |
/* translators: %s: site name. */ |
| 679 |
return sprintf(__('⚠️ [%s] Final reminder: complete your booking', 'yatra'), $site); |
| 680 |
|
| 681 |
default: |
| 682 |
// Pro modules register their own types via |
| 683 |
// `yatra_transactional_email_type_to_keys` — they |
| 684 |
// supply default copy through this filter. Returning |
| 685 |
// empty string means "no extension claimed this type" |
| 686 |
// and we fall back to the generic notification line. |
| 687 |
$custom = (string) apply_filters( |
| 688 |
'yatra_transactional_email_default_subject', |
| 689 |
'', |
| 690 |
$type, |
| 691 |
$v |
| 692 |
); |
| 693 |
if ($custom !== '') { |
| 694 |
return $custom; |
| 695 |
} |
| 696 |
/* translators: %s: site name. */ |
| 697 |
return sprintf(__('✉️ [%s] Notification', 'yatra'), $site); |
| 698 |
} |
| 699 |
} |
| 700 |
|
| 701 |
/** |
| 702 |
* @param array<string, string> $v |
| 703 |
*/ |
| 704 |
private static function defaultBody(string $type, array $v): string |
| 705 |
{ |
| 706 |
switch ($type) { |
| 707 |
case self::TYPE_BOOKING_CONFIRMATION: |
| 708 |
return EmailTemplateDefaults::fallbackTransactionalBooking($v); |
| 709 |
|
| 710 |
case self::TYPE_PAYMENT_CONFIRMATION: |
| 711 |
return EmailTemplateDefaults::fallbackTransactionalPayment($v); |
| 712 |
|
| 713 |
case self::TYPE_BOOKING_CANCELLATION: |
| 714 |
return EmailTemplateDefaults::fallbackTransactionalCancellation($v); |
| 715 |
|
| 716 |
case self::TYPE_BOOKING_REMINDER: |
| 717 |
return EmailTemplateDefaults::fallbackTransactionalReminder($v); |
| 718 |
|
| 719 |
case self::TYPE_ADMIN_NEW_BOOKING: |
| 720 |
return EmailTemplateDefaults::fallbackAdminNewBooking($v); |
| 721 |
|
| 722 |
case self::TYPE_ADMIN_PAYMENT_RECEIVED: |
| 723 |
return EmailTemplateDefaults::fallbackAdminPaymentReceived($v); |
| 724 |
|
| 725 |
case self::TYPE_ADMIN_BOOKING_CANCELLED: |
| 726 |
return EmailTemplateDefaults::fallbackAdminBookingCancelled($v); |
| 727 |
|
| 728 |
case self::TYPE_TRIP_CONSENT_REQUEST: |
| 729 |
return EmailTemplateDefaults::fallbackTransactionalTripConsent($v); |
| 730 |
|
| 731 |
case self::TYPE_CUSTOMER_EMAIL_VERIFICATION: |
| 732 |
return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v); |
| 733 |
|
| 734 |
case self::TYPE_GUEST_EMAIL_VERIFICATION: |
| 735 |
// Reuse the customer-verification body. The flow is |
| 736 |
// similar — click a magic link to prove ownership of |
| 737 |
// the address — and operators that have already |
| 738 |
// customised the customer-verification copy get a |
| 739 |
// consistent look across both. Differentiating copy is |
| 740 |
// injected at call-time via the intro_paragraph / |
| 741 |
// footer_note merge tags by the booking handler. |
| 742 |
return EmailTemplateDefaults::fallbackTransactionalCustomerEmailVerification($v); |
| 743 |
|
| 744 |
case self::TYPE_BOOKING_COMPLETED: |
| 745 |
return EmailTemplateDefaults::fallbackTransactionalBookingCompleted($v); |
| 746 |
|
| 747 |
case self::TYPE_BOOKING_EXPIRED_CUSTOMER: |
| 748 |
return EmailTemplateDefaults::fallbackTransactionalBookingExpiredCustomer($v); |
| 749 |
|
| 750 |
case self::TYPE_ADMIN_BOOKING_EXPIRED: |
| 751 |
return EmailTemplateDefaults::fallbackAdminBookingExpired($v); |
| 752 |
|
| 753 |
case self::TYPE_SCHEDULED_PAYMENT_REMINDER: |
| 754 |
return EmailTemplateDefaults::fallbackTransactionalScheduledPaymentReminder($v); |
| 755 |
|
| 756 |
case self::TYPE_SCHEDULED_PAYMENT_SUCCEEDED: |
| 757 |
return EmailTemplateDefaults::fallbackTransactionalScheduledPaymentSucceeded($v); |
| 758 |
|
| 759 |
case self::TYPE_SCHEDULED_PAYMENT_FAILED: |
| 760 |
return EmailTemplateDefaults::fallbackTransactionalScheduledPaymentFailed($v); |
| 761 |
|
| 762 |
case self::TYPE_ADMIN_SCHEDULED_PAYMENT_FAILED: |
| 763 |
return EmailTemplateDefaults::fallbackAdminScheduledPaymentFailed($v); |
| 764 |
|
| 765 |
case self::TYPE_ENQUIRY_ADMIN: |
| 766 |
return EmailTemplateDefaults::fallbackTransactionalEnquiryAdmin($v); |
| 767 |
|
| 768 |
case self::TYPE_ENQUIRY_CUSTOMER_RECEIVED: |
| 769 |
return EmailTemplateDefaults::fallbackTransactionalEnquiryReceived($v); |
| 770 |
|
| 771 |
case self::TYPE_ENQUIRY_CUSTOMER_RESPONSE: |
| 772 |
return EmailTemplateDefaults::fallbackTransactionalEnquiryResponse($v); |
| 773 |
|
| 774 |
case self::TYPE_REVIEW_REQUEST: |
| 775 |
return EmailTemplateDefaults::fallbackTransactionalReviewRequest($v); |
| 776 |
|
| 777 |
case self::TYPE_ABANDONED_BOOKING_RECOVERY_FIRST: |
| 778 |
return EmailTemplateDefaults::fallbackTransactionalAbandonedBookingRecoveryFirst($v); |
| 779 |
|
| 780 |
case self::TYPE_ABANDONED_BOOKING_RECOVERY_SECOND: |
| 781 |
return EmailTemplateDefaults::fallbackTransactionalAbandonedBookingRecoverySecond($v); |
| 782 |
|
| 783 |
case self::TYPE_ABANDONED_BOOKING_RECOVERY_FINAL: |
| 784 |
return EmailTemplateDefaults::fallbackTransactionalAbandonedBookingRecoveryFinal($v); |
| 785 |
|
| 786 |
default: |
| 787 |
// Pro modules register their own types via |
| 788 |
// `yatra_transactional_email_type_to_keys` — they |
| 789 |
// supply default body markup through this filter. |
| 790 |
// Returning empty string falls back to the generic |
| 791 |
// notification block. |
| 792 |
$custom = (string) apply_filters( |
| 793 |
'yatra_transactional_email_default_body', |
| 794 |
'', |
| 795 |
$type, |
| 796 |
$v |
| 797 |
); |
| 798 |
if ($custom !== '') { |
| 799 |
return $custom; |
| 800 |
} |
| 801 |
return EmailTemplateLayout::customer( |
| 802 |
'✉️', |
| 803 |
__('Notification', 'yatra'), |
| 804 |
'<p style="margin:0;color:#475569;">' . esc_html__('This is an automated message from your travel site.', 'yatra') . '</p>', |
| 805 |
esc_html($v['site_name'] ?? get_bloginfo('name')) |
| 806 |
); |
| 807 |
} |
| 808 |
} |
| 809 |
|
| 810 |
/** |
| 811 |
* Build variables from a booking row (admin / cron). |
| 812 |
* |
| 813 |
* Includes rich tags from {@see BookingEmailRichMergeTags::forBooking()}: |
| 814 |
* `payment_gateway`, `payment_gateway_label`, `payment_schedule`, `payment_schedule_label`, |
| 815 |
* `travelers_list`, `travelers_list_html`, `traveler_custom_fields_html`, `booking_custom_fields_html`, |
| 816 |
* `special_requests`, `special_requests_html`. |
| 817 |
* |
| 818 |
* Note: `{{payment_method}}` on payment emails is the instrument label (e.g. Card) merged by callers; |
| 819 |
* gateway/slug labels use `payment_gateway` / `payment_gateway_label`. Deposit vs full uses `payment_schedule*`. |
| 820 |
* |
| 821 |
* @return array<string, string> Filter: `yatra_booking_email_variables`. |
| 822 |
*/ |
| 823 |
public static function variablesFromBooking(object $booking): array |
| 824 |
{ |
| 825 |
$currency = $booking->currency ?? SettingsService::getCurrency(); |
| 826 |
$travelDate = !empty($booking->travel_date) |
| 827 |
? date_i18n(get_option('date_format'), strtotime((string) $booking->travel_date)) |
| 828 |
: ''; |
| 829 |
|
| 830 |
$bookingId = (int) ($booking->id ?? 0); |
| 831 |
$base = [ |
| 832 |
'customer_name' => trim((string) (($booking->contact_first_name ?? '') . ' ' . ($booking->contact_last_name ?? ''))), |
| 833 |
'customer_first_name' => (string) ($booking->contact_first_name ?? ''), |
| 834 |
'customer_last_name' => (string) ($booking->contact_last_name ?? ''), |
| 835 |
'customer_email' => (string) ($booking->contact_email ?? ''), |
| 836 |
'customer_phone' => (string) ($booking->contact_phone ?? ''), |
| 837 |
'booking_reference' => (string) ($booking->reference ?? ''), |
| 838 |
'booking_id' => (string) $bookingId, |
| 839 |
'booking_url' => $bookingId > 0 ? home_url('/my-account/bookings/' . $bookingId) : home_url('/'), |
| 840 |
'trip_name' => (string) ($booking->trip_title ?? ''), |
| 841 |
'trip_url' => !empty($booking->trip_slug) |
| 842 |
? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode((string) $booking->trip_slug) . '/') |
| 843 |
: home_url('/'), |
| 844 |
'travel_date' => $travelDate, |
| 845 |
'travelers_count' => (string) (int) ($booking->travelers_count ?? 0), |
| 846 |
'total_amount_formatted' => yatra_format_price((float) ($booking->total_amount ?? 0)), |
| 847 |
'amount_due_formatted' => yatra_format_price((float) ($booking->amount_due ?? 0)), |
| 848 |
// Aliases for the legacy / customer-customised template |
| 849 |
// syntax: many templates (including ones edited via Settings |
| 850 |
// → Email Templates) reference `{{total_amount}}` and |
| 851 |
// `{{balance_due}}` directly rather than the |
| 852 |
// `_formatted` variants. Without these aliases the |
| 853 |
// placeholders survived unsubstituted into the rendered |
| 854 |
// email body. Aliases use the same formatted-with-currency |
| 855 |
// value as the canonical keys above so templates remain |
| 856 |
// visually consistent regardless of which name is used. |
| 857 |
'total_amount' => yatra_format_price((float) ($booking->total_amount ?? 0)), |
| 858 |
'balance_due' => yatra_format_price((float) ($booking->amount_due ?? 0)), |
| 859 |
'amount_due' => yatra_format_price((float) ($booking->amount_due ?? 0)), |
| 860 |
'amount_paid' => yatra_format_price((float) ($booking->amount_paid ?? 0)), |
| 861 |
'amount_paid_formatted' => yatra_format_price((float) ($booking->amount_paid ?? 0)), |
| 862 |
'currency' => $currency, |
| 863 |
'booking_status' => (string) ($booking->status ?? ''), |
| 864 |
'payment_status' => (string) ($booking->payment_status ?? ''), |
| 865 |
'admin_url' => admin_url('admin.php?page=yatra'), |
| 866 |
]; |
| 867 |
|
| 868 |
$rich = BookingEmailRichMergeTags::forBooking($booking); |
| 869 |
|
| 870 |
/** @var array<string, string> $merged */ |
| 871 |
$merged = array_merge($base, $rich); |
| 872 |
|
| 873 |
return apply_filters('yatra_booking_email_variables', $merged, $booking); |
| 874 |
} |
| 875 |
|
| 876 |
/** |
| 877 |
* Merge tags for enquiry emails (row from EnquiryRepository::findWithTrip()). |
| 878 |
* |
| 879 |
* @param object $enquiry Row with name, email, phone, message, trip_title, etc. |
| 880 |
* @return array<string, string> |
| 881 |
*/ |
| 882 |
public static function variablesFromEnquiry(object $enquiry, string $responsePlain = ''): array |
| 883 |
{ |
| 884 |
$trip = trim((string) ($enquiry->trip_title ?? '')); |
| 885 |
$tripSlug = (string) ($enquiry->trip_slug ?? ''); |
| 886 |
$tripId = isset($enquiry->trip_id) ? (int) $enquiry->trip_id : 0; |
| 887 |
|
| 888 |
// Defense-in-depth: if repository join didn't provide trip_title/slug but we do have a trip_id, |
| 889 |
// resolve the trip directly so {{trip_name}} doesn't fall back to "General enquiry". |
| 890 |
if (($trip === '' || $tripSlug === '') && $tripId > 0) { |
| 891 |
try { |
| 892 |
$repo = new \Yatra\Repositories\TripRepository(); |
| 893 |
$tripRow = $repo->find($tripId); |
| 894 |
if ($tripRow) { |
| 895 |
if ($trip === '' && !empty($tripRow->title)) { |
| 896 |
$trip = trim((string) $tripRow->title); |
| 897 |
} |
| 898 |
if ($tripSlug === '' && !empty($tripRow->slug)) { |
| 899 |
$tripSlug = (string) $tripRow->slug; |
| 900 |
} |
| 901 |
} |
| 902 |
} catch (\Throwable $e) { |
| 903 |
// Ignore: keep existing values/fallback. |
| 904 |
} |
| 905 |
} |
| 906 |
|
| 907 |
$tripUrl = $tripSlug !== '' |
| 908 |
? home_url('/' . SettingsService::getTripBase() . '/' . rawurlencode($tripSlug) . '/') |
| 909 |
: home_url('/'); |
| 910 |
|
| 911 |
$created = (string) ($enquiry->created_at ?? ''); |
| 912 |
$enquiryDate = $created !== '' |
| 913 |
? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($created) ?: time()) |
| 914 |
: ''; |
| 915 |
|
| 916 |
$vars = [ |
| 917 |
'customer_name' => (string) ($enquiry->name ?? ''), |
| 918 |
'customer_email' => (string) ($enquiry->email ?? ''), |
| 919 |
'customer_phone' => (string) ($enquiry->phone ?? ''), |
| 920 |
'enquiry_id' => (string) (int) ($enquiry->id ?? 0), |
| 921 |
'enquiry_date' => $enquiryDate, |
| 922 |
'subject' => (string) ($enquiry->subject ?? ''), |
| 923 |
'trip_name' => $trip !== '' ? $trip : __('General enquiry', 'yatra'), |
| 924 |
'trip_url' => $tripUrl, |
| 925 |
'message' => nl2br(esc_html((string) ($enquiry->message ?? ''))), |
| 926 |
'original_message' => (string) ($enquiry->message ?? ''), |
| 927 |
]; |
| 928 |
|
| 929 |
// Response-only tags are injected solely on the response email so the |
| 930 |
// sidebar for `enquiry.created` doesn't surface tags that would render |
| 931 |
// empty in that context. |
| 932 |
if ($responsePlain !== '') { |
| 933 |
$responseHtml = nl2br(esc_html($responsePlain)); |
| 934 |
$vars['response'] = $responseHtml; |
| 935 |
$vars['response_message'] = $responseHtml; |
| 936 |
$vars['response_date'] = date_i18n(get_option('date_format') . ' ' . get_option('time_format')); |
| 937 |
} |
| 938 |
|
| 939 |
return $vars; |
| 940 |
} |
| 941 |
|
| 942 |
/** |
| 943 |
* @param object $booking Booking row (contact_*, reference, …) |
| 944 |
* @param array<string, string> $extra e.g. expiry_hours, expiry_notice_html |
| 945 |
* @return array<string, string> |
| 946 |
*/ |
| 947 |
public static function variablesFromBookingWithExtras(object $booking, array $extra = []): array |
| 948 |
{ |
| 949 |
return array_merge(self::variablesFromBooking($booking), $extra); |
| 950 |
} |
| 951 |
|
| 952 |
/** |
| 953 |
* @param object $booking Booking row |
| 954 |
* @param object $scheduledRow Scheduled payment row (amount, currency, scheduled_date, payment_type, …) |
| 955 |
* @param array<string, string> $extra failure_reason, balance_after_formatted, permanent_failure, … |
| 956 |
* @return array<string, string> |
| 957 |
*/ |
| 958 |
public static function variablesFromScheduledPayment(object $booking, object $scheduledRow, array $extra = []): array |
| 959 |
{ |
| 960 |
$currency = (string) ($scheduledRow->currency ?? $booking->currency ?? SettingsService::getCurrency()); |
| 961 |
$amount = (float) ($scheduledRow->amount ?? 0); |
| 962 |
$schedDate = !empty($scheduledRow->scheduled_date) |
| 963 |
? date_i18n(get_option('date_format'), strtotime((string) $scheduledRow->scheduled_date)) |
| 964 |
: ''; |
| 965 |
|
| 966 |
$base = self::variablesFromBooking($booking); |
| 967 |
$base['scheduled_amount_formatted'] = yatra_format_price($amount, $currency); |
| 968 |
$base['scheduled_date_formatted'] = $schedDate; |
| 969 |
$base['payment_type_label'] = (string) ($scheduledRow->payment_type ?? ''); |
| 970 |
$base['scheduled_payment_id'] = (string) (int) ($scheduledRow->id ?? 0); |
| 971 |
|
| 972 |
return array_merge($base, $extra); |
| 973 |
} |
| 974 |
} |
| 975 |
|