From eeae163782dead26107a51198d9a504656146d19 Mon Sep 17 00:00:00 2001 From: laxman h Date: Tue, 12 May 2026 20:00:29 +0530 Subject: [PATCH] new mail templates added for edge scenerios --- docs/RE_Dealer_Email_Content_Client_Brief.md | 118 ++ docs/RE_Dealer_Email_Templates.md | 1761 +++++++++++++++++ src/constants/allowed-email-template-codes.ts | 10 + src/constants/onboarding-email-defaults.ts | 50 + .../architectural_plan_request.html | 13 + ...ealership_agreement_signature_request.html | 8 + .../document_received_acknowledgement.html | 10 + .../document_rejected_resubmit.html | 12 + .../document_submission_reminder.html | 12 + src/emailtemplates/fdd_document_request.html | 15 + .../loi_acknowledgement_request.html | 7 + .../prospect_document_request.html | 15 + .../security_deposit_request.html | 14 + .../statutory_document_request.html | 13 + src/modules/fdd/fdd.controller.ts | 28 + src/modules/loa/loa.controller.ts | 44 +- src/modules/loi/loi.controller.ts | 84 +- .../onboarding/onboarding.controller.ts | 218 ++ src/modules/onboarding/onboarding.routes.ts | 6 +- src/scripts/seed-master-emails.ts | 70 + src/services/NotificationService.ts | 59 + 21 files changed, 2554 insertions(+), 13 deletions(-) create mode 100644 docs/RE_Dealer_Email_Content_Client_Brief.md create mode 100644 docs/RE_Dealer_Email_Templates.md create mode 100644 src/constants/onboarding-email-defaults.ts create mode 100644 src/emailtemplates/architectural_plan_request.html create mode 100644 src/emailtemplates/dealership_agreement_signature_request.html create mode 100644 src/emailtemplates/document_received_acknowledgement.html create mode 100644 src/emailtemplates/document_rejected_resubmit.html create mode 100644 src/emailtemplates/document_submission_reminder.html create mode 100644 src/emailtemplates/fdd_document_request.html create mode 100644 src/emailtemplates/loi_acknowledgement_request.html create mode 100644 src/emailtemplates/prospect_document_request.html create mode 100644 src/emailtemplates/security_deposit_request.html create mode 100644 src/emailtemplates/statutory_document_request.html diff --git a/docs/RE_Dealer_Email_Content_Client_Brief.md b/docs/RE_Dealer_Email_Content_Client_Brief.md new file mode 100644 index 0000000..f4e4ca9 --- /dev/null +++ b/docs/RE_Dealer_Email_Content_Client_Brief.md @@ -0,0 +1,118 @@ +# Royal Enfield Dealer Onboarding — Email Content Brief (Client & Stakeholders) + +**Audience:** Marketing, Legal, Dealer Development leadership, and anyone preparing or approving customer-facing copy. +**Not a technical document:** This brief explains *what* each communication is for, *who* sees it, and *what your team should supply*. Engineering implements delivery; you own the words and policy. + +**Where to edit live copy:** After go-live, authorised users can change subject lines and HTML bodies in the application under **Master Configuration → Email Templates** without developer involvement. Run the seed script only when adding *new* template types. + +**Companion (technical) catalogue:** For merge-field names, file paths, and automation triggers tied to code, see `RE_Dealer_Email_Templates.md`. + +--- + +## How to use this document + +1. **Review each row** in the tables below. Decide whether the default copy fits your brand and regulatory needs. +2. **Fill the “Your team should confirm” column** with decisions (e.g. exact deposit amounts, bank account text, legal disclaimers, contact paths). +3. **Send your marked-up copy** back to the implementation team *or* enter it directly in **Email Templates** in the admin portal. +4. **Placeholders** shown in curly braces (e.g. `{{applicantName}}`) are replaced automatically by the system—do not remove them unless Product agrees to drop that personalisation. + +**Tone guidance (default):** Professional, warm, concise; Royal Enfield voice; avoid jargon where a plain “next step” works; always include a clear call-to-action and support path. + +--- + +## A. Onboarding — Applicant (prospect) journey + +| # | Working name | Purpose (plain English) | Who receives it | When it is sent (business event) | Your team should confirm | +|---|--------------|-------------------------|-----------------|----------------------------------|---------------------------| +| A1 | Opportunity available | Invite applicant to complete the assessment questionnaire | Applicant email | Location is in an open opportunity window | Questionnaire link text, support contact | +| A2 | Non-opportunity regret | Politely close the lead when no window exists | Applicant | Application submitted, no opportunity | Regret wording, future contact policy | +| A3 | Questionnaire submitted | Acknowledge receipt of assessment | Applicant | Questionnaire submitted | Optional next-step expectation | +| A4 | Questionnaire reminder | Nudge to complete pending questionnaire | Applicant (+ WhatsApp if mobile on file) | Bulk reminder from DD-Admin | Reminder frequency policy | +| A5 | Shortlisted | Congratulate and point to portal | Applicant | Bulk or single shortlist | Portal URL / “what happens next” | +| A6 | **Prospect document request** | Ask for KYC / business documents after shortlist | Applicant (+ WhatsApp if available) | Immediately after shortlist | **Full checklist** (PAN, net worth, bank statements, etc.), deadlines, file rules | +| A7 | Application rejected | Formal rejection with optional reason | Applicant | Rejection at any evaluation stage | Standard reason codes, escalation contact | +| A8 | Interview (applicant / panelist) | Schedule, reschedule, or cancel interviews | Applicant and/or panelists | Interview actions in assessment module | Meeting etiquette, virtual link policy | +| A9 | LOI issued | LOI available on portal | Applicant (email only per policy) | LOI document generated | LOI legal disclaimer, portal path | +| A10 | **Statutory document request** | Collect licences, NOCs, deeds, GST, etc. post-LOI | Applicant | After LOI issuance email | **Authoritative statutory list** for your state/entity types | +| A11 | **LOI acknowledgement reminder** | Chase if LOI not acknowledged | Applicant (+ WhatsApp if available) | **Manual:** DD-Admin sends bulk “LOI ack reminders” for selected applications | Deadline text, consequences of non-ack | +| A12 | **Security deposit request** | Request deposit and proof upload | Applicant (+ WhatsApp if available) | After applicant acknowledges LOI | **Amount**, bank vs online flow, timeline | +| A13 | Payment verified (internal) | Finance verified deposit | DD-Admin / DD-Lead | Finance marks payment paid | Internal wording only | +| A14 | LOA issued | Appointment letter ready | Applicant + internal teams | LOA fully approved | Celebration tone, dealer code display rules | +| A15 | **Dealership agreement signature** | E-sign request for agreement | Applicant | Same event as LOA issuance (second email) | **Agreement** legal intro, signing deadline | +| A16 | Dealer codes ready | SAP sales/service codes | Applicant | Dealer code generation stage | Code formatting, next operational steps | +| A17 | **FDD document request** | Financial due diligence checklist | Applicant | FDD agency assigned | **FDD checklist**, partner naming, confidentiality line | +| A18 | **Architecture / site inputs** | Request drawings, photos, civic approvals | Applicant | Architecture lead assigned on application | **Input list** aligned with your architecture SOP | +| A19 | **Document received acknowledgement** | Confirm each upload | Applicant | Each document upload to the application | Whether per-file email is acceptable volume-wise | +| A20 | **Document submission reminder** | Remind pending uploads | Applicant (+ WhatsApp if available) | **Manual:** bulk “document reminders” from DD-Admin | Default pending text vs customised list per campaign | +| A21 | **Document rejected — resubmit** | Explain rejection and ask for corrected file | Applicant (+ WhatsApp if available) | DD-Admin / DD-Lead / Legal rejects a document on portal | Standard rejection categories, appeal path | +| A22 | Onboarding status update | Generic status change | Applicant | Many workflow transitions | When to use vs specific templates | +| A23 | EOR complete | All EOR items verified | DD-Head / NBH (configurable) | EOR checklist 100% | Internal handoff language | +| A24 | Inauguration logged | Dealership live | Internal teams | Inauguration recorded | Distribution list | + +--- + +## B. Collaboration & workflow (internal and dealer) + +| # | Working name | Purpose | Who receives it | When | Your team should confirm | +|---|--------------|---------|-----------------|------|---------------------------| +| B1 | User assigned | Someone joined the case as participant | Assigned user | Participant added | Role titles | +| B2 | Worknote mention | @mention in worknote | Mentioned user | Mention in collaboration | — | +| B3 | Action required | Next approver’s turn | Next actor (+ WhatsApp if phone) | Workflow advance / send-back | CTA labels | +| B4 | Dealer status update | Milestone to dealer | Dealer / applicant | Interim or terminal workflow events | Terminal vs interim policy | + +--- + +## C. Resignation, termination, relocation, constitutional change, F&F, SLA + +Use the same pattern: confirm legal wording for **termination**, **show-cause**, **F&F amounts**, and **SLA** escalation text with Legal and Finance. Detailed rows can mirror Section A format in your next revision; the technical catalogue lists every code already live. + +--- + +## D. Operational notes for your team + +1. **Bulk actions (DD-Admin):** + - **Questionnaire reminders** — existing endpoint. + - **Document submission reminders** — new; send to selected `applicationIds`; optional custom “pending” paragraph. + - **LOI acknowledgement reminders** — new; only sends if LOI is approved, a generated LOI document exists, and **no** acknowledgement record is present yet. + +2. **Document rejection:** DD-Admin, DD-Lead, Legal Admin, or Super Admin can call the reject API (or future UI button) with a **mandatory reason**; the applicant receives the resubmission email. + +3. **WhatsApp:** Where a template supports WhatsApp, the system uses the mobile number on the applicant or user record. If no number is stored, email is still sent. + +4. **Default checklists in code:** Until you replace them in **Email Templates**, the system uses sensible default bullet lists for prospect documents, statutory documents, FDD, and architecture inputs. **Legal should replace these** with your official lists. + +--- + +## E. Sign-off sheet (optional) + +| Template (working name) | Owner | Copy approved (Y/N) | Legal approved (Y/N) | Effective date | +|-------------------------|-------|---------------------|------------------------|------------------| +| Prospect document request | | | | | +| Statutory document request | | | | | +| LOI acknowledgement reminder | | | | | +| Security deposit request | | | | | +| Dealership agreement signature | | | | | +| FDD document request | | | | | +| Architecture / site inputs | | | | | +| Document received acknowledgement | | | | | +| Document submission reminder | | | | | +| Document rejected — resubmit | | | | | + +--- + +## F. Reference — API endpoints (for IT / integration; not for dealers) + +All routes require an authenticated staff token (same as other onboarding APIs). + +| Action | Method & path | JSON body | +|--------|----------------|-----------| +| Bulk document upload reminder | `POST /api/onboarding/applications/document-reminders` | `{ "applicationIds": ["", ...], "pendingDocuments": "", "dueDate": "" }` | +| Bulk LOI acknowledgement reminder | `POST /api/onboarding/applications/loi-ack-reminders` | `{ "applicationIds": ["", ...], "dueDate": "" }` — skips applications that already have an LOI acknowledgement on the latest generated LOI document | +| Reject an uploaded document | `POST /api/onboarding/applications/:id/documents/:documentId/reject` | `{ "rejectionReason": "", "dueDate": "" }` — roles: DD Admin, DD Lead, Legal Admin, Super Admin | + +--- + +**Document version:** 1.0 +**Aligned with application build:** May 2026 + +When you update copy in production, record the change owner and date in your own change log; the portal stores template versions in the database. diff --git a/docs/RE_Dealer_Email_Templates.md b/docs/RE_Dealer_Email_Templates.md new file mode 100644 index 0000000..1f0a044 --- /dev/null +++ b/docs/RE_Dealer_Email_Templates.md @@ -0,0 +1,1761 @@ +# RE Dealer Management System — Email Communications Catalogue + +**Document Owner:** Dealer Development — Product / Engineering +**Last updated:** 2026-05-11 +**Status:** Living document. Section A lists every email wired in the application. **Section B** covers the ten **onboarding document / LOI / LOA / FDD / architecture** templates that are now implemented; share **`RE_Dealer_Email_Content_Client_Brief.md`** with non-technical stakeholders for copy sign-off. + +> Purpose: A single source of truth for every outbound mail the platform sends — what triggers it, who receives it, what channels it goes on, and the exact copy currently rendered to the recipient. Use this for stakeholder sign-off, copy review, and to plan the next set of communications (Document Collection, Statutory Document Collection, etc.). + +--- + +## Table of Contents + +1. [Conventions & Common Elements](#1-conventions--common-elements) +2. [Master Index of Email Templates](#2-master-index-of-email-templates) +3. [Section A — Currently Implemented Mails](#section-a--currently-implemented-mails) + - A.1 Dealer Onboarding + - A.2 Dealer Resignation + - A.3 Dealer Termination + - A.4 Constitutional Change + - A.5 Dealer Relocation + - A.6 Full & Final (F&F) Settlement + - A.7 Workflow / Collaboration / Generic + - A.8 SLA & Escalation Mails +4. [Section B — Onboarding extensions (implemented)](#section-b--onboarding-extensions-implemented) +5. [Appendix — Common Header / Footer / CTA Partials](#appendix--common-header--footer--cta-partials) + +--- + +## 1. Conventions & Common Elements + +- Every mail is rendered through a Handlebars template stored under `backend/src/emailtemplates/*.html` and seeded into the `email_templates` table (`backend/src/scripts/seed-master-emails.ts`, `seed-missing-templates.ts`, `seed-interview-templates.ts`). +- Master Configuration (`Admin → Master → Email Templates`) lets DD-Admin edit Subject, Body, and toggle Active without code changes. +- Each template ships with a fixed set of merge placeholders (e.g. `{{applicantName}}`, `{{applicationId}}`, `{{link}}`). +- Every mail is wrapped with: + - **Header partial:** Royal Enfield black banner with logo (`partials/email_header.html`). + - **Footer partial:** Copyright + "do not reply" line (`partials/email_footer.html`). + - **Primary CTA partial:** Red action button rendered only when `ctaUrl` is supplied (`partials/primary_cta.html`). +- Channels per recipient are decided in code (`NotificationService.notify`) — typically a combination of `system` (in‑app), `email`, and `whatsapp` (when a mobile number is on file). + +--- + +## 2. Master Index of Email Templates + +| # | Template Code | Module | Recipient(s) | Channels | Status | +|---|---|---|---|---|---| +| 1 | `OPPORTUNITY` | Onboarding | Applicant | email | Implemented | +| 2 | `NON_OPPORTUNITY` | Onboarding | Applicant | email | Implemented | +| 3 | `QUESTIONNAIRE_SUBMITTED` | Onboarding | Applicant | email | Implemented | +| 4 | `QUESTIONNAIRE_REMINDER` | Onboarding | Applicant | email + whatsapp | Implemented | +| 5 | `APPLICANT_SHORTLISTED` | Onboarding | Applicant | email | Implemented | +| 6 | `APPLICANT_REJECTED` | Onboarding | Applicant | email + whatsapp | Implemented | +| 7 | `INTERVIEW_SCHEDULED` | Onboarding | Applicant / Panelist | email | Implemented (legacy) | +| 8 | `INTERVIEW_SCHEDULED_APPLICANT` | Onboarding | Applicant | email + whatsapp + system | Implemented | +| 9 | `INTERVIEW_SCHEDULED_PANELIST` | Onboarding | Panelist (Internal) | email + whatsapp + system | Implemented | +| 10 | `INTERVIEW_RESCHEDULED_APPLICANT` | Onboarding | Applicant | email + system | Implemented | +| 11 | `INTERVIEW_RESCHEDULED_PANELIST` | Onboarding | Panelist | email + system | Implemented | +| 12 | `INTERVIEW_CANCELLED_APPLICANT` | Onboarding | Applicant | email + system | Implemented | +| 13 | `INTERVIEW_CANCELLED_PANELIST` | Onboarding | Panelist | email + system | Implemented | +| 14 | `USER_ASSIGNED` | Collaboration | Internal user (participant) | email + system | Implemented | +| 15 | `LOI_ISSUED` | Onboarding | Applicant | email | Implemented | +| 16 | `LOA_ISSUED` | Onboarding | Applicant + internal | email + system | Implemented | +| 17 | `DEALER_CODE_READY` | Onboarding | Applicant + internal | email + system | Implemented | +| 18 | `ONBOARDING_STATUS_UPDATE` | Onboarding | Applicant | email + system | Implemented | +| 19 | `ONBOARDING_PAYMENT_VERIFIED` | Onboarding | DD-Admin / Internal | email + system | Implemented | +| 20 | `EOR_COMPLETED` | Onboarding | DD-Head + NBH | email + system | Implemented | +| 21 | `INAUGURATION_COMPLETED` | Onboarding | Internal teams | email + system | Implemented | +| 22 | `RESIGNATION_RECEIVED` | Resignation | Dealer | email + whatsapp | Implemented | +| 23 | `RESIGNATION_SUBMITTED` | Resignation | Internal (ASM, RBM, DD-ZM, …) | email + whatsapp + system | Implemented | +| 24 | `RESIGNATION_APPROVED` | Resignation | Dealer | email + system | Implemented | +| 25 | `RESIGNATION_UPDATE` | Resignation | Dealer | system (email optional) | Implemented | +| 26 | `TERMINATION_INITIATED` | Termination | Internal stakeholders | email + whatsapp + system | Implemented | +| 27 | `TERMINATION_SCN_ISSUED` | Termination | Dealer + Internal | email + whatsapp + system | Implemented | +| 28 | `TERMINATION_LETTER_ISSUED` | Termination | DD-Lead, DD-Admin, Finance | email + system | Implemented | +| 29 | `TERMINATION_FINAL_CLOSURE_DEALER` | Termination | Dealer | email + whatsapp + system | Implemented | +| 30 | `TERMINATION_UPDATE` | Termination | Dealer | system (email optional) | Implemented | +| 31 | `CONSTITUTIONAL_CHANGE_SUBMITTED` | Constitutional | Internal stakeholders | email + whatsapp + system | Implemented | +| 32 | `CONSTITUTIONAL_CHANGE_APPROVED` | Constitutional | Dealer | email + system | Implemented | +| 33 | `CONSTITUTIONAL_CHANGE_UPDATE` | Constitutional | Dealer | system (email optional) | Implemented | +| 34 | `RELOCATION_RECEIVED` | Relocation | Dealer | email + system | Implemented | +| 35 | `RELOCATION_SUBMITTED` | Relocation | ASM | email + whatsapp + system | Implemented | +| 36 | `RELOCATION_APPROVED` | Relocation | Dealer | email + system | Implemented | +| 37 | `RELOCATION_UPDATE` | Relocation | Dealer | system (email optional) | Implemented | +| 38 | `FNF_INITIATED` | F&F | Finance, Dept heads, DD-Admin | email + system | Implemented | +| 39 | `FNF_SUMMARY_PREPARED` | F&F | Finance | email + system | Implemented | +| 40 | `FNF_SETTLEMENT_APPROVED` | F&F | DD-Admin, Legal, Finance | email + system | Implemented | +| 41 | `WORKFLOW_ACTION_REQUIRED` | Workflow (cross-module) | Next actor | email + whatsapp + system | Implemented | +| 42 | `WORKFLOW_STATUS_UPDATE_DEALER` | Workflow (cross-module) | Dealer | system (terminal: + email + whatsapp) | Implemented | +| 43 | `WORKNOTE_NOTIFICATION` | Collaboration | Mentioned user | email | Implemented | +| 44 | `GENERIC_NOTIFICATION` | Cross-cutting | Any | email | Implemented | +| 45 | `SLA_REMINDER` | SLA | Stage owner | email + system | Implemented | +| 46 | `SLA_BREACH_WARNING` | SLA | Stage owner | email + system | Implemented | +| 47 | `SLA_BREACH` | SLA | Stage owner + escalation | email + system | Implemented | +| 48 | `SLA_ESCALATION` | SLA | Escalation level user | email + system | Implemented | +| **B.1** | `PROSPECT_DOCUMENT_REQUEST` | Onboarding | Applicant | email + whatsapp | **Live** | +| **B.2** | `STATUTORY_DOCUMENT_REQUEST` | Onboarding | Applicant | email | **Live** | +| **B.3** | `DOCUMENT_SUBMISSION_REMINDER` | Onboarding | Applicant | email + whatsapp | **Live** (bulk) | +| **B.4** | `DOCUMENT_RECEIVED_ACKNOWLEDGEMENT` | Onboarding | Applicant | email | **Live** | +| **B.5** | `DOCUMENT_REJECTED_RESUBMIT` | Onboarding | Applicant | email + whatsapp | **Live** | +| **B.6** | `FDD_DOCUMENT_REQUEST` | Onboarding | Applicant | email | **Live** | +| **B.7** | `LOI_ACKNOWLEDGEMENT_REQUEST` | Onboarding | Applicant | email + whatsapp | **Live** (bulk) | +| **B.8** | `SECURITY_DEPOSIT_REQUEST` | Onboarding | Applicant | email + whatsapp | **Live** | +| **B.9** | `DEALERSHIP_AGREEMENT_SIGNATURE_REQUEST` | Onboarding | Applicant | email | **Live** | +| **B.10** | `ARCHITECTURAL_PLAN_REQUEST` | Onboarding | Applicant | email | **Live** | + +--- + +# Section A — Currently Implemented Mails + +For every template below the document records: + +- **Template Code**, **File path**, **Subject line** +- **Trigger (When sent)** +- **Recipients (To)** +- **Channels** (email / WhatsApp / in‑app) +- **Placeholders** +- **Current body copy** as rendered (after substituting partials) + +--- + +## A.1 — Dealer Onboarding + +### A.1.1 `OPPORTUNITY` — Opportunity Available + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/opportunity.html` | +| **Subject** | `Action Required: Royal Enfield Dealership Opportunity` | +| **Trigger** | Applicant applies for a location where a Dealer Development opportunity window is **open**. Sent automatically from `onboarding.controller.ts` on `submitApplication` and on bulk conversion of non-opportunity → opportunity. | +| **Recipient** | Applicant (external) | +| **Channels** | Email | +| **Placeholders** | `applicantName`, `location`, `applicationId`, `link`, `ctaLabel` | + +**Body (rendered):** + +> **Hi {{applicantName}},** +> +> Thank you for expressing interest in a Royal Enfield dealership opportunity for **{{location}}**. +> +> To proceed with your application, we require you to complete a mandatory business assessment questionnaire. This will help us evaluate your profile better. +> +> [ **Complete Questionnaire** ] ← CTA button linking to `{{link}}` +> +> Please complete this at your earliest convenience. If you have any questions, feel free to contact our development team. +> +> Regards, +> Royal Enfield Team + +--- + +### A.1.2 `NON_OPPORTUNITY` — Regret Email + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/non_opportunity.html` | +| **Subject** | `Update on your Royal Enfield Dealership Application` | +| **Trigger** | Applicant applies for a location where **no** opportunity window is open. | +| **Recipient** | Applicant | +| **Channels** | Email | +| **Placeholders** | `applicantName`, `location` | + +**Body:** + +> Dear {{applicantName}}, +> +> Thank you for your interest in a Royal Enfield dealership for **{{location}}**. +> +> After careful review of your application and the current organizational requirements, we regret to inform you that we are unable to proceed with your request at this time. +> +> We will keep your profile in our database for future opportunities. We wish you the very best in your future endeavors. +> +> Regards, +> Royal Enfield Team + +--- + +### A.1.3 `QUESTIONNAIRE_SUBMITTED` — Questionnaire Acknowledgement + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/questionnaire_submitted.html` | +| **Subject** | `Questionnaire Submitted Successfully: {{applicationId}}` | +| **Trigger** | Applicant submits the assessment questionnaire (`questionnaire.controller.ts` / `assessment.controller.ts`). | +| **Recipient** | Applicant | +| **Channels** | Email | +| **Placeholders** | `applicantName`, `location`, `applicationId` | + +**Body:** + +> Hi {{applicantName}}, +> +> Your business assessment questionnaire for **{{location}}** (Application ID: {{applicationId}}) has been successfully submitted. +> +> Our team will review your responses and get back to you with the next steps. +> +> Regards, +> Royal Enfield Team + +--- + +### A.1.4 `QUESTIONNAIRE_REMINDER` — Pending Questionnaire Reminder + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/questionnaire_reminder.html` | +| **Subject** | `Reminder: Complete your Dealer Assessment` | +| **Trigger** | Scheduled job (NotificationService) for applicants whose questionnaire is still pending. | +| **Recipient** | Applicant | +| **Channels** | Email + WhatsApp | +| **Placeholders** | `applicantName`, `location` | + +**Body:** + +> Hi {{applicantName}}, +> +> This is a reminder that we are awaiting your response to the dealership assessment questionnaire for **{{location}}**. +> +> Completing this questionnaire is a mandatory step to move forward with your application. +> +> [ **Complete Questionnaire** ] +> +> If you have already submitted it, please ignore this email. + +--- + +### A.1.5 `APPLICANT_SHORTLISTED` — Shortlist Notification + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/applicant_shortlisted.html` | +| **Subject** | `Congratulations! You are Shortlisted: {{applicationId}}` | +| **Trigger** | DD-Admin shortlists the application (`onboarding.controller.ts → shortlistApplication`). | +| **Recipient** | Applicant | +| **Channels** | Email | +| **Placeholders** | `applicantName`, `location`, `applicationId`, `portalLink`, `ctaLabel` | + +**Body:** + +> Hi {{applicantName}}, +> +> We are pleased to inform you that your dealership application for **{{location}}** has been shortlisted for further evaluation. +> +> Our team will contact you shortly to schedule the next level of interviews. In the meantime, you can track your application status on our dealer portal. +> +> [ **Visit Dealer Portal** ] +> +> Regards, +> Royal Enfield Dealer Development Team + +--- + +### A.1.6 `APPLICANT_REJECTED` — Rejection Notice + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/applicant_rejected.html` | +| **Subject** | `Update on Your Dealership Application — {{applicationId}}` | +| **Trigger** | `WorkflowService.transitionApplication` reaches any rejection stage (Level 1 / Level 2 / Level 3 / LOI / LOA). | +| **Recipient** | Applicant | +| **Channels** | Email + WhatsApp (if phone available) | +| **Placeholders** | `applicantName`, `applicationId`, `location`, `rejectionReason` | + +**Body:** + +> ## Application Rejected — {{applicationId}} +> +> Dear {{applicantName}}, +> +> We regret to inform you that your Royal Enfield Dealership Application (**{{applicationId}}**) for location **{{location}}** has been **rejected** after careful evaluation. +> +> **Reason for Rejection:** +> {{rejectionReason}} +> +> We appreciate your interest in partnering with Royal Enfield. You may reapply in the future when opportunities are available in your area. +> +> For any queries, please contact your local RE representative or reach us at dealer-support@royalenfield.com. +> +> Best Regards, +> Royal Enfield Dealer Development Team + +--- + +### A.1.7 `INTERVIEW_SCHEDULED` — Generic Interview Scheduled (legacy) + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/interview_scheduled.html` | +| **Subject** | `Interview Scheduled: {{applicationId}}` | +| **Trigger** | Legacy fallback when interview-specific variants are not available. | +| **Recipient** | Applicant / Panelist | +| **Channels** | Email | +| **Placeholders** | `name`, `applicationId`, `level`, `dateTime`, `type`, `location` | + +**Body:** + +> Hi {{name}}, +> +> Your interview for the Royal Enfield dealership application (**{{applicationId}}**) has been scheduled. +> +> - **Interview Level:** {{level}} +> - **Date & Time:** {{dateTime}} +> - **Mode/Location:** {{location}} +> - **Type:** {{type}} +> +> Please ensure you are available at the scheduled time. If it's a virtual interview, the link will be shared separately or is included in the location field above. + +--- + +### A.1.8 `INTERVIEW_SCHEDULED_APPLICANT` + +| Field | Value | +|---|---| +| **Seeded by** | `seed-interview-templates.ts` | +| **Subject** | `Interview Scheduled: {{applicationId}}` | +| **Trigger** | DD-Admin schedules a Level 1 / 2 / 3 interview (`assessment.controller.ts`). | +| **Recipient** | Applicant | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `applicantName`, `applicationId`, `type`, `scheduledAt`, `meetLink`, `appLink` | + +**Body:** + +> Dear {{applicantName}}, +> +> Your **{{type}}** for Royal Enfield Dealership Application ({{applicationId}}) has been scheduled. +> +> - **Scheduled Time:** {{scheduledAt}} +> - **Meeting Link/Location:** {{meetLink}} +> +> Please ensure you are available at the scheduled time. +> +> [ **Join Meeting** ] [ View Application ] +> +> Best Regards, +> Royal Enfield Onboarding Team + +--- + +### A.1.9 `INTERVIEW_SCHEDULED_PANELIST` + +| Field | Value | +|---|---| +| **Seeded by** | `seed-interview-templates.ts` | +| **Subject** | `New Interview Assignment: {{applicationId}}` | +| **Trigger** | DD-Admin assigns / re-assigns a panelist. | +| **Recipient** | Panelist (Internal user: DD-ZM, RBM, ZBH, DD-Lead, NBH, DD-Head) | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `panelistName`, `applicantName`, `applicationId`, `type`, `scheduledAt`, `meetLink`, `appLink` | + +**Body:** + +> Hi {{panelistName}}, +> +> You have been assigned as a panelist for **{{type}}** with **{{applicantName}}**. +> +> - **Application ID:** {{applicationId}} +> - **Scheduled Time:** {{scheduledAt}} +> - **Meeting Link/Location:** {{meetLink}} +> +> [ **Join Meeting** ] [ Open Assessment Dashboard ] +> +> Please review the applicant's profile before the session. +> +> Regards, +> System Administrator + +--- + +### A.1.10 `INTERVIEW_RESCHEDULED_APPLICANT` + +| Field | Value | +|---|---| +| **Subject** | `Interview Rescheduled: {{applicationId}}` | +| **Trigger** | DD-Admin reschedules an interview. | +| **Recipient** | Applicant | +| **Channels** | Email + System | + +**Body:** + +> Dear {{applicantName}}, +> +> Your **{{type}}** for Royal Enfield Dealership Application ({{applicationId}}) has been **rescheduled**. +> +> - **New Scheduled Time:** {{scheduledAt}} +> - **Meeting Link/Location:** {{meetLink}} +> +> [ **Join Meeting** ] [ View Application ] +> +> Best Regards, Royal Enfield Onboarding Team + +--- + +### A.1.11 `INTERVIEW_RESCHEDULED_PANELIST` + +Same scenario as above, sent to panelist instead of applicant. + +**Body:** + +> Hi {{panelistName}}, +> +> The **{{type}}** for **{{applicantName}}** ({{applicationId}}) has been **rescheduled**. +> +> - **New Scheduled Time:** {{scheduledAt}} +> - **Meeting Link/Location:** {{meetLink}} +> +> [ Join Meeting ] [ Open Assessment Dashboard ] +> +> Please update your calendar accordingly. + +--- + +### A.1.12 `INTERVIEW_CANCELLED_APPLICANT` + +| Field | Value | +|---|---| +| **Subject** | `Interview Cancelled: {{applicationId}}` | +| **Recipient** | Applicant | +| **Channels** | Email + System | + +**Body:** + +> Dear {{applicantName}}, +> +> We inform you that your **{{type}}** for Royal Enfield Dealership Application ({{applicationId}}) has been **cancelled**. +> +> Our team will reach out to you if a new session is required. +> +> Best Regards, Royal Enfield Onboarding Team + +--- + +### A.1.13 `INTERVIEW_CANCELLED_PANELIST` + +| Field | Value | +|---|---| +| **Subject** | `Interview Cancelled: {{applicationId}}` | +| **Recipient** | Panelist | +| **Channels** | Email + System | + +**Body:** + +> Hi {{panelistName}}, +> +> The **{{type}}** for **{{applicantName}}** ({{applicationId}}) has been **cancelled**. +> +> You no longer need to attend this session. + +--- + +### A.1.14 `USER_ASSIGNED` — Participant Assignment + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/user_assigned.html` | +| **Subject** | `New Application Assignment: {{applicationId}}` | +| **Trigger** | DD-Admin adds a participant to an application (`collaboration.controller.ts`). | +| **Recipient** | Internal user being added (e.g. FDD, Legal, Architecture, ASM) | +| **Channels** | Email + System | +| **Placeholders** | `userName`, `applicationId`, `dealerName`, `participantType` | + +**Body:** + +> Hi {{userName}}, +> +> You have been assigned as a **{{participantType}}** for the following dealership application: +> +> - **Applicant:** {{dealerName}} +> - **Application ID:** {{applicationId}} +> +> Please log in to the portal to review the application and complete your tasks. + +--- + +### A.1.15 `LOI_ISSUED` — Letter of Intent Issued + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/loi_issued.html` | +| **Subject** | `Letter of Intent (LOI) Issued: {{applicationId}}` | +| **Trigger** | DD-Admin issues the LOI after NBH approval (`loi.controller.ts → issueLoi`). | +| **Recipient** | Applicant | +| **Channels** | Email **only** (per SRS §6.16.3.6 — explicitly **no** WhatsApp for LOI/LOA documents) | +| **Placeholders** | `applicantName`, `applicationId`, `ctaLabel` | + +**Body:** + +> ## Congratulations {{applicantName}}! +> +> We are delighted to inform you that your Letter of Intent (LOI) for the Royal Enfield dealership has been issued for the application **{{applicationId}}**. +> +> Please log in to the portal to view and acknowledge the document to move to the next stage. +> +> [ **View LOI** ] + +--- + +### A.1.16 `LOA_ISSUED` — Letter of Appointment Issued + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/loa_issued.html` | +| **Subject** | `Letter of Appointment (LOA) Issued: {{applicationId}}` | +| **Trigger** | LOA fully approved by DD-Head + NBH (`loa.controller.ts`). | +| **Recipient** | Applicant + DD-Admin, DD-Lead (internal copy) | +| **Channels** | Email + System | +| **Placeholders** | `applicantName`, `applicationId`, `dealerCode`, `ctaLabel` | + +**Body:** + +> ## Welcome to the Family! +> +> Dear {{applicantName}}, +> +> We are honored to issue your Letter of Appointment (LOA) for the Royal Enfield dealership ({{applicationId}}). Your dealership is now officially authorized under the code: **{{dealerCode}}**. +> +> We look forward to a successful partnership. +> +> [ **View LOA** ] + +--- + +### A.1.17 `DEALER_CODE_READY` — SAP Dealer Codes Generated + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/dealer_code_ready.html` | +| **Subject** | `SAP Dealer Codes Readiness for {{applicationId}}` | +| **Trigger** | DD-Admin triggers SAP Dealer Code creation (`WorkflowService` → `Dealer Code Generated`). | +| **Recipient** | Applicant + DD-Admin, Finance, Sales, Service heads | +| **Channels** | Email + System | +| **Placeholders** | `applicantName`, `applicationId`, `salesCode`, `serviceCode` | + +**Body:** + +> ## SAP Dealer Codes Generated +> +> Hi {{applicantName}}, +> +> We are pleased to inform you that your SAP Dealer Codes for application **{{applicationId}}** are now ready and active in our system. +> +> ``` +> Sales Code: {{salesCode}} +> Service Code: {{serviceCode}} +> ``` +> +> You can now proceed with system onboarding and initial orders. + +--- + +### A.1.18 `ONBOARDING_STATUS_UPDATE` — Generic Status Update + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/onboarding_status_update.html` | +| **Subject** | `Onboarding Status Update: {{status}} — {{applicationId}}` | +| **Trigger** | Any onboarding status change not covered by LOI / LOA / Dealer Code specific mails (`WorkflowService.transitionApplication`). | +| **Recipient** | Applicant | +| **Channels** | Email + System | +| **Placeholders** | `applicantName`, `applicationId`, `status`, `reason`, `salesCode`, `serviceCode` | + +**Body:** + +> Hi {{applicantName}}, +> +> Your dealership onboarding application status has been updated. +> +> - **Current status:** {{status}} +> - **Application ID:** {{applicationId}} +> - **Details:** {{reason}} +> +> You can sign in to the dealer development portal for more information. +> +> [ **View Application** ] + +--- + +### A.1.19 `ONBOARDING_PAYMENT_VERIFIED` — Security Deposit / Payment Verified + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/onboarding_payment_verified.html` | +| **Subject** | `Payment Verified: {{applicationId}}` | +| **Trigger** | Finance verifies the Security Deposit / initial payment (`settlement.controller.ts`). | +| **Recipient** | DD-Admin, DD-Lead, Finance Team (Internal copy). The applicant is acknowledged via the workflow status update. | +| **Channels** | Email + System | +| **Placeholders** | `applicationId`, `dealerName`, `paymentType`, `amount`, `link` | + +**Body:** + +> ## Payment Verified — {{applicationId}} +> +> Dear Team, +> +> Finance has successfully verified the **{{paymentType}}** for **{{dealerName}}** (Application ID: **{{applicationId}}**). +> +> - **Verified Amount:** ₹{{amount}} +> - **Status:** Verified & Approved +> +> The onboarding process can now proceed to the next stage. +> +> [ **View Application** ] + +--- + +### A.1.20 `EOR_COMPLETED` — EOR Checklist 100% Complete + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/eor_completed.html` | +| **Subject** | `EOR Readiness 100% Completed: {{applicationId}}` | +| **Trigger** | All EOR checklist items verified (`eor.controller.ts`). | +| **Recipient** | DD-Head, NBH (also applies to relocation-EOR scenario for DD-Admin) | +| **Channels** | Email + System (alert-class — SRS §6.19.3.4) | +| **Placeholders** | `recipientName`, `applicantName`, `requestId`, `location`, `completedOn`, `link` | + +**Body:** + +> ## EOR Checklist Complete — {{requestId}} +> +> Dear {{recipientName}}, +> +> All **Essential Operating Requirements (EOR)** for dealer application **{{requestId}}** (Applicant: **{{applicantName}}**) have been marked as completed and verified. +> +> **EOR Status: 100% Complete** — The dealership outlet is now ready for Inauguration review. +> +> | Field | Value | +> |---|---| +> | Application ID | {{requestId}} | +> | Applicant Name | {{applicantName}} | +> | Location | {{location}} | +> | Completed On | {{completedOn}} | +> +> Please review the EOR checklist and authorize the **Inauguration** stage to mark this dealership as live. +> +> [ **Authorize Inauguration** ] + +--- + +### A.1.21 `INAUGURATION_COMPLETED` — Dealership Inauguration Logged + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/inauguration_completed.html` | +| **Subject** | `Dealership Inauguration Logged: {{applicationId}}` | +| **Trigger** | DD-Admin marks the inauguration as complete; dealership goes Live. | +| **Recipient** | DD-Admin, DD-Lead, NBH, Finance, Sales, Service | +| **Channels** | Email + System | +| **Placeholders** | `dealerName`, `applicationId`, `location`, `date` | + +**Body:** + +> ## Dealership Inauguration Logged — {{applicationId}} +> +> Dear Team, +> +> We are pleased to inform you that the inauguration for dealer **{{dealerName}}** (Application ID: **{{applicationId}}**) at **{{location}}** has been successfully logged on **{{date}}**. +> +> The dealership is now marked as **Active** in the system. All relevant teams are requested to update their records accordingly. + +--- + +## A.2 — Dealer Resignation + +### A.2.1 `RESIGNATION_RECEIVED` — Dealer Acknowledgement + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/resignation_received.html` | +| **Subject** | `We received your resignation — {{resignationId}}` | +| **Trigger** | Dealer submits resignation via portal (`workflow-email-notifications.ts → notifyResignationSubmittedEmails`). | +| **Recipient** | Dealer | +| **Channels** | Email + WhatsApp | +| **Placeholders** | `dealerName`, `resignationId`, `lwd`, `link`, `ctaLabel` | + +**Body:** + +> Hi {{dealerName}}, +> +> We have received your resignation request **{{resignationId}}**. +> +> **Last working day (as submitted):** {{lwd}} +> +> Our team will review and progress clearances as per process. You can track status anytime on the portal. +> +> [ **View Request** ] + +--- + +### A.2.2 `RESIGNATION_SUBMITTED` — Internal Notification + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/resignation_submitted.html` | +| **Subject** | `New Resignation Request: {{resignationId}}` | +| **Trigger** | Same event as A.2.1, but for internal reviewers. | +| **Recipient** | ASM (DD-ASM), RBM, DD-ZM, ZBH, DD-Lead, DD-Head, NBH, Legal, DD-Admin | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `dealerName`, `resignationId`, `lwd`, `link`, `ctaLabel` | + +**Body:** + +> Hi Team, +> +> A new resignation request has been submitted by **{{dealerName}}**. +> +> - **Request ID:** {{resignationId}} +> - **Proposed Last Working Day:** {{lwd}} +> +> Please log in to the Dealer Development portal to review and initiate the clearance process. +> +> [ **Review Resignation** ] + +--- + +### A.2.3 `RESIGNATION_APPROVED` — Resignation Approved + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/resignation_approved.html` | +| **Subject** | `Resignation Request Approved: {{resignationId}}` | +| **Trigger** | NBH issues final approval; Legal uploads acceptance letter. | +| **Recipient** | Dealer | +| **Channels** | Email + System | +| **Placeholders** | `dealerName`, `resignationId`, `lwd` | + +**Body:** + +> ## Resignation Request Approved +> +> Dear {{dealerName}}, +> +> Your resignation request (Request ID: {{resignationId}}) has been approved by the Dealer Development team. +> +> **Proposed Last Working Day:** {{lwd}} +> +> The clearance process has been initiated. Please ensure all department dues are cleared as per the timeline. + +--- + +### A.2.4 `RESIGNATION_UPDATE` — Interim Status Update + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/resignation_update.html` | +| **Subject** | `Resignation Status Update: {{status}}` | +| **Trigger** | Any interim stage transition during resignation. | +| **Recipient** | Dealer | +| **Channels** | System (email optional via Admin) | +| **Placeholders** | `dealerName`, `status`, `remarks` | + +**Body:** + +> Hi {{dealerName}}, +> +> This is to inform you that the status of your resignation request has been updated. +> +> - **Current Stage:** {{status}} +> +> {{remarks}} +> +> You can track the progress and clearance status on the dealer development portal. +> +> [ **View Status** ] + +--- + +## A.3 — Dealer Termination + +### A.3.1 `TERMINATION_INITIATED` — Case Initiation Internal Alert + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/termination_initiated.html` | +| **Subject** | `Dealer Termination Case Initiated — {{requestId}}` | +| **Trigger** | DD-ASM creates a termination request (`termination.controller.ts`). | +| **Recipient** | RBM, DD-ZM, ZBH, DD-Lead, Legal, NBH | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `recipientName`, `dealerName`, `requestId`, `category`, `initiatedBy`, `currentStage`, `remarks`, `link`, `ctaLabel` | + +**Body (key bits):** + +> ## Dealer Termination Initiated — {{requestId}} +> +> Dear {{recipientName}}, +> +> A formal termination process has been initiated for dealer **{{dealerName}}** (Request: **{{requestId}}**). +> +> | Field | Value | +> |---|---| +> | Request ID | {{requestId}} | +> | Dealer Name | {{dealerName}} | +> | Termination Category | {{category}} | +> | Initiated By | {{initiatedBy}} | +> | Current Stage | {{currentStage}} | +> +> **Remarks:** {{remarks}} +> +> Please review the termination case and provide your evaluation as required. All decisions must be documented with mandatory work notes. +> +> [ **Review Termination Case** ] +> +> *This notification is confidential and intended only for the named recipient. Do not share this information externally without authorization.* + +--- + +### A.3.2 `TERMINATION_SCN_ISSUED` — Show Cause Notice + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/termination_scn.html` | +| **Subject** | `URGENT: Show Cause Notice Issued: {{terminationId}}` | +| **Trigger** | Legal uploads & issues SCN (`TerminationWorkflowService` / `termination.controller.ts → issueScn`). | +| **Recipient** | Dealer + DD-Admin / Legal copy | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `dealerName`, `terminationId`, `deadline`, `link`, `ctaLabel` | + +**Body:** + +> ## URGENT: Show Cause Notice Issued +> +> Dear {{dealerName}}, +> +> This is to inform you that a **Show Cause Notice** has been issued regarding your dealership contract (Request ID: {{terminationId}}). +> +> You are required to submit your response on the dealer development portal by **{{deadline}}**. Failure to respond may lead to further action as per the dealership agreement. +> +> [ **Submit Response** ] + +--- + +### A.3.3 `TERMINATION_LETTER_ISSUED` — Internal Letter Generated Alert + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/termination_letter_issued.html` | +| **Subject** | `Termination Letter Generated: {{requestId}}` | +| **Trigger** | Legal generates/uploads the official termination letter (`workflow-email-notifications.ts` → terminal event override). | +| **Recipient** | DD-Lead, DD-Admin, Finance | +| **Channels** | Email + System | +| **Placeholders** | `recipientName`, `dealerName`, `requestId`, `link`, `ctaLabel` | + +**Body:** + +> ## Termination Letter Generated — {{requestId}} +> +> Dear {{recipientName}}, +> +> The official **Termination Letter** has been generated and uploaded to the portal for dealer **{{dealerName}}** (Request ID: **{{requestId}}**). +> +> This letter is now visible to the DD-Lead, DD-Admin, and Finance teams. Please proceed with the necessary administrative and financial closure steps. +> +> [ **View Letter** ] + +--- + +### A.3.4 `TERMINATION_FINAL_CLOSURE_DEALER` — Final Closure to Dealer + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/termination_final_closure.html` | +| **Subject** | `Official Notice: Termination of Dealership — {{requestId}}` | +| **Trigger** | Workflow reaches `TERMINATION_STAGES.TERMINATED` (terminal event for dealer). | +| **Recipient** | Dealer | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `dealerName`, `requestId`, `terminationDate`, `link`, `ctaLabel` | + +**Body:** + +> ## Notice of Termination — {{requestId}} +> +> Dear {{dealerName}}, +> +> This is the official notification that your dealership agreement with Royal Enfield (Request ID: **{{requestId}}**) has been formally terminated effective **{{terminationDate}}**. +> +> The final Termination Letter is available for your reference in the dealer portal. Your portal access will remain active for a limited period to allow you to download relevant documents and track the Full & Final (F&F) settlement progress. +> +> [ **View Letter & Portal** ] +> +> For any clarifications regarding the settlement process, please contact the Dealer Development Admin team. +> +> Best Regards, +> Royal Enfield Dealer Development Team + +--- + +### A.3.5 `TERMINATION_UPDATE` — Interim Status to Dealer + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/termination_update.html` | +| **Subject** | `Termination Status Update: {{status}}` | +| **Recipient** | Dealer | +| **Channels** | System (email optional) | +| **Placeholders** | `dealerName`, `status`, `remarks` | + +**Body:** + +> Hi {{dealerName}}, +> +> This is to inform you that the status of your dealership termination request has been updated. +> +> - **Current Stage:** {{status}} +> +> {{remarks}} +> +> Please log in to the portal to check if any actions are required from your end. + +--- + +## A.4 — Constitutional Change + +### A.4.1 `CONSTITUTIONAL_CHANGE_SUBMITTED` — Internal Alert + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/constitutional_change_submitted.html` | +| **Subject** | `New Constitutional Change Request: {{requestId}}` | +| **Trigger** | Dealer submits constitutional change request (`constitutional.controller.ts`). | +| **Recipient** | ASM, RBM, DD-ZM, ZBH, DD-Lead, DD-Head, NBH, Legal | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `dealerName`, `changeType`, `requestId`, `link`, `ctaLabel` | + +**Body:** + +> Hi Team, +> +> A new Constitutional Change request has been submitted by **{{dealerName}}**. +> +> - **Request Type:** {{changeType}} +> - **Request ID:** {{requestId}} +> +> Please log in to the Dealer Development portal to review the request and documents. +> +> [ **Review Request** ] + +--- + +### A.4.2 `CONSTITUTIONAL_CHANGE_APPROVED` — Final Approval to Dealer + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/constitutional_change_approved.html` | +| **Subject** | `Constitutional Change Approved — {{requestId}}` | +| **Trigger** | Workflow reaches `CONSTITUTIONAL_STAGES.COMPLETED` (terminal). | +| **Recipient** | Dealer | +| **Channels** | Email + System | +| **Placeholders** | `dealerName`, `requestId`, `proposedConstitution`, `link` | + +**Body:** + +> ## Constitutional Change Approved — {{requestId}} +> +> Dear {{dealerName}}, +> +> We are pleased to inform you that your request for a **Change in Constitution** (Request ID: **{{requestId}}**) has been officially approved by the Royal Enfield management. +> +> - **New Constitution:** {{proposedConstitution}} +> - **Status:** Approved & Updated +> +> The system records have been updated to reflect this change. You can now proceed with the legally compliant transition as per the approved structure. +> +> [ **View Request Details** ] + +--- + +### A.4.3 `CONSTITUTIONAL_CHANGE_UPDATE` — Interim Update + +| Field | Value | +|---|---| +| **Subject** | `Constitutional Change Update: {{status}}` | +| **Recipient** | Dealer | +| **Channels** | System (email optional) | + +**Body:** + +> Hi {{dealerName}}, +> +> This is to inform you that your Constitutional Change request has been updated. +> +> - **Current Stage:** {{status}} +> +> {{remarks}} +> +> [ **View Status** ] + +--- + +## A.5 — Dealer Relocation + +### A.5.1 `RELOCATION_RECEIVED` — Dealer Acknowledgement + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/relocation_received.html` | +| **Subject** | `Relocation request received — {{requestId}}` | +| **Trigger** | Dealer submits a relocation request. | +| **Recipient** | Dealer | +| **Channels** | Email + System | +| **Placeholders** | `dealerName`, `requestId`, `link`, `ctaLabel`, `distance` | + +**Body:** + +> ## Relocation Request Received — {{requestId}} +> +> Dear {{dealerName}}, +> +> We have received your request for dealership relocation (Request ID: **{{requestId}}**). +> +> The internal feasibility assessment and multi-level review process have been initiated. You will be notified of any document requirements or status updates via the portal and email. +> +> **Current Status:** ASM Review (In Progress) +> +> [ **Track Request** ] +> +> You can track the real-time progress of your request by logging into the Dealer Portal. + +--- + +### A.5.2 `RELOCATION_SUBMITTED` — Internal Alert to ASM + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/relocation_submitted.html` | +| **Subject** | `New relocation request: {{requestId}}` | +| **Trigger** | Same event as A.5.1, sent to assigned ASM. | +| **Recipient** | ASM | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `dealerName`, `requestId`, `outletCode`, `link`, `ctaLabel`, `distance` | + +**Body:** + +> ## New Relocation Request: {{requestId}} +> +> Dear Team, +> +> A new dealership relocation request has been submitted by **{{dealerName}}** for outlet **{{outletCode}}**. +> +> - Request ID: {{requestId}} +> - Outlet Code: {{outletCode}} +> - Distance from existing site: {{distance}} km +> +> Please log in to the Dealer Development portal to review the proposed location, property documents, and feasibility details. +> +> [ **Review Request** ] + +--- + +### A.5.3 `RELOCATION_APPROVED` — Final Approval to Dealer + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/relocation_approved.html` | +| **Subject** | `Relocation Approved — {{requestId}}` | +| **Recipient** | Dealer | +| **Channels** | Email + System | +| **Placeholders** | `dealerName`, `requestId`, `newLocation`, `link` | + +**Body:** + +> ## Relocation Approved — {{requestId}} +> +> Dear {{dealerName}}, +> +> We are pleased to inform you that your request for **Dealership Relocation** (Request ID: **{{requestId}}**) has been officially approved by the Royal Enfield management. +> +> - **New Location:** {{newLocation}} +> - **Status:** Approved & Records Updated +> +> Our team will coordinate with you for the physical transition and site readiness audit. You can track the next steps via the portal. +> +> [ **View Request Details** ] + +--- + +### A.5.4 `RELOCATION_UPDATE` — Interim Status + +| Field | Value | +|---|---| +| **Subject** | `Relocation update — {{requestId}} ({{status}})` | +| **Recipient** | Dealer | +| **Channels** | System (email optional) | + +**Body:** + +> Hi {{dealerName}}, +> +> Your relocation request **{{requestId}}** has been updated. +> +> - **Current stage:** {{status}} +> +> {{remarks}} +> +> [ **View Status** ] + +--- + +## A.6 — Full & Final (F&F) Settlement + +### A.6.1 `FNF_INITIATED` — F&F Triggered + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/fnf_initiated.html` | +| **Subject** | `F&F Settlement Initiated — {{requestId}}` | +| **Trigger** | F&F case is created on or after the Last Working Day (resignation closure / termination closure / settlement controller). | +| **Recipient** | Finance, Spares Manager, Service Manager, Accounts Manager, DD-Admin, DD-Lead, ZBH, RBM | +| **Channels** | Email + System | +| **Placeholders** | `recipientName`, `dealerName`, `requestId`, `lwd`, `initiatedBy`, `link`, `ctaLabel` | + +**Body:** + +> ## Full & Final Settlement Initiated — {{requestId}} +> +> Dear {{recipientName}}, +> +> The Full & Final (F&F) settlement process has been initiated for dealer **{{dealerName}}** (Request: **{{requestId}}**) effective from the Last Working Day. +> +> | Field | Value | +> |---|---| +> | Request ID | {{requestId}} | +> | Dealer Name | {{dealerName}} | +> | Initiated By | {{initiatedBy}} | +> | Last Working Day | {{lwd}} | +> +> All department clearances (NOC, Payables, Receivables, etc.) must be submitted within the stipulated timeline. Please log in and update your department's clearance status. +> +> [ **View F&F Settlement** ] + +--- + +### A.6.2 `FNF_SUMMARY_PREPARED` — Summary Ready for Finance + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/fnf_summary_prepared.html` | +| **Subject** | `F&F Settlement Summary Prepared: {{fnfId}}` | +| **Trigger** | All 16 department clearances received; Finance auto-summary computed. | +| **Recipient** | Finance team | +| **Channels** | Email + System | +| **Placeholders** | `fnfId`, `dealerName`, `netAmount`, `link`, `ctaLabel` | + +**Body:** + +> ## F&F Settlement Summary Prepared — {{fnfId}} +> +> Dear Finance Team, +> +> The initial Full & Final (F&F) settlement summary has been prepared for **{{dealerName}}** (F&F ID: **{{fnfId}}**). +> +> - **Calculated Net Amount:** ₹{{netAmount}} +> - **Status:** Pending Final Approval +> +> Please review the consolidated departmental responses and the settlement summary to proceed with final approval and payment processing. +> +> [ **Review & Approve** ] +> +> *Confidential: This summary contains sensitive financial data. Review only via authorized portal access.* + +--- + +### A.6.3 `FNF_SETTLEMENT_APPROVED` — Final F&F Approval + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/fnf_settlement_approved.html` | +| **Subject** | `F&F Settlement Approved: {{fnfId}}` | +| **Trigger** | Finance approves the final settlement summary. | +| **Recipient** | DD-Admin, Legal, Finance | +| **Channels** | Email + System | +| **Placeholders** | `fnfId`, `dealerName`, `settlementAmount`, `link`, `ctaLabel` | + +**Body:** + +> ## F&F Settlement Approved — {{fnfId}} +> +> Dear Team, +> +> The final Full & Final (F&F) settlement for dealer **{{dealerName}}** (F&F ID: **{{fnfId}}**) has been **Approved** by Finance. +> +> - **Settlement Amount:** ₹{{settlementAmount}} +> - **Status:** Approved & Closed +> +> The DD-Admin and Legal teams are requested to update their records and proceed with final account closure. +> +> [ **View Settlement** ] + +--- + +## A.7 — Workflow / Collaboration / Generic + +### A.7.1 `WORKFLOW_ACTION_REQUIRED` — Generic Next-Actor Alert + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/workflow_action_required.html` | +| **Subject** | `Action Required: {{requestId}} — {{targetStage}}` | +| **Trigger** | Cross-module workflow transition reaches the next actor's stage (or a send-back to ASM occurs). | +| **Recipient** | Next assigned actor (role-based; DD-ZM / RBM / ZBH / DD-Lead / DD-Head / NBH / Legal / Finance / FDD / Architecture) | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `requestId`, `dealerName`, `targetStage`, `remarks`, `link`, `ctaLabel` | + +**Body:** + +> ## Action Required: {{requestId}} +> +> Hi, +> +> The request **{{requestId}}** (Dealer: **{{dealerName}}**) has reached the **{{targetStage}}** stage and requires your review and action. +> +> **Remarks from previous stage:** {{remarks}} +> +> Please log in to the RE Dealer Management Portal to review the case details and take action. +> +> [ **Review & Take Action** ] +> +> *This is an automated notification. Please do not reply to this email.* + +--- + +### A.7.2 `WORKFLOW_STATUS_UPDATE_DEALER` — Generic Dealer Update + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/workflow_status_update_dealer.html` | +| **Subject** | `Update on Your Request — {{requestId}}` | +| **Trigger** | Any interim or terminal workflow event (Resignation / Termination / Constitutional / Relocation / Onboarding) that affects the dealer. | +| **Recipient** | Dealer | +| **Channels** | System always; Email + WhatsApp on terminal events (rejection / completion / revocation) | +| **Placeholders** | `requestId`, `dealerName`, `targetStage`, `remarks`, `link`, `ctaLabel` | + +**Body:** + +> ## Update on Your Request — {{requestId}} +> +> Dear {{dealerName}}, +> +> Your request **{{requestId}}** has been updated. Current status: **{{targetStage}}**. +> +> **Note:** {{remarks}} +> +> You can track the live progress of your request on the dealer portal: +> +> [ **View Request** ] + +--- + +### A.7.3 `WORKNOTE_NOTIFICATION` — Mention Notification + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/worknote_notification.html` | +| **Subject** | `New Update on Application: {{applicationId}}` | +| **Trigger** | A user is @mentioned in a work note (`collaboration.controller.ts`). | +| **Recipient** | The mentioned user | +| **Channels** | Email | +| **Placeholders** | `userName`, `applicationId`, `dealerName`, `message` | + +**Body:** + +> Hi {{userName}}, +> +> You have a new work note or update on an application you are participating in. +> +> - **Application:** {{dealerName}} ({{applicationId}}) +> - **Update:** {{message}} +> +> [ **View Worknote** ] + +--- + +### A.7.4 `GENERIC_NOTIFICATION` — Fallback Template + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/generic_notification.html` | +| **Subject** | `{{title}}` | +| **Trigger** | Used by NotificationService when no specific template applies. | +| **Recipient** | Any user | +| **Channels** | Email | +| **Placeholders** | `title`, `message` | + +**Body:** + +> ## {{title}} +> +> {{message}} +> +> Please log in to the portal for more details. + +--- + +## A.8 — SLA & Escalation Mails + +### A.8.1 `SLA_REMINDER` — Approaching Deadline + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/sla_reminder.html` | +| **Subject** | `SLA Reminder: {{applicationId}} — {{stageName}}` | +| **Trigger** | SLA scheduler detects stage nearing TAT. | +| **Recipient** | Current stage owner | +| **Channels** | Email + System | +| **Placeholders** | `applicationId`, `stageName`, `link` | + +**Body:** + +> ## SLA Reminder +> +> This is a reminder that the following application is approaching its SLA deadline. +> +> - **Application ID:** {{applicationId}} +> - **SLA Stage:** {{stageName}} +> +> Please ensure this stage is completed promptly to avoid a breach. +> +> [ **Open Case** ] + +--- + +### A.8.2 `SLA_BREACH_WARNING` + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/sla_breach_warning.html` | +| **Subject** | `SLA breach: {{applicationId}} — {{stageName}}` | +| **Trigger** | An SLA tracking record breaches its deadline. | +| **Recipient** | Stage owner | +| **Channels** | Email + System | +| **Placeholders** | `applicationId`, `stageName`, `currentStage` | + +**Body:** + +> ## SLA breach alert +> +> The following application has breached SLA for a workflow stage. +> +> - Application ID: {{applicationId}} +> - SLA stage: {{stageName}} +> - Current stage: {{currentStage}} +> +> Please take immediate action in the portal. +> +> [ **Open Case** ] + +--- + +### A.8.3 `SLA_BREACH` + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/sla_breach.html` | +| **Subject** | `SLA BREACHED: {{applicationId}} — {{stageName}}` | +| **Trigger** | Confirmed breach event. | +| **Recipient** | Stage owner + escalation cohort | +| **Channels** | Email + System | + +**Body:** + +> ## SLA BREACHED +> +> The following application has exceeded its allotted Turnaround Time (TAT). +> +> - **Application ID:** {{applicationId}} +> - **SLA Stage:** {{stageName}} +> +> Action is required immediately to minimize further delay. +> +> [ **Open Case** ] + +--- + +### A.8.4 `SLA_ESCALATION` — Multi-Level Escalation + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/sla_escalation.html` | +| **Subject** | `SLA ESCALATION [L{{level}}]: {{applicationId}} — {{stageName}}` | +| **Trigger** | Escalation chain L1 → L2 → L3 triggered when previous level does not act. | +| **Recipient** | Escalation level user (typically RBM → ZBH → DD-Head / NBH) | +| **Channels** | Email + System | +| **Placeholders** | `applicationId`, `stageName`, `level`, `timeValue`, `timeUnit`, `link` | + +**Body:** + +> ## SLA ESCALATION [Level {{level}}] +> +> The following application remains incomplete after an SLA breach for **{{stageName}}** and has been escalated to you. +> +> - **Application ID:** {{applicationId}} +> - **Escalation Level:** Level {{level}} +> - **Delay:** {{timeValue}} {{timeUnit}} past breach +> +> Please review and intervene to resolve the pending activity. +> +> [ **Open Case** ] + +--- + +# Section B — Onboarding extensions (implemented) + +> **For clients / Marketing / Legal:** Use **`RE_Dealer_Email_Content_Client_Brief.md`** in this folder — no code, only audiences, triggers, and sign-off. +> Subsections **B.1–B.10** below keep technical fields (files, placeholders) and sample prose for administrators. + +Templates are in `backend/src/emailtemplates/`, codes in `allowed-email-template-codes.ts`, seeds in `seed-master-emails.ts`, and triggers in `onboarding.controller.ts`, `loi.controller.ts`, `loa.controller.ts`, and `fdd.controller.ts` as noted per template. + +--- + +## B.1 `PROSPECT_DOCUMENT_REQUEST` — Initial prospect document collection *(implemented)* + +| Field | Value | +|---|---| +| **File** | `backend/src/emailtemplates/prospect_document_request.html` | +| **Subject** | `Documents Required — Royal Enfield Dealership Application {{applicationId}}` | +| **Trigger** | Automatically after **shortlist** (same flow as shortlisted congratulations mail). | +| **Recipient** | Applicant; WhatsApp if mobile on file. | +| **Placeholders** | `applicantName`, `location`, `applicationId`, `dueDate`, `documentList`, `link`, `ctaLabel` | + +> Hi {{applicantName}}, +> +> Congratulations on being shortlisted for the Royal Enfield Dealership opportunity at **{{location}}** (Application ID: **{{applicationId}}**). +> +> To proceed with your evaluation we need the following documents to be uploaded on the Dealer Portal by **{{dueDate}}**: +> +> 1. PAN Card (Proprietor / Partners / Directors) +> 2. Aadhaar Card or Passport +> 3. Address Proof of the proposed dealership site (Lease deed / Title deed) +> 4. Net Worth Certificate from a Chartered Accountant (last 2 years) +> 5. Bank Statement (last 6 months) +> 6. Income Tax Returns (last 3 years) +> 7. Photograph(s) of the proposed dealership site +> +> *(Final document list is determined by your application type — please refer to the portal for the authoritative checklist.)* +> +> [ **Upload Documents** ] → opens `{{uploadLink}}` +> +> All documents must be clear, in PDF / JPG format, and under 10 MB per file. For any clarifications, contact your local RE representative or write to dealer-support@royalenfield.com. +> +> Regards, +> Royal Enfield Dealer Development Team + +--- + +## B.2 `STATUTORY_DOCUMENT_REQUEST` — Statutory Documents Collection (Post LOI) + +| Field | Value | +|---|---| +| **Subject** | `Statutory Documents Required — {{applicationId}}` | +| **Trigger** | Sent after the LOI is issued. Aligns with the Architectural Work stage and Statutory Document Collection sub-stage of Module 1.8.2 in the test stories. | +| **Recipient** | Applicant (now Dealer-in-making) | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `applicantName`, `applicationId`, `dealerCode`, `statutoryList`, `dueDate`, `uploadLink`, `ctaLabel` | + +**Proposed body:** + +> Hi {{applicantName}}, +> +> With the Letter of Intent issued for application **{{applicationId}}**, we now need the statutory documents necessary to open the dealership at the agreed location. +> +> Please upload the following on the Dealer Portal by **{{dueDate}}**: +> +> 1. **GST Registration Certificate** +> 2. **Trade License / Shop & Establishment Certificate** from the local municipal authority +> 3. **MSME / Udyam Registration Certificate** (if applicable) +> 4. **Fire Safety NOC** from the local fire department +> 5. **Pollution Control Board NOC** +> 6. **Building Plan Approval** from the local municipal corporation +> 7. **Electricity / Power Sanction Letter** (commercial) +> 8. **Lease Agreement / Sale Deed** (registered) of the dealership premises +> 9. **Property Tax Receipt** (latest) +> 10. **Insurance** — Building, Inventory & Public Liability +> 11. **Bank Account Opening Letter** in the dealership entity's name +> 12. **Memorandum / Articles of Association** (for Pvt. Ltd. entities) **or** Partnership Deed (for LLPs / Firms) +> 13. **Authorized Signatory List** + Board Resolution (where applicable) +> +> [ **Upload Statutory Documents** ] → `{{uploadLink}}` +> +> These documents are mandatory for the issuance of the Letter of Appointment (LOA) and SAP Dealer Codes. Any delay or missing documents will postpone your onboarding timeline. Our DD-Admin team will reach out for clarifications if anything needs re-submission. +> +> Regards, +> Royal Enfield Dealer Development Team + +--- + +## B.3 `DOCUMENT_SUBMISSION_REMINDER` — Pending Document Reminder + +| Field | Value | +|---|---| +| **Subject** | `Reminder: Pending documents for {{applicationId}}` | +| **Trigger** | Cron job — fires if a prospect / dealer has not uploaded all required documents within the configured SLA (e.g. T+3 days after `PROSPECT_DOCUMENT_REQUEST` / `STATUTORY_DOCUMENT_REQUEST`). | +| **Recipient** | Applicant | +| **Channels** | Email + WhatsApp | +| **Placeholders** | `applicantName`, `applicationId`, `pendingDocuments`, `dueDate`, `uploadLink` | + +**Proposed body:** + +> Hi {{applicantName}}, +> +> Friendly reminder — the following documents are still pending against your application **{{applicationId}}**: +> +> {{pendingDocuments}} +> +> Please upload them by **{{dueDate}}** to avoid delays in your application processing. +> +> [ **Upload Pending Documents** ] +> +> If you have already uploaded these documents, please ignore this email. + +--- + +## B.4 `DOCUMENT_RECEIVED_ACKNOWLEDGEMENT` + +| Field | Value | +|---|---| +| **Subject** | `Documents received — {{applicationId}}` | +| **Trigger** | When the prospect / dealer uploads all mandatory documents for a category. | +| **Recipient** | Applicant + DD-Admin (cc) | +| **Channels** | Email | +| **Placeholders** | `applicantName`, `applicationId`, `documentCategory`, `receivedOn`, `link` | + +**Proposed body:** + +> Hi {{applicantName}}, +> +> Thank you. We have received the following documents for your dealership application **{{applicationId}}** (Category: **{{documentCategory}}**) on **{{receivedOn}}**. +> +> Our DD-Admin and Legal teams will now verify the submitted documents. You will receive a separate notification if any document needs to be re-submitted, or once the verification is complete. +> +> [ **View Submission** ] + +--- + +## B.5 `DOCUMENT_REJECTED_RESUBMIT` — Resubmission Request + +| Field | Value | +|---|---| +| **Subject** | `Action Required: Re-submit documents for {{applicationId}}` | +| **Trigger** | DD-Admin / Legal marks a submitted document as Rejected / Re-submission Required. | +| **Recipient** | Applicant | +| **Channels** | Email + WhatsApp + System | +| **Placeholders** | `applicantName`, `applicationId`, `documentName`, `rejectionReason`, `dueDate`, `uploadLink` | + +**Proposed body:** + +> Hi {{applicantName}}, +> +> One or more documents you uploaded for application **{{applicationId}}** could not be accepted. +> +> - **Document:** {{documentName}} +> - **Reason:** {{rejectionReason}} +> +> Please re-upload the corrected document on the Dealer Portal by **{{dueDate}}**. +> +> [ **Re-Upload Document** ] +> +> If you have any queries regarding the reason, please contact your assigned DD representative or write to dealer-support@royalenfield.com. + +--- + +## B.6 `FDD_DOCUMENT_REQUEST` — FDD (Financial Due Diligence) Document Collection + +| Field | Value | +|---|---| +| **Subject** | `FDD Documents Required — {{applicationId}}` | +| **Trigger** | DD-Admin assigns an FDD partner; the partner needs to receive a complete document checklist from the applicant. | +| **Recipient** | Applicant (cc: FDD partner, DD-Admin) | +| **Channels** | Email | +| **Placeholders** | `applicantName`, `applicationId`, `fddPartnerName`, `documentChecklist`, `dueDate`, `uploadLink` | + +**Proposed body:** + +> Hi {{applicantName}}, +> +> As part of the Financial Due Diligence (FDD) for your dealership application **{{applicationId}}**, **{{fddPartnerName}}** has been engaged to perform the assessment. +> +> Please upload the following documents on the Dealer Portal by **{{dueDate}}**: +> +> {{documentChecklist}} +> +> Typical documents include audited financial statements (last 3 FYs), GST returns (last 12 months), bank statements (last 12 months), proof of working capital, existing loan obligations, and director / partner net worth statements. +> +> [ **Upload FDD Documents** ] +> +> The FDD partner will independently contact you for any clarifications. Any non-disclosure will impact your application evaluation. + +--- + +## B.7 `LOI_ACKNOWLEDGEMENT_REQUEST` + +| Field | Value | +|---|---| +| **Subject** | `Action Required: Acknowledge your Letter of Intent — {{applicationId}}` | +| **Trigger** | Within 24h of `LOI_ISSUED` if the applicant has not signed / acknowledged the LOI in the portal. | +| **Recipient** | Applicant | +| **Channels** | Email + WhatsApp | +| **Placeholders** | `applicantName`, `applicationId`, `loiLink`, `dueDate` | + +**Proposed body:** + +> Hi {{applicantName}}, +> +> Your Letter of Intent (LOI) for **{{applicationId}}** is awaiting your acknowledgement. +> +> Please log in to the Dealer Portal, review the document, and digitally acknowledge it by **{{dueDate}}**. Without your acknowledgement we cannot proceed to the next stage of onboarding. +> +> [ **Acknowledge LOI** ] + +--- + +## B.8 `SECURITY_DEPOSIT_REQUEST` + +| Field | Value | +|---|---| +| **Subject** | `Security Deposit Payment Required — {{applicationId}}` | +| **Trigger** | After LOI is acknowledged by the applicant; before LOA processing. | +| **Recipient** | Applicant | +| **Channels** | Email | +| **Placeholders** | `applicantName`, `applicationId`, `amount`, `paymentLink`, `bankDetails`, `dueDate` | + +**Proposed body:** + +> Hi {{applicantName}}, +> +> Per the terms of your Letter of Intent for **{{applicationId}}**, the next step is to remit the **Security Deposit of ₹{{amount}}** before **{{dueDate}}**. +> +> You may pay through any of the following channels: +> +> - Online payment via the Dealer Portal: [ **Pay Now** ] → `{{paymentLink}}` +> - RTGS / NEFT to the following account: +> +> ``` +> {{bankDetails}} +> ``` +> +> After remitting, please upload the payment confirmation (UTR / Transaction reference) on the portal. Finance will verify within 2 working days and we will notify you (see `ONBOARDING_PAYMENT_VERIFIED`). + +--- + +## B.9 `DEALERSHIP_AGREEMENT_SIGNATURE_REQUEST` + +| Field | Value | +|---|---| +| **Subject** | `Dealership Agreement ready for signature — {{applicationId}}` | +| **Trigger** | LOA is issued — Legal generates the dealership agreement; awaiting applicant + Royal Enfield authorised signatory's e-sign. | +| **Recipient** | Applicant | +| **Channels** | Email | +| **Placeholders** | `applicantName`, `applicationId`, `dealerCode`, `agreementLink`, `dueDate` | + +**Proposed body:** + +> Hi {{applicantName}}, +> +> Congratulations once again on your appointment as a Royal Enfield Dealer ({{dealerCode}}). +> +> The **Dealership Agreement** for application **{{applicationId}}** is now ready for your signature. Please review the document in full and e-sign it on the Dealer Portal by **{{dueDate}}**. +> +> [ **Review & Sign Agreement** ] +> +> Once your signature is recorded, the document is automatically routed to Royal Enfield's authorised signatory for counter-signature. + +--- + +## B.10 `ARCHITECTURAL_PLAN_REQUEST` + +| Field | Value | +|---|---| +| **Subject** | `Architectural Plan & Site Inputs Required — {{applicationId}}` | +| **Trigger** | DD-Admin assigns the Architecture team to a freshly issued LOI (Test Story 1.8.1). | +| **Recipient** | Applicant (cc: Architecture Lead) | +| **Channels** | Email | +| **Placeholders** | `applicantName`, `applicationId`, `architectName`, `inputsList`, `dueDate`, `uploadLink` | + +**Proposed body:** + +> Hi {{applicantName}}, +> +> Our Architecture team (led by **{{architectName}}**) is ready to begin the dealership site design for **{{applicationId}}**. +> +> To kick this off, please share the following on the portal by **{{dueDate}}**: +> +> {{inputsList}} +> +> Typical inputs needed: +> +> 1. Site dimensions and orientation drawings +> 2. Photographs of site (front, side, internal) and surroundings +> 3. Civic / municipal site approvals and zoning details +> 4. Electricity load sanction letter +> 5. Soil-test report (if available) +> 6. Proposed entry, parking, signage location preferences +> +> [ **Upload Site Inputs** ] +> +> Once these are received, the Architecture team will share a draft plan within 7 working days. + +--- + +# Appendix — Common Header / Footer / CTA Partials + +### Header Partial — `partials/email_header.html` + +Renders the black Royal Enfield banner with the centred logo. Used by every templated email. + +```html + + + + + + + +
+
+ Royal Enfield +
+
+``` + +### Footer Partial — `partials/email_footer.html` + +```html +
+ +
+ + +``` + +### Primary CTA Partial — `partials/primary_cta.html` + +Renders a red action button when a CTA URL is supplied. + +```html +{{#if ctaUrl}} + +{{/if}} +``` + +--- + +## Change Log + +| Date | Author | Description | +|---|---|---| +| 2026-05-11 | Engineering | Section B templates implemented in app; added `RE_Dealer_Email_Content_Client_Brief.md` for client copy prep. | diff --git a/src/constants/allowed-email-template-codes.ts b/src/constants/allowed-email-template-codes.ts index bbb9137..c8e312e 100644 --- a/src/constants/allowed-email-template-codes.ts +++ b/src/constants/allowed-email-template-codes.ts @@ -4,11 +4,17 @@ export const ALLOWED_EMAIL_TEMPLATE_CODES = [ 'APPLICANT_SHORTLISTED', 'APPLICANT_REJECTED', + 'ARCHITECTURAL_PLAN_REQUEST', 'CONSTITUTIONAL_CHANGE_SUBMITTED', + 'DEALERSHIP_AGREEMENT_SIGNATURE_REQUEST', 'CONSTITUTIONAL_CHANGE_APPROVED', 'CONSTITUTIONAL_CHANGE_UPDATE', 'DEALER_CODE_READY', + 'DOCUMENT_RECEIVED_ACKNOWLEDGEMENT', + 'DOCUMENT_REJECTED_RESUBMIT', + 'DOCUMENT_SUBMISSION_REMINDER', 'EOR_COMPLETED', + 'FDD_DOCUMENT_REQUEST', 'FNF_INITIATED', 'FNF_SUMMARY_PREPARED', 'FNF_SETTLEMENT_APPROVED', @@ -22,13 +28,16 @@ export const ALLOWED_EMAIL_TEMPLATE_CODES = [ 'INTERVIEW_CANCELLED_APPLICANT', 'INTERVIEW_CANCELLED_PANELIST', 'LOA_ISSUED', + 'LOI_ACKNOWLEDGEMENT_REQUEST', 'LOI_ISSUED', 'NON_OPPORTUNITY', 'ONBOARDING_PAYMENT_VERIFIED', 'ONBOARDING_STATUS_UPDATE', 'OPPORTUNITY', + 'PROSPECT_DOCUMENT_REQUEST', 'QUESTIONNAIRE_REMINDER', 'QUESTIONNAIRE_SUBMITTED', + 'SECURITY_DEPOSIT_REQUEST', 'RELOCATION_RECEIVED', 'RELOCATION_SUBMITTED', 'RELOCATION_APPROVED', @@ -38,6 +47,7 @@ export const ALLOWED_EMAIL_TEMPLATE_CODES = [ 'RESIGNATION_SUBMITTED', 'RESIGNATION_UPDATE', 'SLA_BREACH_WARNING', + 'STATUTORY_DOCUMENT_REQUEST', 'SLA_REMINDER', 'SLA_BREACH', 'SLA_ESCALATION', diff --git a/src/constants/onboarding-email-defaults.ts b/src/constants/onboarding-email-defaults.ts new file mode 100644 index 0000000..5fea3c1 --- /dev/null +++ b/src/constants/onboarding-email-defaults.ts @@ -0,0 +1,50 @@ +/** + * Default copy blocks for onboarding “planned” emails. + * Admins can override full templates in Master → Email Templates; these are fallbacks from code. + */ +export const DEFAULT_PROSPECT_DOCUMENT_CHECKLIST = [ + 'PAN (applicant / entity)', + 'Identity & address proof', + 'Address proof of proposed dealership site (lease / title)', + 'Net worth certificate (CA, last 2 years)', + 'Bank statements (last 6 months)', + 'Income tax returns (last 3 years)', + 'Photographs of the proposed dealership site' +].join('\n'); + +export const DEFAULT_STATUTORY_DOCUMENT_CHECKLIST = [ + 'GST registration certificate', + 'Trade licence / Shop & Establishment', + 'Fire safety NOC', + 'Pollution control board NOC (if applicable)', + 'Approved building plan from local authority', + 'Commercial electricity sanction', + 'Registered lease / sale deed for premises', + 'Latest property tax receipt', + 'Building & public liability insurance (as per policy)', + 'MOA/AOA or partnership deed (as applicable)', + 'Board resolution / authorised signatory list (if applicable)' +].join('\n'); + +export const DEFAULT_FDD_DOCUMENT_CHECKLIST = [ + 'Audited financial statements (last 3 financial years)', + 'GST returns (last 12 months)', + 'Bank statements (last 12 months)', + 'Proof of working capital', + 'Schedule of existing loans and obligations', + 'Director / partner net-worth statements (as applicable)' +].join('\n'); + +export const DEFAULT_ARCHITECTURE_SITE_INPUTS = [ + 'Site dimensions and orientation (sketch or CAD if available)', + 'Photographs: front, sides, internal, and surroundings', + 'Civic / municipal approvals and zoning classification', + 'Soil-test report (if available)', + 'Preferred entry, parking, and signage locations' +].join('\n'); + +export function formatDueDateDaysFromNow(days: number): string { + const d = new Date(); + d.setDate(d.getDate() + days); + return d.toLocaleDateString('en-IN', { dateStyle: 'medium' }); +} diff --git a/src/emailtemplates/architectural_plan_request.html b/src/emailtemplates/architectural_plan_request.html new file mode 100644 index 0000000..ca1ad64 --- /dev/null +++ b/src/emailtemplates/architectural_plan_request.html @@ -0,0 +1,13 @@ +{{> email_header}} +

Hi {{applicantName}},

+

Our architecture team (coordinated by {{architectName}}) will begin the dealership layout and site design work for application {{applicationId}}.

+

Please provide the site inputs listed on the Dealer Portal by {{dueDate}}.

+{{#if inputsList}} +
+

{{inputsList}}

+
+{{/if}} +{{> primary_cta}} +

Timely submission helps us meet your showroom and workshop readiness timelines.

+

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/emailtemplates/dealership_agreement_signature_request.html b/src/emailtemplates/dealership_agreement_signature_request.html new file mode 100644 index 0000000..60297e8 --- /dev/null +++ b/src/emailtemplates/dealership_agreement_signature_request.html @@ -0,0 +1,8 @@ +{{> email_header}} +

Congratulations {{applicantName}},

+

Your Letter of Appointment is in place for application {{applicationId}}{{#if dealerCode}} (Dealer code: {{dealerCode}}){{/if}}.

+

The Dealership Agreement is now ready for your review and e-signature on the Dealer Portal. Please complete your signature by {{dueDate}}.

+{{> primary_cta}} +

Once you sign, the agreement will be routed for Royal Enfield’s counter-signature as per process.

+

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/emailtemplates/document_received_acknowledgement.html b/src/emailtemplates/document_received_acknowledgement.html new file mode 100644 index 0000000..247bb32 --- /dev/null +++ b/src/emailtemplates/document_received_acknowledgement.html @@ -0,0 +1,10 @@ +{{> email_header}} +

Hi {{applicantName}},

+

Thank you. We have received your upload for application {{applicationId}}.

+

Category: {{documentCategory}}
+Document type: {{documentName}}
+Received on: {{receivedOn}}

+

Our team will verify the submission. You will receive a separate notification if anything needs to be corrected or re-uploaded.

+{{> primary_cta}} +

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/emailtemplates/document_rejected_resubmit.html b/src/emailtemplates/document_rejected_resubmit.html new file mode 100644 index 0000000..fd6741d --- /dev/null +++ b/src/emailtemplates/document_rejected_resubmit.html @@ -0,0 +1,12 @@ +{{> email_header}} +

Hi {{applicantName}},

+

One or more documents for application {{applicationId}} could not be accepted in their current form.

+
+

Document: {{documentName}}

+

Reason: {{rejectionReason}}

+
+

Please re-upload the corrected document on the Dealer Portal by {{dueDate}}.

+{{> primary_cta}} +

For clarification, contact your assigned Dealer Development representative.

+

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/emailtemplates/document_submission_reminder.html b/src/emailtemplates/document_submission_reminder.html new file mode 100644 index 0000000..299dbc6 --- /dev/null +++ b/src/emailtemplates/document_submission_reminder.html @@ -0,0 +1,12 @@ +{{> email_header}} +

Hi {{applicantName}},

+

This is a reminder regarding your dealership application {{applicationId}}.

+

The following items are still pending on the portal:

+
+

{{pendingDocuments}}

+
+

Please complete your uploads by {{dueDate}} so we can continue processing without delay.

+{{> primary_cta}} +

If you have already submitted everything, you may ignore this reminder.

+

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/emailtemplates/fdd_document_request.html b/src/emailtemplates/fdd_document_request.html new file mode 100644 index 0000000..28b1b1e --- /dev/null +++ b/src/emailtemplates/fdd_document_request.html @@ -0,0 +1,15 @@ +{{> email_header}} +

Hi {{applicantName}},

+

As part of Financial Due Diligence (FDD) for your dealership application {{applicationId}}, we have engaged {{fddPartnerName}} to complete the assessment.

+

Please upload the documents requested on the Dealer Portal by {{dueDate}}.

+{{#if documentChecklist}} +
+

{{documentChecklist}}

+
+{{else}} +

Typical requirements include audited financials, GST returns, bank statements, working-capital proof, and details of existing obligations—refer to the portal for your exact checklist.

+{{/if}} +{{> primary_cta}} +

The FDD partner may contact you directly for clarifications. Incomplete disclosure may affect the outcome of your application.

+

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/emailtemplates/loi_acknowledgement_request.html b/src/emailtemplates/loi_acknowledgement_request.html new file mode 100644 index 0000000..78fe9b4 --- /dev/null +++ b/src/emailtemplates/loi_acknowledgement_request.html @@ -0,0 +1,7 @@ +{{> email_header}} +

Hi {{applicantName}},

+

Your Letter of Intent (LOI) for application {{applicationId}} is awaiting your acknowledgement on the Dealer Portal.

+

Please review the document and complete your acknowledgement by {{dueDate}}. We cannot move to the next onboarding steps until this is done.

+{{> primary_cta}} +

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/emailtemplates/prospect_document_request.html b/src/emailtemplates/prospect_document_request.html new file mode 100644 index 0000000..92dc154 --- /dev/null +++ b/src/emailtemplates/prospect_document_request.html @@ -0,0 +1,15 @@ +{{> email_header}} +

Hi {{applicantName}},

+

Congratulations on being shortlisted for the Royal Enfield dealership opportunity at {{location}} (Application ID: {{applicationId}}).

+

To proceed with your evaluation, please upload the required documents on the Dealer Portal by {{dueDate}}.

+{{#if documentList}} +
+

Document checklist:

+

{{documentList}}

+
+{{/if}} +

All documents should be clear, in PDF or JPG format, and within the size limits shown on the portal.

+{{> primary_cta}} +

For queries, contact your Royal Enfield Dealer Development representative or write to dealer-support@royalenfield.com.

+

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/emailtemplates/security_deposit_request.html b/src/emailtemplates/security_deposit_request.html new file mode 100644 index 0000000..c1dce9e --- /dev/null +++ b/src/emailtemplates/security_deposit_request.html @@ -0,0 +1,14 @@ +{{> email_header}} +

Hi {{applicantName}},

+

Following acknowledgement of your Letter of Intent for application {{applicationId}}, the next step is to remit the security deposit of ₹{{amount}} before {{dueDate}}.

+

You may pay online through the portal or transfer funds as per the instructions shown after you log in.

+{{#if bankDetails}} +
+

Bank transfer details (if applicable):

+

{{bankDetails}}

+
+{{/if}} +{{> primary_cta}} +

After payment, please upload the payment confirmation (UTR / reference) on the portal. Finance will verify and you will receive a confirmation notification.

+

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/emailtemplates/statutory_document_request.html b/src/emailtemplates/statutory_document_request.html new file mode 100644 index 0000000..18891c6 --- /dev/null +++ b/src/emailtemplates/statutory_document_request.html @@ -0,0 +1,13 @@ +{{> email_header}} +

Hi {{applicantName}},

+

Your Letter of Intent (LOI) has been issued for application {{applicationId}}. The next step is to submit statutory and compliance documents required for dealership authorisation.

+

Please upload the documents listed on the Dealer Portal by {{dueDate}}.

+{{#if statutoryList}} +
+

{{statutoryList}}

+
+{{/if}} +

Our DD-Admin and Legal teams will verify each submission. You will be notified if any document needs to be re-submitted.

+{{> primary_cta}} +

Regards,
Royal Enfield Dealer Development Team

+{{> email_footer}} diff --git a/src/modules/fdd/fdd.controller.ts b/src/modules/fdd/fdd.controller.ts index c5cba6c..90fe9e1 100644 --- a/src/modules/fdd/fdd.controller.ts +++ b/src/modules/fdd/fdd.controller.ts @@ -7,6 +7,7 @@ import { AUDIT_ACTIONS, APPLICATION_STATUS, APPLICATION_STAGES, ROLES } from '.. import { WorkflowService } from '../../services/WorkflowService.js'; import { pickApplicationAuditContext, safeAuditLogCreate } from '../../services/applicationAuditLog.service.js'; import { NotificationService } from '../../services/NotificationService.js'; +import { formatDueDateDaysFromNow, DEFAULT_FDD_DOCUMENT_CHECKLIST } from '../../constants/onboarding-email-defaults.js'; export const getAssignment = async (req: Request, res: Response) => { try { @@ -99,8 +100,11 @@ export const assignAgency = async (req: AuthRequest, res: Response) => { channels: phone ? ['system', 'email', 'whatsapp'] : ['system', 'email'], templateCode: 'USER_ASSIGNED', placeholders: { + userName: fddUser.fullName || 'Team', applicantName: application.applicantName || '', applicationId: application.applicationId, + dealerName: application.applicantName || '', + participantType: 'FDD Partner', link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`, ctaLabel: 'View Assignment', phone: phone || '' @@ -108,6 +112,30 @@ export const assignAgency = async (req: AuthRequest, res: Response) => { }).catch((e: any) => console.error('[FDD] Agency notify failed:', e)); } + const portalBaseFdd = process.env.FRONTEND_URL || 'http://localhost:5173'; + if (application.email) { + const applicantAcct = await db.User.findOne({ + where: { email: application.email }, + attributes: ['id', 'mobileNumber'] + }); + const fddPartnerName = fddUser ? (fddUser as any).fullName || 'Assigned FDD partner' : 'Royal Enfield FDD team'; + await NotificationService.notify(applicantAcct?.id ?? null, application.email, { + title: `FDD documents required — ${application.applicationId}`, + message: 'Please upload the financial documents requested for due diligence.', + channels: ['email'], + templateCode: 'FDD_DOCUMENT_REQUEST', + placeholders: { + applicantName: application.applicantName || 'Applicant', + applicationId: application.applicationId, + fddPartnerName, + dueDate: formatDueDateDaysFromNow(14), + documentChecklist: DEFAULT_FDD_DOCUMENT_CHECKLIST, + link: `${portalBaseFdd}/applications/${application.id}`, + ctaLabel: 'Upload FDD documents' + } + }).catch((e: any) => console.error('[FDD] Applicant document request email failed:', e)); + } + res.status(201).json({ success: true, message: 'FDD Agency assigned', data: assignment }); } catch (error) { console.error('Assign FDD agency error:', error); diff --git a/src/modules/loa/loa.controller.ts b/src/modules/loa/loa.controller.ts index a074b5a..342156e 100644 --- a/src/modules/loa/loa.controller.ts +++ b/src/modules/loa/loa.controller.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; -const { LoaRequest, LoaApproval, LoaDocumentGenerated, SecurityDeposit, AuditLog, StageApprovalPolicy, StageApprovalAction, User } = db; +const { LoaRequest, LoaApproval, LoaDocumentGenerated, SecurityDeposit, AuditLog, StageApprovalPolicy, StageApprovalAction, User, DealerCode } = db; +import { formatDueDateDaysFromNow } from '../../constants/onboarding-email-defaults.js'; import { AuthRequest } from '../../types/express.types.js'; import { AUDIT_ACTIONS, APPLICATION_STATUS, APPLICATION_STAGES, ROLES } from '../../common/config/constants.js'; import { WorkflowService } from '../../services/WorkflowService.js'; @@ -249,6 +250,47 @@ export const approveRequest = async (req: AuthRequest, res: Response) => { } } + const appFull = await db.Application.findByPk(request.applicationId, { + include: [{ model: DealerCode, as: 'dealerCode', required: false }] + }); + if (appFull?.email) { + const portalBaseLoa = process.env.FRONTEND_URL || 'http://localhost:5173'; + const dc = (appFull as any).dealerCode; + const dealerCodeStr = dc?.salesCode || dc?.serviceCode || 'Available on portal after SAP sync'; + const applicantAcct = await User.findOne({ + where: { email: appFull.email }, + attributes: ['id', 'mobileNumber'] + }); + await NotificationService.notify(applicantAcct?.id ?? null, appFull.email, { + title: `LOA issued — ${appFull.applicationId}`, + message: 'Your Letter of Appointment is ready. View it on the Dealer Portal.', + channels: ['email'], + templateCode: 'LOA_ISSUED', + placeholders: { + applicantName: appFull.applicantName || '', + applicationId: appFull.applicationId, + dealerCode: dealerCodeStr, + link: `${portalBaseLoa}/applications/${appFull.id}`, + ctaLabel: 'View LOA' + } + }).catch((e: any) => console.error('[LOA] Applicant LOA email failed:', e)); + + await NotificationService.notify(applicantAcct?.id ?? null, appFull.email, { + title: `Sign dealership agreement — ${appFull.applicationId}`, + message: 'Please review and e-sign your dealership agreement on the portal.', + channels: ['email'], + templateCode: 'DEALERSHIP_AGREEMENT_SIGNATURE_REQUEST', + placeholders: { + applicantName: appFull.applicantName || '', + applicationId: appFull.applicationId, + dealerCode: dealerCodeStr, + dueDate: formatDueDateDaysFromNow(14), + link: `${portalBaseLoa}/applications/${appFull.id}`, + ctaLabel: 'Review & sign agreement' + } + }).catch((e: any) => console.error('[LOA] Agreement signature email failed:', e)); + } + res.json({ success: true, message: 'LOA fully approved and issued' }); } else { // SEQUENTIAL APPROVAL BRIDGE: diff --git a/src/modules/loi/loi.controller.ts b/src/modules/loi/loi.controller.ts index 541e5c0..ec1e35e 100644 --- a/src/modules/loi/loi.controller.ts +++ b/src/modules/loi/loi.controller.ts @@ -1,11 +1,12 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; -const { LoiRequest, LoiApproval, LoiDocumentGenerated, LoiAcknowledgement, AuditLog, StageApprovalPolicy, StageApprovalAction, User } = db; +const { LoiRequest, LoiApproval, LoiDocumentGenerated, LoiAcknowledgement, AuditLog, StageApprovalPolicy, StageApprovalAction, User, SecurityDeposit } = db; import { AuthRequest } from '../../types/express.types.js'; import { AUDIT_ACTIONS, APPLICATION_STATUS, ROLES } from '../../common/config/constants.js'; import { WorkflowService } from '../../services/WorkflowService.js'; import { NotificationService } from '../../services/NotificationService.js'; import { sendEmail } from '../../common/utils/email.service.js'; +import { formatDueDateDaysFromNow, DEFAULT_STATUTORY_DOCUMENT_CHECKLIST } from '../../constants/onboarding-email-defaults.js'; const LOI_STAGE_CODE = 'LOI_APPROVAL'; @@ -70,12 +71,22 @@ export const acknowledgeRequest = async (req: AuthRequest, res: Response) => { const request = await LoiRequest.findByPk(requestId); if (!request) return res.status(404).json({ success: false, message: 'LOI Request not found' }); + const latestGen = await LoiDocumentGenerated.findOne({ + where: { requestId: request.id }, + order: [['generatedAt', 'DESC']] + }); + const loiDocId = (documentId as string) || latestGen?.id; + if (!loiDocId || !req.user?.id) { + return res.status(400).json({ + success: false, + message: 'LOI document reference and a signed-in applicant are required to acknowledge the LOI.' + }); + } + await LoiAcknowledgement.create({ - requestId, - applicationId: request.applicationId, - documentId, - acknowledgedAt: new Date(), - status: 'Acknowledged' + loiDocId, + applicantId: req.user.id, + remarks: null }); const application = await db.Application.findByPk(request.applicationId); @@ -84,6 +95,37 @@ export const acknowledgeRequest = async (req: AuthRequest, res: Response) => { reason: 'LOI Acknowledged by applicant', progressPercentage: 90 }); + + const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173'; + const deposit = + (await SecurityDeposit.findOne({ + where: { applicationId: application.id, depositType: 'SECURITY_DEPOSIT' } + })) || (await SecurityDeposit.findOne({ where: { applicationId: application.id } })); + const amountNum = deposit?.amount != null ? Number(deposit.amount) : 200000; + const amountStr = Number.isFinite(amountNum) ? amountNum.toLocaleString('en-IN') : '200,000'; + + const applicantUser = await User.findOne({ + where: { email: application.email }, + attributes: ['id', 'mobileNumber'] + }); + const phone = (applicantUser as any)?.mobileNumber || application.phone || ''; + + await NotificationService.notify(applicantUser?.id ?? null, application.email, { + title: `Security deposit — ${application.applicationId}`, + message: `Please remit the security deposit for ${application.applicationId} as per your LOI.`, + channels: phone ? ['email', 'whatsapp'] : ['email'], + templateCode: 'SECURITY_DEPOSIT_REQUEST', + placeholders: { + applicantName: application.applicantName || 'Applicant', + applicationId: application.applicationId, + amount: amountStr, + dueDate: formatDueDateDaysFromNow(14), + bankDetails: 'Use the payment instructions shown on the Dealer Portal after you sign in.', + link: `${portalBase}/applications/${application.id}`, + ctaLabel: 'Pay or upload proof', + phone: String(phone || '') + } + }).catch((e: any) => console.error('[LOI] Security deposit email failed:', e)); } res.json({ success: true, message: 'LOI Acknowledged by applicant' }); @@ -405,11 +447,13 @@ export const generateDocument = async (req: AuthRequest, res: Response) => { docId = docRecord.id; } - const doc = docId ? await LoiDocumentGenerated.create({ - requestId, - documentId: docId, - version: '1.0' - }) : null; + const doc = docId + ? await LoiDocumentGenerated.create({ + requestId: reqRecord ? reqRecord.id : requestId, + documentId: docId, + version: '1.0' + }) + : null; // Bridge: Transition from LOI Issued -> Dealer Code Generation const request = await LoiRequest.findByPk(requestId); @@ -455,6 +499,24 @@ export const generateDocument = async (req: AuthRequest, res: Response) => { }).catch((e: any) => console.error('[LOI] stakeholder notify failed:', e)); } } + + if (application.email) { + const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173'; + await NotificationService.notify(null, application.email, { + title: `Statutory documents — ${application.applicationId}`, + message: 'Please upload statutory and compliance documents on the Dealer Portal.', + channels: ['email'], + templateCode: 'STATUTORY_DOCUMENT_REQUEST', + placeholders: { + applicantName: application.applicantName || 'Applicant', + applicationId: application.applicationId, + dueDate: formatDueDateDaysFromNow(21), + statutoryList: DEFAULT_STATUTORY_DOCUMENT_CHECKLIST, + link: `${portalBase}/applications/${application.id}`, + ctaLabel: 'Upload statutory documents' + } + }).catch((e: any) => console.error('[LOI] Statutory document email failed:', e)); + } } } diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index ef868a0..fa05c06 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -14,6 +14,11 @@ import { NomenclatureService } from '../../common/utils/nomenclature.js'; import { pickApplicationAuditContext, safeAuditLogCreate } from '../../services/applicationAuditLog.service.js'; import { isAutoAssignmentEnabled } from '../../services/AutoAssignmentConfigService.js'; import { NotificationService } from '../../services/NotificationService.js'; +import { + formatDueDateDaysFromNow, + DEFAULT_PROSPECT_DOCUMENT_CHECKLIST, + DEFAULT_ARCHITECTURE_SITE_INPUTS +} from '../../constants/onboarding-email-defaults.js'; const { DocumentStageConfig } = db; @@ -716,6 +721,25 @@ export const uploadDocuments = async (req: any, res: Response) => { }, }); + if (application.email) { + const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173'; + await NotificationService.notify(null, application.email, { + title: `Document received — ${application.applicationId}`, + message: `We received your upload: ${documentType}.`, + channels: ['email'], + templateCode: 'DOCUMENT_RECEIVED_ACKNOWLEDGEMENT', + placeholders: { + applicantName: application.applicantName || 'Applicant', + applicationId: application.applicationId, + documentCategory: stage || 'Onboarding', + documentName: documentType, + receivedOn: new Date().toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }), + link: `${portalBase}/applications/${application.id}`, + ctaLabel: 'View uploads' + } + }).catch((mailErr: any) => console.error('[uploadDocuments] acknowledgement email:', mailErr)); + } + res.status(201).json({ success: true, message: 'Document uploaded successfully', @@ -822,6 +846,29 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => { application.applicationId ).catch(err => console.error('Failed to send shortlist email:', err)); + const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173'; + const applicantUser = await User.findOne({ + where: { email: application.email }, + attributes: ['id', 'mobileNumber'] + }); + const prospectPhone = (applicantUser as any)?.mobileNumber || application.phone || ''; + await NotificationService.notify(applicantUser?.id ?? null, application.email, { + title: `Documents required — ${application.applicationId}`, + message: 'Please upload the requested documents to continue your dealership evaluation.', + channels: prospectPhone ? ['email', 'whatsapp'] : ['email'], + templateCode: 'PROSPECT_DOCUMENT_REQUEST', + placeholders: { + applicantName: application.applicantName || 'Applicant', + location: application.preferredLocation || application.city || 'your proposed location', + applicationId: application.applicationId, + dueDate: formatDueDateDaysFromNow(7), + documentList: DEFAULT_PROSPECT_DOCUMENT_CHECKLIST, + link: `${portalBase}/applications/${application.id}`, + ctaLabel: 'Upload documents', + phone: String(prospectPhone || '') + } + }).catch((err: any) => console.error('Failed to send prospect document request email:', err)); + // Add manual assignees as participants if provided if (assignedToArr.length > 0) { for (const userId of assignedToArr) { @@ -1101,6 +1148,26 @@ export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => defaults: { joinedMethod: 'auto' } }); + const architect = await User.findByPk(targetUserId, { attributes: ['fullName'] }); + const portalBaseArch = process.env.FRONTEND_URL || 'http://localhost:5173'; + if (application.email) { + await NotificationService.notify(null, application.email, { + title: `Architecture & site inputs — ${application.applicationId}`, + message: 'Please share site inputs for your dealership layout.', + channels: ['email'], + templateCode: 'ARCHITECTURAL_PLAN_REQUEST', + placeholders: { + applicantName: application.applicantName || 'Applicant', + applicationId: application.applicationId, + architectName: architect?.fullName || 'Royal Enfield Architecture', + dueDate: formatDueDateDaysFromNow(10), + inputsList: DEFAULT_ARCHITECTURE_SITE_INPUTS, + link: `${portalBaseArch}/applications/${application.id}`, + ctaLabel: 'Provide site inputs' + } + }).catch((e: any) => console.error('[Architecture] Applicant email failed:', e)); + } + res.json({ success: true, message: 'Architecture team assigned successfully' }); } catch (error) { console.error('Assign architecture team error:', error); @@ -1682,3 +1749,154 @@ export const sendBulkReminders = async (req: AuthRequest, res: Response) => { res.status(500).json({ success: false, message: 'Error sending reminders' }); } }; + +export const sendBulkDocumentReminders = async (req: AuthRequest, res: Response) => { + try { + const { applicationIds, pendingDocuments, dueDate } = req.body; + if (!applicationIds || !Array.isArray(applicationIds) || applicationIds.length === 0) { + return res.status(400).json({ success: false, message: 'applicationIds array is required' }); + } + + const applications = await Application.findAll({ + where: { id: { [Op.in]: applicationIds } } + }); + + for (const app of applications) { + await NotificationService.sendDocumentSubmissionReminder(app.email, app.phone, app.applicantName, { + applicationId: app.applicationId, + pendingDocuments: pendingDocuments || undefined, + dueDate: dueDate || formatDueDateDaysFromNow(7), + link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${app.id}` + }); + await safeAuditLogCreate({ + userId: req.user?.id || null, + action: 'REMINDER_SENT', + entityType: 'application', + entityId: app.id, + newData: { + template: 'DOCUMENT_SUBMISSION_REMINDER', + sentAt: new Date(), + context: pickApplicationAuditContext(app) + } + }); + } + + res.json({ success: true, message: `Document reminders sent to ${applications.length} applicant(s)` }); + } catch (error) { + console.error('Send bulk document reminders error:', error); + res.status(500).json({ success: false, message: 'Error sending document reminders' }); + } +}; + +export const sendBulkLoiAckReminders = async (req: AuthRequest, res: Response) => { + try { + const { applicationIds, dueDate } = req.body; + if (!applicationIds || !Array.isArray(applicationIds) || applicationIds.length === 0) { + return res.status(400).json({ success: false, message: 'applicationIds array is required' }); + } + + const applications = await Application.findAll({ + where: { id: { [Op.in]: applicationIds } } + }); + + let sent = 0; + for (const app of applications) { + const lr = await db.LoiRequest.findOne({ + where: { applicationId: app.id, status: 'Approved' } + }); + if (!lr) continue; + + const latestGen = await db.LoiDocumentGenerated.findOne({ + where: { requestId: lr.id }, + order: [['generatedAt', 'DESC']] + }); + if (!latestGen) continue; + + const ackCount = await db.LoiAcknowledgement.count({ where: { loiDocId: latestGen.id } }); + if (ackCount > 0) continue; + + await NotificationService.sendLoiAcknowledgementReminder(app.email, app.phone, app.applicantName, { + applicationId: app.applicationId, + dueDate: dueDate || formatDueDateDaysFromNow(7), + link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/prospect-login` + }); + sent++; + } + + res.json({ success: true, message: `LOI acknowledgement reminders sent: ${sent} of ${applications.length} selected` }); + } catch (error) { + console.error('Send bulk LOI ack reminders error:', error); + res.status(500).json({ success: false, message: 'Error sending LOI acknowledgement reminders' }); + } +}; + +export const rejectOnboardingDocument = async (req: AuthRequest, res: Response) => { + try { + const allowed: string[] = [ROLES.DD_ADMIN, ROLES.DD_LEAD, ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN]; + if (!req.user?.roleCode || !allowed.includes(req.user.roleCode)) { + return res.status(403).json({ success: false, message: 'Forbidden: insufficient role to reject documents' }); + } + + const { id, documentId } = req.params; + const { rejectionReason, dueDate } = req.body as { rejectionReason?: string; dueDate?: string }; + if (!rejectionReason || String(rejectionReason).trim().length === 0) { + return res.status(400).json({ success: false, message: 'rejectionReason is required' }); + } + + const targetId = id as string; + const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId); + const application = await Application.findOne({ + where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId } + }); + if (!application) { + return res.status(404).json({ success: false, message: 'Application not found' }); + } + + const doc = await OnboardingDocument.findOne({ + where: { id: documentId, applicationId: application.id } + }); + if (!doc) { + return res.status(404).json({ success: false, message: 'Document not found' }); + } + + await doc.update({ status: 'rejected' }); + + if (application.email) { + const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173'; + await NotificationService.notify(null, application.email, { + title: `Document re-upload required — ${application.applicationId}`, + message: `A document was not accepted: ${doc.documentType}.`, + channels: application.phone ? ['email', 'whatsapp'] : ['email'], + templateCode: 'DOCUMENT_REJECTED_RESUBMIT', + placeholders: { + applicantName: application.applicantName || 'Applicant', + applicationId: application.applicationId, + documentName: doc.documentType, + rejectionReason: String(rejectionReason).trim(), + dueDate: dueDate || formatDueDateDaysFromNow(7), + link: `${portalBase}/applications/${application.id}`, + ctaLabel: 'Re-upload document', + phone: application.phone || '' + } + }).catch((e: any) => console.error('[rejectOnboardingDocument] email failed:', e)); + } + + await safeAuditLogCreate({ + userId: req.user?.id || null, + action: AUDIT_ACTIONS.DOCUMENT_REJECTED, + entityType: 'application', + entityId: application.id, + newData: { + documentId: doc.id, + documentRejected: true, + reason: rejectionReason, + context: pickApplicationAuditContext(application) + } + }); + + res.json({ success: true, message: 'Document marked as rejected and applicant notified' }); + } catch (error) { + console.error('Reject onboarding document error:', error); + res.status(500).json({ success: false, message: 'Error rejecting document' }); + } +}; diff --git a/src/modules/onboarding/onboarding.routes.ts b/src/modules/onboarding/onboarding.routes.ts index 45d2c4f..5961a5b 100644 --- a/src/modules/onboarding/onboarding.routes.ts +++ b/src/modules/onboarding/onboarding.routes.ts @@ -6,7 +6,8 @@ import { assignArchitectureTeam, updateArchitectureStatus, generateDealerCodes, retriggerEvaluators, getDocumentConfigs, getDocumentConfigMetadata, createDocumentConfig, updateDocumentConfig, deleteDocumentConfig, updateApplication, - exportApplicationResponses, convertToOpportunity, bulkConvertToOpportunity, sendBulkReminders + exportApplicationResponses, convertToOpportunity, bulkConvertToOpportunity, sendBulkReminders, + sendBulkDocumentReminders, sendBulkLoiAckReminders, rejectOnboardingDocument } from './onboarding.controller.js'; import { authenticate } from '../../common/middleware/auth.js'; import { checkRevocation } from '../../common/middleware/checkRevocation.js'; @@ -30,10 +31,13 @@ router.get('/document-configs/metadata', getDocumentConfigMetadata); router.get('/document-configs', getDocumentConfigs); router.post('/applications/shortlist', bulkShortlist); // Existing route, updated to named import router.post('/applications/reminders', sendBulkReminders); +router.post('/applications/document-reminders', sendBulkDocumentReminders); +router.post('/applications/loi-ack-reminders', sendBulkLoiAckReminders); router.get('/applications/:id', checkRevocation as any, getApplicationById); router.put('/applications/:id', checkRevocation as any, updateApplication); router.put('/applications/:id/status', checkRevocation as any, updateApplicationStatus); router.post('/applications/:id/documents', uploadSingle, checkRevocation as any, uploadDocuments); +router.post('/applications/:id/documents/:documentId/reject', checkRevocation as any, rejectOnboardingDocument); router.get('/applications/:id/documents', checkRevocation as any, getApplicationDocuments); router.post('/applications/bulk-convert-to-opportunity', bulkConvertToOpportunity); router.post('/applications/:id/convert-to-opportunity', checkRevocation as any, convertToOpportunity); diff --git a/src/scripts/seed-master-emails.ts b/src/scripts/seed-master-emails.ts index c1aaeef..8fb1339 100644 --- a/src/scripts/seed-master-emails.ts +++ b/src/scripts/seed-master-emails.ts @@ -303,6 +303,76 @@ const seedTemplates = async () => { subject: 'Update on Your Request — {{requestId}}', fileName: 'workflow_status_update_dealer.html', placeholders: ['dealerName', 'requestId', 'targetStage', 'remarks', 'link', 'ctaLabel'] + }, + { + templateCode: 'PROSPECT_DOCUMENT_REQUEST', + description: 'Post-shortlist: request KYC and business documents from the applicant', + subject: 'Documents Required — Royal Enfield Dealership Application {{applicationId}}', + fileName: 'prospect_document_request.html', + placeholders: ['applicantName', 'location', 'applicationId', 'dueDate', 'documentList', 'link', 'ctaLabel'] + }, + { + templateCode: 'STATUTORY_DOCUMENT_REQUEST', + description: 'After LOI: request statutory and compliance documents for authorisation', + subject: 'Statutory Documents Required — {{applicationId}}', + fileName: 'statutory_document_request.html', + placeholders: ['applicantName', 'applicationId', 'dueDate', 'statutoryList', 'link', 'ctaLabel'] + }, + { + templateCode: 'DOCUMENT_SUBMISSION_REMINDER', + description: 'Reminder for pending document uploads (manual / bulk send)', + subject: 'Reminder: Pending documents for {{applicationId}}', + fileName: 'document_submission_reminder.html', + placeholders: ['applicantName', 'applicationId', 'pendingDocuments', 'dueDate', 'link', 'ctaLabel'] + }, + { + templateCode: 'DOCUMENT_RECEIVED_ACKNOWLEDGEMENT', + description: 'Acknowledgement when applicant uploads a document', + subject: 'Documents received — {{applicationId}}', + fileName: 'document_received_acknowledgement.html', + placeholders: ['applicantName', 'applicationId', 'documentCategory', 'documentName', 'receivedOn', 'link', 'ctaLabel'] + }, + { + templateCode: 'DOCUMENT_REJECTED_RESUBMIT', + description: 'When DD-Admin / Legal rejects an onboarding document', + subject: 'Action Required: Re-submit documents for {{applicationId}}', + fileName: 'document_rejected_resubmit.html', + placeholders: ['applicantName', 'applicationId', 'documentName', 'rejectionReason', 'dueDate', 'link', 'ctaLabel'] + }, + { + templateCode: 'FDD_DOCUMENT_REQUEST', + description: 'Applicant checklist when FDD agency is assigned', + subject: 'FDD Documents Required — {{applicationId}}', + fileName: 'fdd_document_request.html', + placeholders: ['applicantName', 'applicationId', 'fddPartnerName', 'dueDate', 'documentChecklist', 'link', 'ctaLabel'] + }, + { + templateCode: 'LOI_ACKNOWLEDGEMENT_REQUEST', + description: 'Chase applicant to acknowledge LOI on the portal', + subject: 'Action Required: Acknowledge your Letter of Intent — {{applicationId}}', + fileName: 'loi_acknowledgement_request.html', + placeholders: ['applicantName', 'applicationId', 'dueDate', 'link', 'ctaLabel'] + }, + { + templateCode: 'SECURITY_DEPOSIT_REQUEST', + description: 'After LOI acknowledgement: security deposit payment instructions', + subject: 'Security Deposit Payment Required — {{applicationId}}', + fileName: 'security_deposit_request.html', + placeholders: ['applicantName', 'applicationId', 'amount', 'dueDate', 'bankDetails', 'link', 'ctaLabel'] + }, + { + templateCode: 'DEALERSHIP_AGREEMENT_SIGNATURE_REQUEST', + description: 'After LOA: request e-signature on dealership agreement', + subject: 'Dealership Agreement ready for signature — {{applicationId}}', + fileName: 'dealership_agreement_signature_request.html', + placeholders: ['applicantName', 'applicationId', 'dealerCode', 'dueDate', 'link', 'ctaLabel'] + }, + { + templateCode: 'ARCHITECTURAL_PLAN_REQUEST', + description: 'When architecture lead is assigned: site inputs from applicant', + subject: 'Architectural Plan & Site Inputs Required — {{applicationId}}', + fileName: 'architectural_plan_request.html', + placeholders: ['applicantName', 'applicationId', 'architectName', 'dueDate', 'inputsList', 'link', 'ctaLabel'] } ]; diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index dc4102b..34b2395 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -162,4 +162,63 @@ export class NotificationService { } }); } + + /** + * Reminder for pending onboarding document uploads (bulk or scheduled send). + */ + static async sendDocumentSubmissionReminder( + email: string, + phone: string | null | undefined, + applicantName: string, + options: { applicationId: string; pendingDocuments?: string; dueDate?: string; link?: string } + ) { + const base = process.env.FRONTEND_URL || 'http://localhost:5173'; + const channels: ('email' | 'whatsapp')[] = ['email']; + if (phone) channels.push('whatsapp'); + await this.notify(null, email, { + title: `Reminder: Pending documents — ${options.applicationId}`, + message: `Hi ${applicantName}, please upload pending documents for ${options.applicationId}.`, + channels, + templateCode: 'DOCUMENT_SUBMISSION_REMINDER', + placeholders: { + applicantName, + applicationId: options.applicationId, + pendingDocuments: + options.pendingDocuments || + 'Please sign in to the Dealer Portal to view your personalised document checklist.', + dueDate: options.dueDate || 'within seven calendar days', + link: options.link || `${base}/applications`, + ctaLabel: 'Upload documents', + phone: phone || '' + } + }); + } + + /** + * Chase applicant to acknowledge issued LOI (bulk send from DD-Admin). + */ + static async sendLoiAcknowledgementReminder( + email: string, + phone: string | null | undefined, + applicantName: string, + options: { applicationId: string; link?: string; dueDate?: string } + ) { + const base = process.env.FRONTEND_URL || 'http://localhost:5173'; + const channels: ('email' | 'whatsapp')[] = ['email']; + if (phone) channels.push('whatsapp'); + await this.notify(null, email, { + title: `Acknowledge your LOI — ${options.applicationId}`, + message: `Hi ${applicantName}, your Letter of Intent is awaiting acknowledgement on the portal.`, + channels, + templateCode: 'LOI_ACKNOWLEDGEMENT_REQUEST', + placeholders: { + applicantName, + applicationId: options.applicationId, + dueDate: options.dueDate || 'within seven calendar days', + link: options.link || `${base}/prospect-login`, + ctaLabel: 'Acknowledge LOI', + phone: phone || '' + } + }); + } }