| @@ -5,9 +5,16 @@ | ||
| 5 | 5 | * Module-gated templates (e.g. trip consent, abandoned recovery) stay listed with real |
| 6 | 6 | * on/off + content from site settings while the module is off — view-only until the module is active. |
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | -import { __ } from "./i18n"; | |
| 9 | +import { __, sprintf, brandName } from "./i18n"; | |
| 10 | + | |
| 11 | +// Pre-compute "{brand} Pro" once at module load. Used by the | |
| 12 | +// operator-facing template descriptions below so a white-labelled | |
| 13 | +// site shows the operator's brand instead of literal "Yatra Pro". | |
| 14 | +// brandName() reads window.yatraAdmin.brandName which is injected | |
| 15 | +// before any React module loads, so this value is always correct. | |
| 16 | +const BRAND_PRO_NAME = brandName() + " Pro"; | |
| 10 | 17 | import type { EmailSettingsValues } from "../components/settings/email-settings-types"; |
| 11 | 18 | |
| 12 | 19 | /** Rich booking merge tags (gateway, schedule, travelers, booking meta, special requests). */ |
| 13 | 20 | const BOOKING_RICH_MERGE_TAGS = |
| @@ -25,8 +32,10 @@ | ||
| 25 | 32 | isCoreFree: boolean; |
| 26 | 33 | /** If set, subject/body cannot be edited until Pro + this module slug is active (row still listed). */ |
| 27 | 34 | requiresModule?: string; |
| 28 | 35 | settingsFlag?: keyof EmailSettingsValues; |
| 36 | + /** Only relevant when deposits / partial payments are enabled. */ | |
| 37 | + requiresPartialPayments?: boolean; | |
| 29 | 38 | settingsSubject?: keyof EmailSettingsValues; |
| 30 | 39 | settingsBody?: keyof EmailSettingsValues; |
| 31 | 40 | /** Shown in core editor sidebar for merge tags */ |
| 32 | 41 | mergeTags?: string; |
| @@ -34,12 +43,16 @@ | ||
| 34 | 43 | |
| 35 | 44 | export const CORE_FREE_TEMPLATE_KEYS = [ |
| 36 | 45 | "booking_confirmation", |
| 37 | 46 | "payment_received", |
| 47 | + "partial_payment_received", | |
| 38 | 48 | "booking_cancelled", |
| 39 | 49 | "trip_reminder", |
| 40 | 50 | "trip_consent_request", |
| 41 | 51 | "customer_email_verification", |
| 52 | + "guest_email_verification", | |
| 53 | + "account_email_change_request", | |
| 54 | + "account_email_changed", | |
| 42 | 55 | "booking_completed", |
| 43 | 56 | "booking_expired_customer", |
| 44 | 57 | "admin_booking_expired", |
| 45 | 58 | "scheduled_payment_reminder", |
| @@ -49,9 +62,11 @@ | ||
| 49 | 62 | "enquiry_received", |
| 50 | 63 | "enquiry_admin", |
| 51 | 64 | "enquiry_response", |
| 52 | 65 | "review_request", |
| 53 | - "abandoned_booking_recovery", | |
| 66 | + "abandoned_booking_recovery_first", | |
| 67 | + "abandoned_booking_recovery_second", | |
| 68 | + "abandoned_booking_recovery_final", | |
| 54 | 69 | "admin_new_booking", |
| 55 | 70 | "admin_payment_received", |
| 56 | 71 | "admin_booking_cancelled", |
| 57 | 72 | ] as const; |
| @@ -102,8 +117,29 @@ | ||
| 102 | 117 | "{{site_name}}, {{customer_name}}, {{customer_first_name}}, {{booking_reference}}, {{trip_name}}, {{travel_date}}, {{payment_amount_formatted}}, {{payment_method}}, {{transaction_id}}, {{total_amount_formatted}}, {{currency}}, " + |
| 103 | 118 | BOOKING_RICH_MERGE_TAGS, |
| 104 | 119 | }, |
| 105 | 120 | { |
| 121 | + template_key: "partial_payment_received", | |
| 122 | + event_key: "payment.partial_received", | |
| 123 | + name: __("Partial Payment Received", "yatra"), | |
| 124 | + description: __( | |
| 125 | + "Sent when a payment is recorded but a balance is still outstanding. Only used when deposits or partial payments are enabled and this template is switched on — otherwise the standard Payment Received email is sent for every payment.", | |
| 126 | + "yatra", | |
| 127 | + ), | |
| 128 | + category: "payment", | |
| 129 | + recipient_type: "customer", | |
| 130 | + to_email: "{customer_email}", | |
| 131 | + isCoreFree: true, | |
| 132 | + // Shown only when the site actually takes deposits / partial payments. | |
| 133 | + requiresPartialPayments: true, | |
| 134 | + settingsFlag: "email_template_partial_payment", | |
| 135 | + settingsSubject: "email_tpl_partial_payment_subject", | |
| 136 | + settingsBody: "email_tpl_partial_payment_body", | |
| 137 | + mergeTags: | |
| 138 | + "{{site_name}}, {{customer_name}}, {{customer_first_name}}, {{booking_reference}}, {{trip_name}}, {{travel_date}}, {{payment_amount_formatted}}, {{amount_paid_formatted}}, {{amount_due_formatted}}, {{total_amount_formatted}}, {{payment_method}}, {{transaction_id}}, {{currency}}, " + | |
| 139 | + BOOKING_RICH_MERGE_TAGS, | |
| 140 | + }, | |
| 141 | + { | |
| 106 | 142 | template_key: "booking_cancelled", |
| 107 | 143 | event_key: "booking.cancelled", |
| 108 | 144 | name: __("Booking Cancellation", "yatra"), |
| 109 | 145 | description: __("Sent when a booking is marked cancelled.", "yatra"), |
| @@ -174,8 +210,62 @@ | ||
| 174 | 210 | mergeTags: |
| 175 | 211 | "{{site_name}}, {{site_url}}, {{customer_first_name}}, {{customer_name}}, {{verification_link}}, {{intro_paragraph}}, {{expiry_notice_html}}, {{footer_note}}", |
| 176 | 212 | }, |
| 177 | 213 | { |
| 214 | + template_key: "guest_email_verification", | |
| 215 | + event_key: "account.email_verification", | |
| 216 | + name: __("Guest email verification (checkout)", "yatra"), | |
| 217 | + description: __( | |
| 218 | + "Sent to a guest at checkout who must verify their email before the booking is finalized (when 'Require guest email verification' is enabled). Distinct from the account-registration verification above.", | |
| 219 | + "yatra", | |
| 220 | + ), | |
| 221 | + category: "account", | |
| 222 | + recipient_type: "customer", | |
| 223 | + to_email: "{customer_email}", | |
| 224 | + isCoreFree: true, | |
| 225 | + settingsFlag: "email_template_guest_verification", | |
| 226 | + settingsSubject: "email_tpl_guest_verification_subject", | |
| 227 | + settingsBody: "email_tpl_guest_verification_body", | |
| 228 | + mergeTags: | |
| 229 | + "{{site_name}}, {{site_url}}, {{customer_first_name}}, {{customer_name}}, {{verification_link}}, {{intro_paragraph}}, {{expiry_notice_html}}, {{footer_note}}", | |
| 230 | + }, | |
| 231 | + { | |
| 232 | + template_key: "account_email_change_request", | |
| 233 | + event_key: "account.email_change_request", | |
| 234 | + name: __("Account email change request", "yatra"), | |
| 235 | + description: __( | |
| 236 | + "Sent to the NEW address when a customer changes their account email. Contains the confirmation link; the change only applies once they click it.", | |
| 237 | + "yatra", | |
| 238 | + ), | |
| 239 | + category: "account", | |
| 240 | + recipient_type: "customer", | |
| 241 | + to_email: "{new_email}", | |
| 242 | + isCoreFree: true, | |
| 243 | + settingsFlag: "email_template_account_email_change", | |
| 244 | + settingsSubject: "email_tpl_account_email_change_subject", | |
| 245 | + settingsBody: "email_tpl_account_email_change_body", | |
| 246 | + mergeTags: | |
| 247 | + "{{site_name}}, {{site_url}}, {{customer_first_name}}, {{customer_name}}, {{new_email}}, {{verification_link}}, {{intro_paragraph}}, {{footer_note}}", | |
| 248 | + }, | |
| 249 | + { | |
| 250 | + template_key: "account_email_changed", | |
| 251 | + event_key: "account.email_changed", | |
| 252 | + name: __("Account email changed", "yatra"), | |
| 253 | + description: __( | |
| 254 | + "Security notice sent to the OLD address once a customer's account email change is confirmed.", | |
| 255 | + "yatra", | |
| 256 | + ), | |
| 257 | + category: "account", | |
| 258 | + recipient_type: "customer", | |
| 259 | + to_email: "{customer_email}", | |
| 260 | + isCoreFree: true, | |
| 261 | + settingsFlag: "email_template_account_email_changed", | |
| 262 | + settingsSubject: "email_tpl_account_email_changed_subject", | |
| 263 | + settingsBody: "email_tpl_account_email_changed_body", | |
| 264 | + mergeTags: | |
| 265 | + "{{site_name}}, {{site_url}}, {{customer_first_name}}, {{customer_name}}, {{new_email}}, {{intro_paragraph}}, {{footer_note}}", | |
| 266 | + }, | |
| 267 | + { | |
| 178 | 268 | template_key: "admin_new_booking", |
| 179 | 269 | event_key: "booking.created", |
| 180 | 270 | name: __("Admin: New Booking", "yatra"), |
| 181 | 271 | description: __( |
| @@ -235,9 +325,9 @@ | ||
| 235 | 325 | template_key: "new_booking", |
| 236 | 326 | event_key: "booking.created", |
| 237 | 327 | name: __("New Booking Confirmation", "yatra"), |
| 238 | 328 | description: __( |
| 239 | - "Pro automation template. Enable Email Automation to edit and send.", | |
| 329 | + "Pro automation alternative for the free Booking Confirmation template. Enable Email Automation to edit and send.", | |
| 240 | 330 | "yatra", |
| 241 | 331 | ), |
| 242 | 332 | category: "booking", |
| 243 | 333 | recipient_type: "customer", |
| @@ -242,8 +332,13 @@ | ||
| 242 | 332 | category: "booking", |
| 243 | 333 | recipient_type: "customer", |
| 244 | 334 | to_email: "{customer_email}", |
| 245 | 335 | isCoreFree: false, |
| 336 | + // Gate behind Email Automation so the free / Pro versions don't appear as | |
| 337 | + // a confusing duplicate in the customer's view (e.g. two "Payment Received" | |
| 338 | + // rows). isModuleGatedLocked() reads this and locks the row when the | |
| 339 | + // module isn't active. | |
| 340 | + requiresModule: "email_automation", | |
| 246 | 341 | }, |
| 247 | 342 | { |
| 248 | 343 | template_key: "booking_payment", |
| 249 | 344 | event_key: "payment.received", |
| @@ -248,9 +343,9 @@ | ||
| 248 | 343 | template_key: "booking_payment", |
| 249 | 344 | event_key: "payment.received", |
| 250 | 345 | name: __("Booking payment notice", "yatra"), |
| 251 | 346 | description: __( |
| 252 | - "Pro automation template. Enable Email Automation to edit and send.", | |
| 347 | + "Pro automation alternative for the free Payment Received template. Enable Email Automation to edit and send.", | |
| 253 | 348 | "yatra", |
| 254 | 349 | ), |
| 255 | 350 | category: "payment", |
| 256 | 351 | recipient_type: "customer", |
| @@ -255,8 +350,9 @@ | ||
| 255 | 350 | category: "payment", |
| 256 | 351 | recipient_type: "customer", |
| 257 | 352 | to_email: "{customer_email}", |
| 258 | 353 | isCoreFree: false, |
| 354 | + requiresModule: "email_automation", | |
| 259 | 355 | }, |
| 260 | 356 | { |
| 261 | 357 | template_key: "booking_confirmed", |
| 262 | 358 | event_key: "booking.confirmed", |
| @@ -261,9 +357,9 @@ | ||
| 261 | 357 | template_key: "booking_confirmed", |
| 262 | 358 | event_key: "booking.confirmed", |
| 263 | 359 | name: __("Booking Confirmed", "yatra"), |
| 264 | 360 | description: __( |
| 265 | - "Pro automation template. Enable Email Automation to edit and send.", | |
| 361 | + "Pro automation template fired on booking confirmation. Enable Email Automation to edit and send.", | |
| 266 | 362 | "yatra", |
| 267 | 363 | ), |
| 268 | 364 | category: "booking", |
| 269 | 365 | recipient_type: "customer", |
| @@ -268,8 +364,9 @@ | ||
| 268 | 364 | category: "booking", |
| 269 | 365 | recipient_type: "customer", |
| 270 | 366 | to_email: "{customer_email}", |
| 271 | 367 | isCoreFree: false, |
| 368 | + requiresModule: "email_automation", | |
| 272 | 369 | }, |
| 273 | 370 | { |
| 274 | 371 | template_key: "booking_completed", |
| 275 | 372 | event_key: "booking.completed", |
| @@ -350,9 +447,12 @@ | ||
| 350 | 447 | { |
| 351 | 448 | template_key: "scheduled_payment_succeeded", |
| 352 | 449 | event_key: "scheduled.payment.succeeded", |
| 353 | 450 | name: __("Scheduled payment succeeded", "yatra"), |
| 354 | - description: __("Sent after a scheduled / installment charge succeeds.", "yatra"), | |
| 451 | + description: __( | |
| 452 | + "Sent after a scheduled / installment charge succeeds.", | |
| 453 | + "yatra", | |
| 454 | + ), | |
| 355 | 455 | category: "payment", |
| 356 | 456 | recipient_type: "customer", |
| 357 | 457 | to_email: "{customer_email}", |
| 358 | 458 | isCoreFree: true, |
| @@ -420,9 +520,12 @@ | ||
| 420 | 520 | { |
| 421 | 521 | template_key: "enquiry_received", |
| 422 | 522 | event_key: "enquiry.created", |
| 423 | 523 | name: __("Enquiry Received", "yatra"), |
| 424 | - description: __("Confirmation sent to the customer after they submit an enquiry.", "yatra"), | |
| 524 | + description: __( | |
| 525 | + "Confirmation sent to the customer after they submit an enquiry.", | |
| 526 | + "yatra", | |
| 527 | + ), | |
| 425 | 528 | category: "enquiry", |
| 426 | 529 | recipient_type: "customer", |
| 427 | 530 | to_email: "{customer_email}", |
| 428 | 531 | isCoreFree: true, |
| @@ -435,9 +538,12 @@ | ||
| 435 | 538 | { |
| 436 | 539 | template_key: "enquiry_admin", |
| 437 | 540 | event_key: "enquiry.created", |
| 438 | 541 | name: __("Admin: New Enquiry", "yatra"), |
| 439 | - description: __("Sent to your admin email when a new enquiry is submitted.", "yatra"), | |
| 542 | + description: __( | |
| 543 | + "Sent to your admin email when a new enquiry is submitted.", | |
| 544 | + "yatra", | |
| 545 | + ), | |
| 440 | 546 | category: "enquiry", |
| 441 | 547 | recipient_type: "admin", |
| 442 | 548 | to_email: "{{admin_email}}", |
| 443 | 549 | isCoreFree: true, |
| @@ -450,9 +556,12 @@ | ||
| 450 | 556 | { |
| 451 | 557 | template_key: "enquiry_response", |
| 452 | 558 | event_key: "enquiry.responded", |
| 453 | 559 | name: __("Enquiry Response", "yatra"), |
| 454 | - description: __("Sent to the customer when you reply to their enquiry.", "yatra"), | |
| 560 | + description: __( | |
| 561 | + "Sent to the customer when you reply to their enquiry.", | |
| 562 | + "yatra", | |
| 563 | + ), | |
| 455 | 564 | category: "enquiry", |
| 456 | 565 | recipient_type: "customer", |
| 457 | 566 | to_email: "{customer_email}", |
| 458 | 567 | isCoreFree: true, |
| @@ -481,14 +590,18 @@ | ||
| 481 | 590 | "{{site_name}}, {{customer_name}}, {{trip_name}}, {{review_url}}, {{booking_reference}}, " + |
| 482 | 591 | BOOKING_RICH_MERGE_TAGS, |
| 483 | 592 | }, |
| 484 | 593 | { |
| 485 | - template_key: "abandoned_booking_recovery", | |
| 594 | + template_key: "abandoned_booking_recovery_first", | |
| 486 | 595 | event_key: "booking.abandoned_recovery", |
| 487 | - name: __("Abandoned booking recovery", "yatra"), | |
| 488 | - description: __( | |
| 489 | - "Used by Yatra Pro abandoned checkout recovery (merge tags include module copy in {{recovery_intro_html}}).", | |
| 490 | - "yatra", | |
| 596 | + name: __("Abandoned booking recovery (First)", "yatra"), | |
| 597 | + description: sprintf( | |
| 598 | + /* translators: %s: brand name (e.g. "Yatra Pro" or operator's white-labeled brand) */ | |
| 599 | + __( | |
| 600 | + "First reminder sent by %s abandoned checkout recovery (module copy in {{recovery_intro_html}}).", | |
| 601 | + "yatra", | |
| 602 | + ), | |
| 603 | + BRAND_PRO_NAME, | |
| 491 | 604 | ), |
| 492 | 605 | category: "booking", |
| 493 | 606 | recipient_type: "customer", |
| 494 | 607 | to_email: "{customer_email}", |
| @@ -493,15 +606,63 @@ | ||
| 493 | 606 | recipient_type: "customer", |
| 494 | 607 | to_email: "{customer_email}", |
| 495 | 608 | isCoreFree: true, |
| 496 | 609 | requiresModule: "abandoned_booking_recovery", |
| 497 | - settingsFlag: "email_template_abandoned_booking_recovery", | |
| 498 | - settingsSubject: "email_tpl_abandoned_booking_recovery_subject", | |
| 499 | - settingsBody: "email_tpl_abandoned_booking_recovery_body", | |
| 610 | + settingsFlag: "email_template_abandoned_booking_recovery_first", | |
| 611 | + settingsSubject: "email_tpl_abandoned_booking_recovery_first_subject", | |
| 612 | + settingsBody: "email_tpl_abandoned_booking_recovery_first_body", | |
| 500 | 613 | mergeTags: |
| 501 | 614 | "{{site_name}}, {{customer_name}}, {{trip_name}}, {{recovery_link}}, {{booking_reference}}, {{recovery_reminder_label}}, {{recovery_intro_html}}, " + |
| 502 | 615 | BOOKING_RICH_MERGE_TAGS, |
| 503 | 616 | }, |
| 617 | + { | |
| 618 | + template_key: "abandoned_booking_recovery_second", | |
| 619 | + event_key: "booking.abandoned_recovery", | |
| 620 | + name: __("Abandoned booking recovery (Second)", "yatra"), | |
| 621 | + description: sprintf( | |
| 622 | + /* translators: %s: brand name */ | |
| 623 | + __( | |
| 624 | + "Second reminder sent by %s abandoned checkout recovery (module copy in {{recovery_intro_html}}).", | |
| 625 | + "yatra", | |
| 626 | + ), | |
| 627 | + BRAND_PRO_NAME, | |
| 628 | + ), | |
| 629 | + category: "booking", | |
| 630 | + recipient_type: "customer", | |
| 631 | + to_email: "{customer_email}", | |
| 632 | + isCoreFree: true, | |
| 633 | + requiresModule: "abandoned_booking_recovery", | |
| 634 | + settingsFlag: "email_template_abandoned_booking_recovery_second", | |
| 635 | + settingsSubject: "email_tpl_abandoned_booking_recovery_second_subject", | |
| 636 | + settingsBody: "email_tpl_abandoned_booking_recovery_second_body", | |
| 637 | + mergeTags: | |
| 638 | + "{{site_name}}, {{customer_name}}, {{trip_name}}, {{recovery_link}}, {{booking_reference}}, {{recovery_reminder_label}}, {{recovery_intro_html}}, " + | |
| 639 | + BOOKING_RICH_MERGE_TAGS, | |
| 640 | + }, | |
| 641 | + { | |
| 642 | + template_key: "abandoned_booking_recovery_final", | |
| 643 | + event_key: "booking.abandoned_recovery", | |
| 644 | + name: __("Abandoned booking recovery (Final)", "yatra"), | |
| 645 | + description: sprintf( | |
| 646 | + /* translators: %s: brand name */ | |
| 647 | + __( | |
| 648 | + "Final reminder sent by %s abandoned checkout recovery (module copy in {{recovery_intro_html}}).", | |
| 649 | + "yatra", | |
| 650 | + ), | |
| 651 | + BRAND_PRO_NAME, | |
| 652 | + ), | |
| 653 | + category: "booking", | |
| 654 | + recipient_type: "customer", | |
| 655 | + to_email: "{customer_email}", | |
| 656 | + isCoreFree: true, | |
| 657 | + requiresModule: "abandoned_booking_recovery", | |
| 658 | + settingsFlag: "email_template_abandoned_booking_recovery_final", | |
| 659 | + settingsSubject: "email_tpl_abandoned_booking_recovery_final_subject", | |
| 660 | + settingsBody: "email_tpl_abandoned_booking_recovery_final_body", | |
| 661 | + mergeTags: | |
| 662 | + "{{site_name}}, {{customer_name}}, {{trip_name}}, {{recovery_link}}, {{booking_reference}}, {{recovery_reminder_label}}, {{recovery_intro_html}}, " + | |
| 663 | + BOOKING_RICH_MERGE_TAGS, | |
| 664 | + }, | |
| 504 | 665 | ]; |
| 505 | 666 | |
| 506 | 667 | export function getCatalogEntryByTemplateKey( |
| 507 | 668 | key: string, |
| @@ -536,8 +697,21 @@ | ||
| 536 | 697 | is_locked: boolean; |
| 537 | 698 | variables: string[]; |
| 538 | 699 | created_at?: string; |
| 539 | 700 | updated_at?: string; |
| 701 | + /** | |
| 702 | + * Pro Email Automation trip-specific overrides. `overrides` is the global | |
| 703 | + * template's key when this row is an override ("" otherwise); `targets` | |
| 704 | + * says which trips it applies to; `priority` orders overrides of the same | |
| 705 | + * global template; `effective_event_key` is the event the row fires on | |
| 706 | + * (an override's own event_key is stored empty). | |
| 707 | + */ | |
| 708 | + overrides?: string; | |
| 709 | + /** True when this global template may have trip-specific overrides. */ | |
| 710 | + overridable?: boolean; | |
| 711 | + targets?: { trips?: number[]; categories?: number[]; trip_types?: string[] }; | |
| 712 | + priority?: number; | |
| 713 | + effective_event_key?: string; | |
| 540 | 714 | }; |
| 541 | 715 | |
| 542 | 716 | export type BuildLocalTemplateRowsOptions = { |
| 543 | 717 | isProPluginActive: boolean; |
| @@ -551,15 +725,22 @@ | ||
| 551 | 725 | const pro = options?.isProPluginActive ?? false; |
| 552 | 726 | const moduleOk = (slug: string) => |
| 553 | 727 | options?.isModuleActive ? options.isModuleActive(slug) : false; |
| 554 | 728 | |
| 555 | - return EMAIL_TEMPLATES_CATALOG.map((entry) => { | |
| 729 | + // Sites that never take a deposit or part payment have no use for the | |
| 730 | + // part-payment template, so it is hidden rather than shown inert. | |
| 731 | + const partialPaymentsOn = Boolean( | |
| 732 | + values.partial_payment || values.enable_deposit || values.deposit_required, | |
| 733 | + ); | |
| 734 | + | |
| 735 | + return EMAIL_TEMPLATES_CATALOG.filter( | |
| 736 | + (entry) => !entry.requiresPartialPayments || partialPaymentsOn, | |
| 737 | + ).map((entry) => { | |
| 556 | 738 | const moduleGateOk = |
| 557 | 739 | !entry.requiresModule || (pro && moduleOk(entry.requiresModule)); |
| 558 | 740 | |
| 559 | 741 | const proCatalogLocked = !entry.isCoreFree; |
| 560 | - const moduleContentLocked = | |
| 561 | - Boolean(entry.requiresModule) && !moduleGateOk; | |
| 742 | + const moduleContentLocked = Boolean(entry.requiresModule) && !moduleGateOk; | |
| 562 | 743 | |
| 563 | 744 | const is_locked = proCatalogLocked || moduleContentLocked; |
| 564 | 745 | |
| 565 | 746 | const hasSettingsKeys = Boolean( |