new mail templates added for edge scenerios
This commit is contained in:
parent
0ab90ee356
commit
eeae163782
118
docs/RE_Dealer_Email_Content_Client_Brief.md
Normal file
118
docs/RE_Dealer_Email_Content_Client_Brief.md
Normal file
@ -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": ["<uuid>", ...], "pendingDocuments": "<optional paragraph>", "dueDate": "<optional display string>" }` |
|
||||
| Bulk LOI acknowledgement reminder | `POST /api/onboarding/applications/loi-ack-reminders` | `{ "applicationIds": ["<uuid>", ...], "dueDate": "<optional>" }` — 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": "<required>", "dueDate": "<optional>" }` — 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.
|
||||
1761
docs/RE_Dealer_Email_Templates.md
Normal file
1761
docs/RE_Dealer_Email_Templates.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
|
||||
50
src/constants/onboarding-email-defaults.ts
Normal file
50
src/constants/onboarding-email-defaults.ts
Normal file
@ -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' });
|
||||
}
|
||||
13
src/emailtemplates/architectural_plan_request.html
Normal file
13
src/emailtemplates/architectural_plan_request.html
Normal file
@ -0,0 +1,13 @@
|
||||
{{> email_header}}
|
||||
<h2>Hi {{applicantName}},</h2>
|
||||
<p>Our architecture team (coordinated by <strong>{{architectName}}</strong>) will begin the dealership layout and site design work for application <strong>{{applicationId}}</strong>.</p>
|
||||
<p>Please provide the site inputs listed on the Dealer Portal by <strong>{{dueDate}}</strong>.</p>
|
||||
{{#if inputsList}}
|
||||
<div class="details">
|
||||
<p style="white-space:pre-line;">{{inputsList}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{> primary_cta}}
|
||||
<p>Timely submission helps us meet your showroom and workshop readiness timelines.</p>
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
@ -0,0 +1,8 @@
|
||||
{{> email_header}}
|
||||
<h2>Congratulations {{applicantName}},</h2>
|
||||
<p>Your <strong>Letter of Appointment</strong> is in place for application <strong>{{applicationId}}</strong>{{#if dealerCode}} (Dealer code: <strong>{{dealerCode}}</strong>){{/if}}.</p>
|
||||
<p>The <strong>Dealership Agreement</strong> is now ready for your review and e-signature on the Dealer Portal. Please complete your signature by <strong>{{dueDate}}</strong>.</p>
|
||||
{{> primary_cta}}
|
||||
<p>Once you sign, the agreement will be routed for Royal Enfield’s counter-signature as per process.</p>
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
10
src/emailtemplates/document_received_acknowledgement.html
Normal file
10
src/emailtemplates/document_received_acknowledgement.html
Normal file
@ -0,0 +1,10 @@
|
||||
{{> email_header}}
|
||||
<h2>Hi {{applicantName}},</h2>
|
||||
<p>Thank you. We have received your upload for application <strong>{{applicationId}}</strong>.</p>
|
||||
<p><strong>Category:</strong> {{documentCategory}}<br>
|
||||
<strong>Document type:</strong> {{documentName}}<br>
|
||||
<strong>Received on:</strong> {{receivedOn}}</p>
|
||||
<p>Our team will verify the submission. You will receive a separate notification if anything needs to be corrected or re-uploaded.</p>
|
||||
{{> primary_cta}}
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
12
src/emailtemplates/document_rejected_resubmit.html
Normal file
12
src/emailtemplates/document_rejected_resubmit.html
Normal file
@ -0,0 +1,12 @@
|
||||
{{> email_header}}
|
||||
<h2>Hi {{applicantName}},</h2>
|
||||
<p>One or more documents for application <strong>{{applicationId}}</strong> could not be accepted in their current form.</p>
|
||||
<div class="details">
|
||||
<p><strong>Document:</strong> {{documentName}}</p>
|
||||
<p><strong>Reason:</strong> {{rejectionReason}}</p>
|
||||
</div>
|
||||
<p>Please re-upload the corrected document on the Dealer Portal by <strong>{{dueDate}}</strong>.</p>
|
||||
{{> primary_cta}}
|
||||
<p>For clarification, contact your assigned Dealer Development representative.</p>
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
12
src/emailtemplates/document_submission_reminder.html
Normal file
12
src/emailtemplates/document_submission_reminder.html
Normal file
@ -0,0 +1,12 @@
|
||||
{{> email_header}}
|
||||
<h2>Hi {{applicantName}},</h2>
|
||||
<p>This is a reminder regarding your dealership application <strong>{{applicationId}}</strong>.</p>
|
||||
<p>The following items are still pending on the portal:</p>
|
||||
<div class="details">
|
||||
<p style="white-space:pre-line;">{{pendingDocuments}}</p>
|
||||
</div>
|
||||
<p>Please complete your uploads by <strong>{{dueDate}}</strong> so we can continue processing without delay.</p>
|
||||
{{> primary_cta}}
|
||||
<p>If you have already submitted everything, you may ignore this reminder.</p>
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
15
src/emailtemplates/fdd_document_request.html
Normal file
15
src/emailtemplates/fdd_document_request.html
Normal file
@ -0,0 +1,15 @@
|
||||
{{> email_header}}
|
||||
<h2>Hi {{applicantName}},</h2>
|
||||
<p>As part of <strong>Financial Due Diligence (FDD)</strong> for your dealership application <strong>{{applicationId}}</strong>, we have engaged <strong>{{fddPartnerName}}</strong> to complete the assessment.</p>
|
||||
<p>Please upload the documents requested on the Dealer Portal by <strong>{{dueDate}}</strong>.</p>
|
||||
{{#if documentChecklist}}
|
||||
<div class="details">
|
||||
<p style="white-space:pre-line;">{{documentChecklist}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<p>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.</p>
|
||||
{{/if}}
|
||||
{{> primary_cta}}
|
||||
<p>The FDD partner may contact you directly for clarifications. Incomplete disclosure may affect the outcome of your application.</p>
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
7
src/emailtemplates/loi_acknowledgement_request.html
Normal file
7
src/emailtemplates/loi_acknowledgement_request.html
Normal file
@ -0,0 +1,7 @@
|
||||
{{> email_header}}
|
||||
<h2>Hi {{applicantName}},</h2>
|
||||
<p>Your <strong>Letter of Intent (LOI)</strong> for application <strong>{{applicationId}}</strong> is awaiting your acknowledgement on the Dealer Portal.</p>
|
||||
<p>Please review the document and complete your acknowledgement by <strong>{{dueDate}}</strong>. We cannot move to the next onboarding steps until this is done.</p>
|
||||
{{> primary_cta}}
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
15
src/emailtemplates/prospect_document_request.html
Normal file
15
src/emailtemplates/prospect_document_request.html
Normal file
@ -0,0 +1,15 @@
|
||||
{{> email_header}}
|
||||
<h2>Hi {{applicantName}},</h2>
|
||||
<p>Congratulations on being shortlisted for the Royal Enfield dealership opportunity at <strong>{{location}}</strong> (Application ID: <strong>{{applicationId}}</strong>).</p>
|
||||
<p>To proceed with your evaluation, please upload the required documents on the Dealer Portal by <strong>{{dueDate}}</strong>.</p>
|
||||
{{#if documentList}}
|
||||
<div class="details">
|
||||
<p><strong>Document checklist:</strong></p>
|
||||
<p style="white-space:pre-line;">{{documentList}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
<p>All documents should be clear, in PDF or JPG format, and within the size limits shown on the portal.</p>
|
||||
{{> primary_cta}}
|
||||
<p>For queries, contact your Royal Enfield Dealer Development representative or write to <a href="mailto:dealer-support@royalenfield.com">dealer-support@royalenfield.com</a>.</p>
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
14
src/emailtemplates/security_deposit_request.html
Normal file
14
src/emailtemplates/security_deposit_request.html
Normal file
@ -0,0 +1,14 @@
|
||||
{{> email_header}}
|
||||
<h2>Hi {{applicantName}},</h2>
|
||||
<p>Following acknowledgement of your Letter of Intent for application <strong>{{applicationId}}</strong>, the next step is to remit the <strong>security deposit of ₹{{amount}}</strong> before <strong>{{dueDate}}</strong>.</p>
|
||||
<p>You may pay online through the portal or transfer funds as per the instructions shown after you log in.</p>
|
||||
{{#if bankDetails}}
|
||||
<div class="details">
|
||||
<p><strong>Bank transfer details (if applicable):</strong></p>
|
||||
<p style="white-space:pre-line;">{{bankDetails}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{> primary_cta}}
|
||||
<p>After payment, please upload the payment confirmation (UTR / reference) on the portal. Finance will verify and you will receive a confirmation notification.</p>
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
13
src/emailtemplates/statutory_document_request.html
Normal file
13
src/emailtemplates/statutory_document_request.html
Normal file
@ -0,0 +1,13 @@
|
||||
{{> email_header}}
|
||||
<h2>Hi {{applicantName}},</h2>
|
||||
<p>Your <strong>Letter of Intent (LOI)</strong> has been issued for application <strong>{{applicationId}}</strong>. The next step is to submit statutory and compliance documents required for dealership authorisation.</p>
|
||||
<p>Please upload the documents listed on the Dealer Portal by <strong>{{dueDate}}</strong>.</p>
|
||||
{{#if statutoryList}}
|
||||
<div class="details">
|
||||
<p style="white-space:pre-line;">{{statutoryList}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
<p>Our DD-Admin and Legal teams will verify each submission. You will be notified if any document needs to be re-submitted.</p>
|
||||
{{> primary_cta}}
|
||||
<p>Regards,<br>Royal Enfield Dealer Development Team</p>
|
||||
{{> email_footer}}
|
||||
@ -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);
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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']
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@ -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 || ''
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user