| @@ -16,13 +16,15 @@ | ||
| 16 | 16 | * @return array<string, string> |
| 17 | 17 | */ |
| 18 | 18 | public static function settingsOptionDefaults(): array |
| 19 | 19 | { |
| 20 | - return [ | |
| 20 | + $defaults = [ | |
| 21 | 21 | 'email_tpl_booking_subject' => '✈️ [{{site_name}}] Booking update · {{booking_reference}}', |
| 22 | 22 | 'email_tpl_booking_body' => self::htmlBookingCustomer(), |
| 23 | 23 | 'email_tpl_payment_subject' => '✅ [{{site_name}}] Payment received · {{booking_reference}}', |
| 24 | 24 | 'email_tpl_payment_body' => self::htmlPaymentCustomer(), |
| 25 | + 'email_tpl_partial_payment_subject' => '💳 [{{site_name}}] Part payment received · {{booking_reference}}', | |
| 26 | + 'email_tpl_partial_payment_body' => self::htmlPartialPaymentCustomer(), | |
| 25 | 27 | 'email_tpl_cancellation_subject' => '📋 [{{site_name}}] Booking cancelled · {{booking_reference}}', |
| 26 | 28 | 'email_tpl_cancellation_body' => self::htmlCancellationCustomer(), |
| 27 | 29 | 'email_tpl_reminder_subject' => '🗓️ [{{site_name}}] Your trip is coming up · {{booking_reference}}', |
| 28 | 30 | 'email_tpl_reminder_body' => self::htmlReminderCustomer(), |
| @@ -78,8 +80,26 @@ | ||
| 78 | 80 | 'email_tpl_abandoned_booking_recovery_second_body' => self::htmlAbandonedBookingRecoverySecond(), |
| 79 | 81 | 'email_tpl_abandoned_booking_recovery_final_subject' => '⚠️ [{{site_name}}] Final reminder: complete your booking', |
| 80 | 82 | 'email_tpl_abandoned_booking_recovery_final_body' => self::htmlAbandonedBookingRecoveryFinal(), |
| 81 | 83 | ]; |
| 84 | + | |
| 85 | + // Every template also gets an opt-in Bcc / Cc option, derived from its | |
| 86 | + // own subject key so the two lists can never drift apart (and so a | |
| 87 | + // template type added later picks them up for free). Empty by default: | |
| 88 | + // no address configured means no copy is sent and nothing changes for | |
| 89 | + // existing sites. Registered here because this array doubles as the | |
| 90 | + // whitelist of settings the REST API will persist. | |
| 91 | + foreach (array_keys($defaults) as $key) { | |
| 92 | + if (substr($key, -8) !== '_subject') { | |
| 93 | + continue; | |
| 94 | + } | |
| 95 | + | |
| 96 | + $base = substr($key, 0, -8); | |
| 97 | + $defaults[$base . '_bcc'] = ''; | |
| 98 | + $defaults[$base . '_cc'] = ''; | |
| 99 | + } | |
| 100 | + | |
| 101 | + return $defaults; | |
| 82 | 102 | } |
| 83 | 103 | |
| 84 | 104 | /** |
| 85 | 105 | * Full subject + body for Pro system templates (DB). Null = no bundled default for that key. |
| @@ -112,8 +132,12 @@ | ||
| 112 | 132 | 'booking_cancelled' => [ |
| 113 | 133 | 'subject' => '📋 Booking cancelled · {{booking_reference}}', |
| 114 | 134 | 'body' => self::htmlCancellationCustomer(), |
| 115 | 135 | ], |
| 136 | + 'admin_booking_cancelled' => [ | |
| 137 | + 'subject' => '📋 Booking cancelled · {{booking_reference}} (#{{booking_id}})', | |
| 138 | + 'body' => self::htmlAdminBookingCancelled(), | |
| 139 | + ], | |
| 116 | 140 | 'booking_completed' => [ |
| 117 | 141 | 'subject' => '🌟 Thanks for traveling with us · {{booking_reference}}', |
| 118 | 142 | 'body' => self::htmlTripCompleted(), |
| 119 | 143 | ], |
| @@ -120,8 +144,16 @@ | ||
| 120 | 144 | 'payment_received' => [ |
| 121 | 145 | 'subject' => '✅ Payment received · {{booking_reference}}', |
| 122 | 146 | 'body' => self::htmlPaymentCustomer(), |
| 123 | 147 | ], |
| 148 | + 'admin_payment_received' => [ | |
| 149 | + 'subject' => '✅ Payment received · {{booking_reference}} (#{{booking_id}})', | |
| 150 | + 'body' => self::htmlAdminPaymentReceived(), | |
| 151 | + ], | |
| 152 | + 'partial_payment_received' => [ | |
| 153 | + 'subject' => '💳 Part payment received · {{booking_reference}}', | |
| 154 | + 'body' => self::htmlPartialPaymentCustomer(), | |
| 155 | + ], | |
| 124 | 156 | 'payment_reminder' => [ |
| 125 | 157 | 'subject' => '💳 Payment reminder · {{booking_reference}}', |
| 126 | 158 | 'body' => self::htmlPaymentReminder(), |
| 127 | 159 | ], |
| @@ -314,10 +346,61 @@ | ||
| 314 | 346 | ); |
| 315 | 347 | } |
| 316 | 348 | |
| 317 | 349 | /** |
| 350 | + * Part-payment received — a balance is still outstanding. | |
| 351 | + * | |
| 352 | + * Wording differs from the full-payment email on purpose: it confirms what | |
| 353 | + * was received AND states what is still owed, which is the whole reason this | |
| 354 | + * template is separate. | |
| 355 | + * | |
| 318 | 356 | * @param array<string, string> $v |
| 319 | 357 | */ |
| 358 | + public static function fallbackTransactionalPartialPayment(array $v): string | |
| 359 | + { | |
| 360 | + // Null-coalescing rather than `?:` so this renders cleanly even when a | |
| 361 | + // caller passes a sparse variable set — this fallback runs on sites that | |
| 362 | + // upgraded before the template's body option was seeded. | |
| 363 | + $name = esc_html( | |
| 364 | + ($v['customer_first_name'] ?? '') ?: ($v['customer_name'] ?? '') ?: __('Customer', 'yatra') | |
| 365 | + ); | |
| 366 | + $site = esc_html($v['site_name'] ?? get_bloginfo('name')); | |
| 367 | + $rows = array_values(array_filter([ | |
| 368 | + ['label' => __('Booking reference', 'yatra'), 'value' => esc_html($v['booking_reference'] ?? '')], | |
| 369 | + ['label' => __('Amount received', 'yatra'), 'value' => esc_html($v['payment_amount_formatted'] ?? '')], | |
| 370 | + ['label' => __('Paid so far', 'yatra'), 'value' => esc_html($v['amount_paid_formatted'] ?? '')], | |
| 371 | + ['label' => __('Balance remaining', 'yatra'), 'value' => esc_html($v['amount_due_formatted'] ?? '')], | |
| 372 | + ['label' => __('Booking total', 'yatra'), 'value' => esc_html($v['total_amount_formatted'] ?? '')], | |
| 373 | + ['label' => __('Method', 'yatra'), 'value' => esc_html($v['payment_method'] ?? '')], | |
| 374 | + ['label' => __('Transaction ID', 'yatra'), 'value' => esc_html($v['transaction_id'] ?? '')], | |
| 375 | + ], static function (array $row): bool { | |
| 376 | + return $row['value'] !== ''; | |
| 377 | + })); | |
| 378 | + | |
| 379 | + $inner = '<p style="margin:0 0 16px;font-size:17px;color:#0f172a;">' | |
| 380 | + . sprintf( | |
| 381 | + /* translators: %s: customer name */ | |
| 382 | + esc_html__('Dear %s,', 'yatra'), | |
| 383 | + $name | |
| 384 | + ) | |
| 385 | + . '</p>' | |
| 386 | + . '<p style="margin:0 0 8px;color:#475569;">' | |
| 387 | + . esc_html__('Thank you — we’ve received your part payment. Your booking is not yet paid in full, so the remaining balance is shown below.', 'yatra') | |
| 388 | + . '</p>' | |
| 389 | + . EmailTemplateLayout::detailCard($rows); | |
| 390 | + | |
| 391 | + return EmailTemplateLayout::customer( | |
| 392 | + '💳', | |
| 393 | + __('Part payment received', 'yatra'), | |
| 394 | + $inner, | |
| 395 | + $site, | |
| 396 | + __('Payment update', 'yatra') | |
| 397 | + ); | |
| 398 | + } | |
| 399 | + | |
| 400 | + /** | |
| 401 | + * @param array<string, string> $v | |
| 402 | + */ | |
| 320 | 403 | public static function fallbackTransactionalCancellation(array $v): string |
| 321 | 404 | { |
| 322 | 405 | $name = esc_html($v['customer_first_name'] ?: $v['customer_name'] ?: __('Customer', 'yatra')); |
| 323 | 406 | $site = esc_html($v['site_name'] ?? get_bloginfo('name')); |
| @@ -883,8 +966,35 @@ | ||
| 883 | 966 | 'Payment received' |
| 884 | 967 | ); |
| 885 | 968 | } |
| 886 | 969 | |
| 970 | + /** | |
| 971 | + * Editor pre-fill for the part-payment template ({{merge tags}} intact). | |
| 972 | + */ | |
| 973 | + private static function htmlPartialPaymentCustomer(): string | |
| 974 | + { | |
| 975 | + $inner = '<p style="margin:0 0 16px;font-size:17px;color:#0f172a;">Dear {{customer_name}},</p>' | |
| 976 | + . '<p style="margin:0 0 20px;color:#475569;">' | |
| 977 | + . esc_html__('Thank you — we’ve received your part payment. The remaining balance for your booking is shown below.', 'yatra') | |
| 978 | + . '</p>' | |
| 979 | + . EmailTemplateLayout::detailCard([ | |
| 980 | + ['label' => 'Booking reference', 'value' => '{{booking_reference}}'], | |
| 981 | + ['label' => 'Amount received', 'value' => '{{payment_amount_formatted}}'], | |
| 982 | + ['label' => 'Paid so far', 'value' => '{{amount_paid_formatted}}'], | |
| 983 | + ['label' => 'Balance remaining', 'value' => '{{amount_due_formatted}}'], | |
| 984 | + ['label' => 'Booking total', 'value' => '{{total_amount_formatted}}'], | |
| 985 | + ['label' => 'Method', 'value' => '{{payment_method}}'], | |
| 986 | + ]); | |
| 987 | + | |
| 988 | + return EmailTemplateLayout::customer( | |
| 989 | + '💳', | |
| 990 | + 'Part payment received', | |
| 991 | + $inner, | |
| 992 | + '{{site_name}}', | |
| 993 | + 'Payment update' | |
| 994 | + ); | |
| 995 | + } | |
| 996 | + | |
| 887 | 997 | private static function htmlCancellationCustomer(): string |
| 888 | 998 | { |
| 889 | 999 | $inner = '<p style="margin:0 0 16px;font-size:17px;color:#0f172a;">Hello {{customer_name}},</p>' |
| 890 | 1000 | . '<p style="margin:0 0 20px;color:#475569;">' |
| @@ -1233,20 +1343,31 @@ | ||
| 1233 | 1343 | } |
| 1234 | 1344 | |
| 1235 | 1345 | private static function htmlScheduledPaymentReminder(): string |
| 1236 | 1346 | { |
| 1347 | + // "Pay your balance now" CTA. The href is the {{balance_payment_url}} | |
| 1348 | + // merge tag (a secure, guest-accessible balance link supplied by Pro | |
| 1349 | + // Scheduled Payments); it is built manually rather than via | |
| 1350 | + // EmailTemplateLayout::button() because that esc_url()s the href, which | |
| 1351 | + // would strip the merge-tag braces. Pro passes an already-escaped URL. | |
| 1352 | + $cta = '<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:28px 0;"><tr>' | |
| 1353 | + . '<td style="border-radius:10px;background:#0d9488;">' | |
| 1354 | + . '<a href="{{balance_payment_url}}" style="display:inline-block;padding:14px 28px;font-family:\'Segoe UI\',Roboto,Arial,sans-serif;font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;border-radius:10px;">' | |
| 1355 | + . esc_html__('Pay your balance now', 'yatra') . '</a></td></tr></table>'; | |
| 1356 | + | |
| 1237 | 1357 | $inner = '<p style="margin:0 0 16px;font-size:17px;color:#0f172a;">Hello {{customer_first_name}},</p>' |
| 1238 | 1358 | . '<p style="margin:0 0 20px;color:#475569;">' |
| 1239 | - . esc_html__('This is a reminder that a scheduled payment is coming up for your booking.', 'yatra') | |
| 1359 | + . esc_html__('This is a reminder that the remaining balance for your booking is coming up.', 'yatra') | |
| 1240 | 1360 | . '</p>' |
| 1241 | 1361 | . EmailTemplateLayout::detailCard([ |
| 1242 | 1362 | ['label' => 'Reference', 'value' => '{{booking_reference}}'], |
| 1243 | 1363 | ['label' => 'Amount', 'value' => '{{scheduled_amount_formatted}}'], |
| 1244 | - ['label' => 'Scheduled date', 'value' => '{{scheduled_date_formatted}}'], | |
| 1364 | + ['label' => 'Due date', 'value' => '{{scheduled_date_formatted}}'], | |
| 1245 | 1365 | ['label' => 'Type', 'value' => '{{payment_type_label}}'], |
| 1246 | 1366 | ]) |
| 1367 | + . $cta | |
| 1247 | 1368 | . '<p style="margin:24px 0 0;font-size:14px;color:#64748b;">' |
| 1248 | - . esc_html__('If you need to update your payment method, contact us before the charge date.', 'yatra') | |
| 1369 | + . esc_html__('If you have any questions about this payment, please contact us before the due date.', 'yatra') | |
| 1249 | 1370 | . '</p>'; |
| 1250 | 1371 | |
| 1251 | 1372 | return EmailTemplateLayout::customer( |
| 1252 | 1373 | '💳', |