reported bugs around 15 were coverd from even new bug list also coverd from bangalore meet
This commit is contained in:
parent
ac31b9ba57
commit
29d67f6ca6
File diff suppressed because one or more lines are too long
6
build/assets/index-BvWiaLmW.css
Normal file
6
build/assets/index-BvWiaLmW.css
Normal file
File diff suppressed because one or more lines are too long
788
build/assets/index-KxZzWQFD.js
Normal file
788
build/assets/index-KxZzWQFD.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,14 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Royal Enfield Onboarding</title>
|
<title>Royal Enfield Onboarding</title>
|
||||||
<script type="module" crossorigin src="/assets/index-ny6fNePT.js"></script>
|
<script type="module" crossorigin src="/assets/index-KxZzWQFD.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index--4WsqmvE.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BvWiaLmW.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -229,14 +229,30 @@ mandatory remarks captured for traceability.
|
|||||||
- The **DD-Lead** consolidates all discussions, documents, and feedback.
|
- The **DD-Lead** consolidates all discussions, documents, and feedback.
|
||||||
- Prepares a **Resignation Presentation** with recommendations and supporting data.
|
- Prepares a **Resignation Presentation** with recommendations and supporting data.
|
||||||
- Uploads the presentation to the portal.
|
- Uploads the presentation to the portal.
|
||||||
- Forwards the case to **NBH** for final decision.
|
- Forwards the case to **DD-Head** for the next-level Dealer Development review.
|
||||||
- The resignation request is reviewed by the **DD-Lead and DD-Head**. At this stage, both
|
- At this stage, the DD-Lead is authorized to **Send Back or Revoke** the resignation
|
||||||
roles are authorized to **Send Back or Revoke** the resignation request for clarification,
|
request for clarification, correction, or reconsideration. **Send Back actions are**
|
||||||
correction, or reconsideration. **Send Back actions are communicated through Work**
|
**communicated through Work Notes** , with **mandatory remarks** recorded for audit and
|
||||||
**Notes** , with **mandatory remarks** recorded for audit and traceability.
|
traceability.
|
||||||
|
|
||||||
```
|
```
|
||||||
4.2.2.6 NBH Final Approval
|
4.2.2.6 DD-Head Review
|
||||||
|
```
|
||||||
|
- The **DD-Head** reviews the DD-Lead's consolidated dossier, presentation, and the
|
||||||
|
cumulative feedback captured by ASM, RBM/DD-ZM, ZBH, and DD-Lead.
|
||||||
|
- Validates that all Dealer Development checks (asset recovery readiness, transition
|
||||||
|
plan, territory impact) have been adequately addressed at the DD layer.
|
||||||
|
- Adds final Dealer Development remarks and either:
|
||||||
|
o **Approve** → Forwards the case to **NBH** for the National-level decision.
|
||||||
|
o **Send Back for Clarification** → Returns the case to **DD-Lead** with mandatory
|
||||||
|
remarks logged through Work Notes.
|
||||||
|
o **Revoke** → Closes the request with a documented rationale captured in Work Notes.
|
||||||
|
- The transition from **DD-Lead → DD-Head → NBH** is fully inline; on DD-Head approval
|
||||||
|
the workflow moves to NBH **without manual intervention** , and the SLA timer for the
|
||||||
|
next stage starts automatically.
|
||||||
|
|
||||||
|
```
|
||||||
|
4.2.2.7 NBH Final Approval
|
||||||
```
|
```
|
||||||
- The **National Business Head (NBH)** reviews the entire resignation dossier.
|
- The **National Business Head (NBH)** reviews the entire resignation dossier.
|
||||||
- Adds final remarks with one of the following outcomes:
|
- Adds final remarks with one of the following outcomes:
|
||||||
@ -251,7 +267,7 @@ mandatory remarks captured for traceability.
|
|||||||
communication and governance.
|
communication and governance.
|
||||||
|
|
||||||
```
|
```
|
||||||
4.2.2.7 Legal Acceptance Letter
|
4.2.2.8 Legal Acceptance Letter
|
||||||
```
|
```
|
||||||
- Once approved by **NBH** , the request is **auto-assigned to the Legal team**.
|
- Once approved by **NBH** , the request is **auto-assigned to the Legal team**.
|
||||||
- Legal verifies the uploaded resignation and issues a **Resignation Acceptance Letter**.
|
- Legal verifies the uploaded resignation and issues a **Resignation Acceptance Letter**.
|
||||||
@ -264,7 +280,7 @@ mandatory remarks captured for traceability.
|
|||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
4.2.2.8 DD-Admin Closure
|
4.2.2.9 DD-Admin Closure
|
||||||
```
|
```
|
||||||
- The **DD-Admin** downloads and shares the final **Resignation Acceptance Letter** with the
|
- The **DD-Admin** downloads and shares the final **Resignation Acceptance Letter** with the
|
||||||
dealer.
|
dealer.
|
||||||
|
|||||||
@ -334,7 +334,7 @@ describe('Resignation Revoke — Dealer notified on terminal event', () => {
|
|||||||
|
|
||||||
// ─── F&F Trigger (LWD) ────────────────────────────────────────────────────────
|
// ─── F&F Trigger (LWD) ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('F&F Trigger — LWD-based gate (SRS §1.1.5 + §4.2.2.8)', () => {
|
describe('F&F Trigger — LWD-based gate (SRS §1.1.5 + §4.2.2.9)', () => {
|
||||||
it('TC-RES-15: F&F initiation is allowed when today >= LWD', () => {
|
it('TC-RES-15: F&F initiation is allowed when today >= LWD', () => {
|
||||||
const lwd = new Date('2026-01-01'); // in the past
|
const lwd = new Date('2026-01-01'); // in the past
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|||||||
@ -200,6 +200,7 @@ export const RESIGNATION_STAGES = {
|
|||||||
RBM: 'RBM + DD-ZM Review',
|
RBM: 'RBM + DD-ZM Review',
|
||||||
ZBH: 'ZBH',
|
ZBH: 'ZBH',
|
||||||
DD_LEAD: 'DD Lead',
|
DD_LEAD: 'DD Lead',
|
||||||
|
DD_HEAD: 'DD Head',
|
||||||
NBH: 'NBH',
|
NBH: 'NBH',
|
||||||
DD_ADMIN: 'DD Admin',
|
DD_ADMIN: 'DD Admin',
|
||||||
/** Post DD Admin — workflow paused until an authorized user runs Push to F&F (no automatic F&F). */
|
/** Post DD Admin — workflow paused until an authorized user runs Push to F&F (no automatic F&F). */
|
||||||
@ -420,6 +421,7 @@ export const AUDIT_ACTIONS = {
|
|||||||
RESIGNATION_REJECTED: 'RESIGNATION_REJECTED',
|
RESIGNATION_REJECTED: 'RESIGNATION_REJECTED',
|
||||||
RESIGNATION_REVOKED: 'RESIGNATION_REVOKED',
|
RESIGNATION_REVOKED: 'RESIGNATION_REVOKED',
|
||||||
RESIGNATION_SENT_BACK: 'RESIGNATION_SENT_BACK',
|
RESIGNATION_SENT_BACK: 'RESIGNATION_SENT_BACK',
|
||||||
|
RESIGNATION_LETTER_DISPATCHED: 'RESIGNATION_LETTER_DISPATCHED',
|
||||||
TERMINATION_REVOKED: 'TERMINATION_REVOKED',
|
TERMINATION_REVOKED: 'TERMINATION_REVOKED',
|
||||||
TERMINATION_SENT_BACK: 'TERMINATION_SENT_BACK',
|
TERMINATION_SENT_BACK: 'TERMINATION_SENT_BACK',
|
||||||
RELOCATION_SENT_BACK: 'RELOCATION_SENT_BACK',
|
RELOCATION_SENT_BACK: 'RELOCATION_SENT_BACK',
|
||||||
@ -538,6 +540,7 @@ export const RESIGNATION_DOCUMENT_STAGES = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const TERMINATION_DOCUMENT_TYPES = [
|
export const TERMINATION_DOCUMENT_TYPES = [
|
||||||
|
'Presentation',
|
||||||
'Termination Recommendation',
|
'Termination Recommendation',
|
||||||
'Show Cause Notice',
|
'Show Cause Notice',
|
||||||
'SCN Response',
|
'SCN Response',
|
||||||
|
|||||||
@ -108,6 +108,7 @@ const RESIGNATION_SLA: SlaCatalogEntry[] = [
|
|||||||
d('RESIGNATION', RESIGNATION_STAGES.RBM, 'RBM, DD-ZM', 3),
|
d('RESIGNATION', RESIGNATION_STAGES.RBM, 'RBM, DD-ZM', 3),
|
||||||
d('RESIGNATION', RESIGNATION_STAGES.ZBH, 'ZBH', 3),
|
d('RESIGNATION', RESIGNATION_STAGES.ZBH, 'ZBH', 3),
|
||||||
d('RESIGNATION', RESIGNATION_STAGES.DD_LEAD, 'DD Lead', 5),
|
d('RESIGNATION', RESIGNATION_STAGES.DD_LEAD, 'DD Lead', 5),
|
||||||
|
d('RESIGNATION', RESIGNATION_STAGES.DD_HEAD, 'DD Head', 5),
|
||||||
d('RESIGNATION', RESIGNATION_STAGES.NBH, 'NBH', 5),
|
d('RESIGNATION', RESIGNATION_STAGES.NBH, 'NBH', 5),
|
||||||
d('RESIGNATION', RESIGNATION_STAGES.LEGAL, 'Legal Admin', 7),
|
d('RESIGNATION', RESIGNATION_STAGES.LEGAL, 'Legal Admin', 7),
|
||||||
d('RESIGNATION', RESIGNATION_STAGES.DD_ADMIN, 'DD Admin', 3),
|
d('RESIGNATION', RESIGNATION_STAGES.DD_ADMIN, 'DD Admin', 3),
|
||||||
|
|||||||
@ -88,6 +88,18 @@ export const uploadSingleIfMultipart = (req: Request, res: Response, next: NextF
|
|||||||
// Multiple files upload
|
// Multiple files upload
|
||||||
export const uploadMultiple = upload.array('files', 10); // Max 10 files
|
export const uploadMultiple = upload.array('files', 10); // Max 10 files
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only parse multipart with multiple files when the client sends multipart/form-data.
|
||||||
|
* Otherwise JSON bodies from express.json() are preserved.
|
||||||
|
*/
|
||||||
|
export const uploadMultipleIfMultipart = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const ct = String(req.headers['content-type'] || '').toLowerCase();
|
||||||
|
if (ct.includes('multipart/form-data')) {
|
||||||
|
return uploadMultiple(req, res, next);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
// Error handler for multer
|
// Error handler for multer
|
||||||
export const handleUploadError = (err: any, req: Request, res: Response, next: NextFunction) => {
|
export const handleUploadError = (err: any, req: Request, res: Response, next: NextFunction) => {
|
||||||
if (err instanceof multer.MulterError) {
|
if (err instanceof multer.MulterError) {
|
||||||
|
|||||||
@ -119,11 +119,21 @@ async function resolveEmailTemplate(templateCode: string) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SendEmailOptions {
|
||||||
|
attachments?: Array<{
|
||||||
|
filename: string;
|
||||||
|
path?: string;
|
||||||
|
content?: Buffer | string;
|
||||||
|
contentType?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export const sendEmail = async (
|
export const sendEmail = async (
|
||||||
to: string,
|
to: string,
|
||||||
subject: string,
|
subject: string,
|
||||||
templateCode: string,
|
templateCode: string,
|
||||||
replacements: Record<string, string>
|
replacements: Record<string, string>,
|
||||||
|
options?: SendEmailOptions
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
let finalHtml = '';
|
let finalHtml = '';
|
||||||
@ -181,7 +191,8 @@ export const sendEmail = async (
|
|||||||
from: fromAddress,
|
from: fromAddress,
|
||||||
to,
|
to,
|
||||||
subject: finalSubject,
|
subject: finalSubject,
|
||||||
html: finalHtml
|
html: finalHtml,
|
||||||
|
...(options?.attachments?.length ? { attachments: options.attachments } : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
const info = await readyTransporter.sendMail(mail);
|
const info = await readyTransporter.sendMail(mail);
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export const getResignationStatusForStage = (stage: string): string => {
|
|||||||
case RESIGNATION_STAGES.ASM:
|
case RESIGNATION_STAGES.ASM:
|
||||||
case RESIGNATION_STAGES.ZBH:
|
case RESIGNATION_STAGES.ZBH:
|
||||||
case RESIGNATION_STAGES.DD_LEAD:
|
case RESIGNATION_STAGES.DD_LEAD:
|
||||||
|
case RESIGNATION_STAGES.DD_HEAD:
|
||||||
case RESIGNATION_STAGES.NBH:
|
case RESIGNATION_STAGES.NBH:
|
||||||
case RESIGNATION_STAGES.DD_ADMIN:
|
case RESIGNATION_STAGES.DD_ADMIN:
|
||||||
return `${stage} Review`;
|
return `${stage} Review`;
|
||||||
|
|||||||
@ -38,7 +38,8 @@ export const getPreviousStage = (requestType: string, currentStage: string): str
|
|||||||
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ASM,
|
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ASM,
|
||||||
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.RBM,
|
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.RBM,
|
||||||
[RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.ZBH,
|
[RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.ZBH,
|
||||||
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_LEAD,
|
[RESIGNATION_STAGES.DD_HEAD]: RESIGNATION_STAGES.DD_LEAD,
|
||||||
|
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_HEAD,
|
||||||
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.NBH,
|
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.NBH,
|
||||||
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
|
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
|
||||||
[RESIGNATION_STAGES.AWAITING_FNF]: RESIGNATION_STAGES.DD_ADMIN,
|
[RESIGNATION_STAGES.AWAITING_FNF]: RESIGNATION_STAGES.DD_ADMIN,
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { Op } from 'sequelize';
|
|||||||
import { sendEmail } from './email.service.js';
|
import { sendEmail } from './email.service.js';
|
||||||
import { NotificationService } from '../../services/NotificationService.js';
|
import { NotificationService } from '../../services/NotificationService.js';
|
||||||
import {
|
import {
|
||||||
import { getFrontendBaseUrl } from './frontendUrl.js';
|
|
||||||
APPLICATION_STAGES,
|
APPLICATION_STAGES,
|
||||||
TERMINATION_STAGES,
|
TERMINATION_STAGES,
|
||||||
CONSTITUTIONAL_STAGES,
|
CONSTITUTIONAL_STAGES,
|
||||||
@ -11,6 +10,7 @@ import { getFrontendBaseUrl } from './frontendUrl.js';
|
|||||||
REQUEST_TYPES,
|
REQUEST_TYPES,
|
||||||
ROLES
|
ROLES
|
||||||
} from '../config/constants.js';
|
} from '../config/constants.js';
|
||||||
|
import { getFrontendBaseUrl } from './frontendUrl.js';
|
||||||
|
|
||||||
const { RequestParticipant, User, Outlet, District, Dealer } = db;
|
const { RequestParticipant, User, Outlet, District, Dealer } = db;
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export const ALLOWED_EMAIL_TEMPLATE_CODES = [
|
|||||||
'RESIGNATION_RECEIVED',
|
'RESIGNATION_RECEIVED',
|
||||||
'RESIGNATION_SUBMITTED',
|
'RESIGNATION_SUBMITTED',
|
||||||
'RESIGNATION_UPDATE',
|
'RESIGNATION_UPDATE',
|
||||||
|
'RESIGNATION_LETTER_DISPATCHED_DEALER',
|
||||||
'SLA_BREACH_WARNING',
|
'SLA_BREACH_WARNING',
|
||||||
'STATUTORY_DOCUMENT_REQUEST',
|
'STATUTORY_DOCUMENT_REQUEST',
|
||||||
'SLA_REMINDER',
|
'SLA_REMINDER',
|
||||||
|
|||||||
21
src/emailtemplates/resignation_letter_dispatched_dealer.html
Normal file
21
src/emailtemplates/resignation_letter_dispatched_dealer.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{{> email_header}}
|
||||||
|
<h2>Your Resignation Acceptance Letter</h2>
|
||||||
|
<p>Dear {{dealerName}},</p>
|
||||||
|
<p>
|
||||||
|
The Dealer Development office has formally issued the acceptance letter for your
|
||||||
|
resignation request (Request ID: <strong>{{resignationId}}</strong>).
|
||||||
|
</p>
|
||||||
|
<p><strong>Proposed Last Working Day:</strong> {{lwd}}</p>
|
||||||
|
<p>
|
||||||
|
The signed acceptance letter is <strong>attached to this email</strong>. Please review
|
||||||
|
it and retain a copy for your records. The Full & Final settlement process will be
|
||||||
|
initiated by the team after your Last Working Day.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For any questions, please contact <strong>{{dispatchedBy}}</strong> or the Dealer
|
||||||
|
Development team using the link below.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{{link}}">{{ctaLabel}}</a>
|
||||||
|
</p>
|
||||||
|
{{> email_footer}}
|
||||||
@ -24,6 +24,9 @@ import { OFFBOARDING_ACTIONS } from '../../common/config/constants.js';
|
|||||||
import { validateOffboardingAction, getPreviousStage } from '../../common/utils/offboardingWorkflow.utils.js';
|
import { validateOffboardingAction, getPreviousStage } from '../../common/utils/offboardingWorkflow.utils.js';
|
||||||
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
||||||
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
|
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
|
||||||
|
import { sendEmail } from '../../common/utils/email.service.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
// Removed generateResignationId and moved to NomenclatureService
|
// Removed generateResignationId and moved to NomenclatureService
|
||||||
const resolveResignationUuid = async (id: string) => {
|
const resolveResignationUuid = async (id: string) => {
|
||||||
@ -395,7 +398,8 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
[RESIGNATION_STAGES.ASM]: RESIGNATION_STAGES.RBM,
|
[RESIGNATION_STAGES.ASM]: RESIGNATION_STAGES.RBM,
|
||||||
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ZBH,
|
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ZBH,
|
||||||
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.DD_LEAD,
|
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.DD_LEAD,
|
||||||
[RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.NBH,
|
[RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.DD_HEAD,
|
||||||
|
[RESIGNATION_STAGES.DD_HEAD]: RESIGNATION_STAGES.NBH,
|
||||||
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.LEGAL,
|
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.LEGAL,
|
||||||
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.DD_ADMIN,
|
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.DD_ADMIN,
|
||||||
// DD Admin approval completes internal review; F&F is started only via explicit Push to F&F.
|
// DD Admin approval completes internal review; F&F is started only via explicit Push to F&F.
|
||||||
@ -410,7 +414,7 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// F&F records are created only from explicit Push to F&F (targetStage), not from sequential approvals.
|
// F&F records are created only from explicit Push to F&F (targetStage), not from sequential approvals.
|
||||||
// LWD gate applies to that manual push (SRS §4.2.2.8).
|
// LWD gate applies to that manual push (SRS §4.2.2.9).
|
||||||
let shouldTriggerFnF = false;
|
let shouldTriggerFnF = false;
|
||||||
if (nextStage === RESIGNATION_STAGES.FNF_INITIATED && targetOverride) {
|
if (nextStage === RESIGNATION_STAGES.FNF_INITIATED && targetOverride) {
|
||||||
const lwdString = resignation.lastOperationalDateServices || resignation.lastOperationalDateSales;
|
const lwdString = resignation.lastOperationalDateServices || resignation.lastOperationalDateSales;
|
||||||
@ -1098,6 +1102,9 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n
|
|||||||
case 'assign':
|
case 'assign':
|
||||||
return assignResignation(req, res, next);
|
return assignResignation(req, res, next);
|
||||||
|
|
||||||
|
case 'dispatch':
|
||||||
|
return dispatchResignationLetter(req, res, next);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@ -1109,3 +1116,247 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch the Resignation Acceptance Letter to the dealer (DD Admin step).
|
||||||
|
*
|
||||||
|
* Workflow context:
|
||||||
|
* Legal Admin uploads the Resignation Acceptance Letter at the Legal stage and
|
||||||
|
* approves → the request transitions to DD Admin. At DD Admin, the assigned admin
|
||||||
|
* formally sends the dispatched copy of the letter to the dealer (email +
|
||||||
|
* download link). On successful dispatch the request advances to Awaiting F&F.
|
||||||
|
*
|
||||||
|
* Authorization:
|
||||||
|
* - DD Admin (assigned role for the DD Admin stage)
|
||||||
|
* - Super Admin (bypass)
|
||||||
|
*/
|
||||||
|
export const dispatchResignationLetter = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
const transaction: Transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
|
|
||||||
|
const allowedRoles: string[] = [ROLES.DD_ADMIN, ROLES.SUPER_ADMIN];
|
||||||
|
if (!allowedRoles.includes(req.user.roleCode as any)) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Only DD Admin or Super Admin can dispatch the resignation acceptance letter.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
const remarks: string = (req.body?.remarks || '').trim();
|
||||||
|
const resolvedId = await resolveResignationUuid(String(id));
|
||||||
|
|
||||||
|
const resignation = await db.Resignation.findOne({
|
||||||
|
where: { id: resolvedId },
|
||||||
|
include: [{ model: db.Outlet, as: 'outlet' }],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!resignation) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(404).json({ success: false, message: 'Resignation not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch is available from DD Admin onwards. If the admin forgot to
|
||||||
|
// send the letter at DD Admin, the action remains accessible while the
|
||||||
|
// request moves through Awaiting F&F / F&F Initiated, so the dealer can
|
||||||
|
// still be informed retroactively. Once the case is closed (Completed /
|
||||||
|
// Rejected / Withdrawn / Revoked) dispatch is no longer meaningful.
|
||||||
|
const dispatchAllowedStages: string[] = [
|
||||||
|
RESIGNATION_STAGES.DD_ADMIN,
|
||||||
|
RESIGNATION_STAGES.AWAITING_FNF,
|
||||||
|
RESIGNATION_STAGES.FNF_INITIATED
|
||||||
|
];
|
||||||
|
const closedStatuses = ['Completed', 'Rejected', 'Withdrawn', 'Revoked', 'Settled'];
|
||||||
|
if (
|
||||||
|
!dispatchAllowedStages.includes(resignation.currentStage as any) ||
|
||||||
|
closedStatuses.includes(String(resignation.status || ''))
|
||||||
|
) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Dispatch can be performed only from DD Admin stage onwards while the request is still open. Current stage: ${resignation.currentStage}, status: ${resignation.status}.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block duplicate dispatch — once the letter has been sent we don't
|
||||||
|
// want a second email going out without the user explicitly knowing.
|
||||||
|
const existingDispatch = await db.ResignationAudit.findOne({
|
||||||
|
where: {
|
||||||
|
resignationId: resignation.id,
|
||||||
|
action: AUDIT_ACTIONS.RESIGNATION_LETTER_DISPATCHED
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (existingDispatch) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Resignation acceptance letter has already been dispatched to the dealer.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick the most recent acceptance letter — preferentially typed as
|
||||||
|
// "Resignation Acceptance Letter"; fall back to any document uploaded at
|
||||||
|
// the Legal stage (covers legacy uploads where documentType was generic).
|
||||||
|
const acceptanceLetter = await db.ResignationDocument.findOne({
|
||||||
|
where: {
|
||||||
|
resignationId: resignation.id,
|
||||||
|
[Op.or]: [
|
||||||
|
{ documentType: 'Resignation Acceptance Letter' },
|
||||||
|
{ stage: RESIGNATION_STAGES.LEGAL }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
order: [['createdAt', 'DESC']],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!acceptanceLetter) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'No Resignation Acceptance Letter is available. Ask Legal to upload the letter before dispatching.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup dealer user for the dealer email channel.
|
||||||
|
// `Resignation.dealerId` is used inconsistently across the codebase —
|
||||||
|
// it may point at the dealer-User directly (User.id) for internal /
|
||||||
|
// legacy rows, or at the Dealer record (Dealer.id) which is then
|
||||||
|
// referenced from User.dealerId. Mirror the resolution used by
|
||||||
|
// ResignationWorkflowService.transitionResignation so we cover both.
|
||||||
|
const dealerUser = await db.User.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id: resignation.dealerId },
|
||||||
|
{ dealerId: resignation.dealerId }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
const portalBase = getFrontendBaseUrl();
|
||||||
|
const dealerName = dealerUser?.fullName || (resignation as any).outlet?.name || 'Dealer';
|
||||||
|
const resignationCode = resignation.resignationId || resignation.id;
|
||||||
|
const lwd = resignation.lastOperationalDateServices || resignation.lastOperationalDateSales || 'N/A';
|
||||||
|
const portalLink = `${portalBase}/dealer-resignation/${resignation.id}`;
|
||||||
|
|
||||||
|
// Resolve the acceptance letter on disk so we can attach the file itself
|
||||||
|
// to the email instead of redirecting the dealer to our portal.
|
||||||
|
// `acceptanceLetter.filePath` is stored as `/uploads/documents/<name>`;
|
||||||
|
// map it back to UPLOAD_DIR using just the basename so we never let an
|
||||||
|
// injected path escape the documents folder.
|
||||||
|
const uploadDir = process.env.UPLOAD_DIR || './uploads';
|
||||||
|
const documentsDir = path.resolve(uploadDir, 'documents');
|
||||||
|
const letterDiskPath = path.resolve(documentsDir, path.basename(acceptanceLetter.filePath));
|
||||||
|
const letterFileExists = fs.existsSync(letterDiskPath);
|
||||||
|
if (!letterFileExists) {
|
||||||
|
logger.warn(
|
||||||
|
`[dispatchResignationLetter] Acceptance letter file missing on disk: ${letterDiskPath}. Email will be sent without attachment.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record audit + timeline entry before sending notifications so the trail
|
||||||
|
// is consistent even if the email transport blips. The transition below
|
||||||
|
// is the workflow gate; this row provides a verbose human-readable note.
|
||||||
|
await db.ResignationAudit.create({
|
||||||
|
userId: req.user.id,
|
||||||
|
resignationId: resignation.id,
|
||||||
|
action: AUDIT_ACTIONS.RESIGNATION_LETTER_DISPATCHED,
|
||||||
|
remarks: remarks || `Resignation acceptance letter dispatched to ${dealerName}`,
|
||||||
|
details: {
|
||||||
|
fileName: acceptanceLetter.fileName,
|
||||||
|
documentId: acceptanceLetter.id,
|
||||||
|
dealerEmail: dealerUser?.email || null,
|
||||||
|
stage: RESIGNATION_STAGES.DD_ADMIN
|
||||||
|
}
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
const timeline = [...(resignation.timeline || []), {
|
||||||
|
stage: RESIGNATION_STAGES.DD_ADMIN,
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: req.user.fullName,
|
||||||
|
role: req.user.roleCode,
|
||||||
|
action: 'Resignation Letter Dispatched',
|
||||||
|
remarks: remarks || `Acceptance letter sent to ${dealerName}${dealerUser?.email ? ` (${dealerUser.email})` : ''}`
|
||||||
|
}];
|
||||||
|
await resignation.update({ timeline }, { transaction });
|
||||||
|
|
||||||
|
const wasAtDDAdmin = resignation.currentStage === RESIGNATION_STAGES.DD_ADMIN;
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
// Advance the workflow only when the dispatch happens at the DD Admin step.
|
||||||
|
// If the admin missed dispatch and the case has already moved on
|
||||||
|
// (Awaiting F&F / F&F Initiated), we send the letter without touching the
|
||||||
|
// current stage to avoid an artificial "step back".
|
||||||
|
if (wasAtDDAdmin) {
|
||||||
|
await ResignationWorkflowService.transitionResignation(
|
||||||
|
resignation,
|
||||||
|
RESIGNATION_STAGES.AWAITING_FNF,
|
||||||
|
req.user.id,
|
||||||
|
{
|
||||||
|
action: 'Resignation Letter Dispatched',
|
||||||
|
remarks: remarks || 'Acceptance letter dispatched to dealer.'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the formal dispatch email to the dealer with the acceptance
|
||||||
|
// letter file attached. Non-fatal: log and proceed if the mailer hiccups.
|
||||||
|
if (dealerUser?.email) {
|
||||||
|
try {
|
||||||
|
await sendEmail(
|
||||||
|
dealerUser.email,
|
||||||
|
`Resignation Acceptance Letter Dispatched: ${resignationCode}`,
|
||||||
|
'RESIGNATION_LETTER_DISPATCHED_DEALER',
|
||||||
|
{
|
||||||
|
dealerName,
|
||||||
|
resignationId: String(resignationCode),
|
||||||
|
lwd: String(lwd),
|
||||||
|
dispatchedBy: req.user.fullName || 'Dealer Development Office',
|
||||||
|
link: portalLink,
|
||||||
|
ctaLabel: 'View Resignation Status'
|
||||||
|
},
|
||||||
|
letterFileExists
|
||||||
|
? {
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
filename: acceptanceLetter.fileName,
|
||||||
|
path: letterDiskPath,
|
||||||
|
contentType: acceptanceLetter.mimeType || undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
} catch (mailErr) {
|
||||||
|
logger.error('[dispatchResignationLetter] dealer email failed:', mailErr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`[dispatchResignationLetter] No dealer user/email found for resignation ${resignation.resignationId}; in-app/portal notification only.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: wasAtDDAdmin
|
||||||
|
? 'Resignation acceptance letter dispatched to the dealer. Request moved to Awaiting F&F.'
|
||||||
|
: 'Resignation acceptance letter dispatched to the dealer. Workflow stage left unchanged.',
|
||||||
|
nextStage: wasAtDDAdmin ? RESIGNATION_STAGES.AWAITING_FNF : resignation.currentStage,
|
||||||
|
document: {
|
||||||
|
id: acceptanceLetter.id,
|
||||||
|
fileName: acceptanceLetter.fileName,
|
||||||
|
filePath: acceptanceLetter.filePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (transaction) {
|
||||||
|
try { await transaction.rollback(); } catch (_) { /* already finalized */ }
|
||||||
|
}
|
||||||
|
logger.error('Error dispatching resignation letter:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -19,6 +19,7 @@ router.put('/:id/withdraw', authenticate as any, resignationController.withdrawR
|
|||||||
router.post('/:id/withdraw', authenticate as any, resignationController.withdrawResignation);
|
router.post('/:id/withdraw', authenticate as any, resignationController.withdrawResignation);
|
||||||
router.put('/:id/sendback', authenticate as any, resignationController.sendBackResignation);
|
router.put('/:id/sendback', authenticate as any, resignationController.sendBackResignation);
|
||||||
router.post('/:id/sendback', authenticate as any, resignationController.sendBackResignation);
|
router.post('/:id/sendback', authenticate as any, resignationController.sendBackResignation);
|
||||||
|
router.post('/:id/dispatch', authenticate as any, resignationController.dispatchResignationLetter);
|
||||||
|
|
||||||
router.put('/:id/clearance', authenticate as any, uploadSingleIfMultipart, resignationController.updateClearance);
|
router.put('/:id/clearance', authenticate as any, uploadSingleIfMultipart, resignationController.updateClearance);
|
||||||
router.post('/:id/documents', authenticate as any, uploadSingle, resignationController.uploadResignationDocument);
|
router.post('/:id/documents', authenticate as any, uploadSingle, resignationController.uploadResignationDocument);
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
|
|||||||
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||||
import { NotificationService } from '../../services/NotificationService.js';
|
import { NotificationService } from '../../services/NotificationService.js';
|
||||||
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
|
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
const portalBase = getFrontendBaseUrl();
|
const portalBase = getFrontendBaseUrl();
|
||||||
|
|
||||||
@ -89,31 +90,64 @@ export const uploadFnFDocument = async (req: AuthRequest, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const fileUrl = req.file ? `/uploads/documents/${req.file.filename}` : req.body.fileUrl;
|
const fileUrl = req.file ? `/uploads/documents/${req.file.filename}` : req.body.fileUrl;
|
||||||
|
|
||||||
if (!fileUrl) {
|
if (!fileUrl) {
|
||||||
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const documentName = (req.body?.documentName || '').toString().trim();
|
||||||
|
if (!documentName) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Document name is required.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentType = (req.body?.documentType || '').toString().trim() || null;
|
||||||
|
|
||||||
const fnf = await FnF.findByPk(id);
|
const fnf = await FnF.findByPk(id);
|
||||||
if (!fnf) {
|
if (!fnf) {
|
||||||
return res.status(404).json({ success: false, message: 'F&F settlement not found' });
|
return res.status(404).json({ success: false, message: 'F&F settlement not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newEntry = {
|
||||||
|
id: uuidv4(),
|
||||||
|
name: documentName,
|
||||||
|
documentType,
|
||||||
|
originalFileName: req.file?.originalname || null,
|
||||||
|
supportingDocument: fileUrl,
|
||||||
|
department: 'Finance', // Default to Finance for generic F&F docs
|
||||||
|
clearedAt: new Date().toISOString(),
|
||||||
|
uploadedBy: req.user?.id || null
|
||||||
|
};
|
||||||
|
|
||||||
const updatedClearances = [
|
const updatedClearances = [
|
||||||
...(fnf.clearanceDocuments || []),
|
...(fnf.clearanceDocuments || []),
|
||||||
{
|
newEntry
|
||||||
name: req.file?.originalname || 'Uploaded Document',
|
|
||||||
supportingDocument: fileUrl,
|
|
||||||
department: 'Finance', // Default to Finance for generic F&F docs
|
|
||||||
clearedAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
await fnf.update({
|
await fnf.update({
|
||||||
clearanceDocuments: updatedClearances
|
clearanceDocuments: updatedClearances
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true, url: fileUrl, name: req.file?.originalname || 'Document' });
|
try {
|
||||||
|
await db.FnFAudit.create({
|
||||||
|
userId: req.user?.id || null,
|
||||||
|
fnfId: String(id),
|
||||||
|
action: AUDIT_ACTIONS.DOCUMENT_UPLOADED,
|
||||||
|
remarks: `Document "${newEntry.name}" uploaded`,
|
||||||
|
details: {
|
||||||
|
documentName: newEntry.name,
|
||||||
|
documentType: newEntry.documentType,
|
||||||
|
originalFileName: newEntry.originalFileName,
|
||||||
|
department: newEntry.department
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (auditErr) {
|
||||||
|
console.error('[uploadFnFDocument] Non-fatal audit failure:', auditErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, document: newEntry });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading F&F document:', error);
|
console.error('Error uploading F&F document:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error uploading F&F document' });
|
res.status(500).json({ success: false, message: 'Error uploading F&F document' });
|
||||||
|
|||||||
@ -47,6 +47,40 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadedFiles: Express.Multer.File[] = Array.isArray(req.files)
|
||||||
|
? (req.files as Express.Multer.File[])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const PPT_MIME_TYPES = [
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||||
|
];
|
||||||
|
const isPptFile = (file: Express.Multer.File) => {
|
||||||
|
const lowerName = String(file.originalname || '').toLowerCase();
|
||||||
|
const hasPptExt = lowerName.endsWith('.ppt') || lowerName.endsWith('.pptx');
|
||||||
|
const hasPptMime = PPT_MIME_TYPES.includes(file.mimetype);
|
||||||
|
return hasPptMime || hasPptExt;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPresentationMandatory = req.user.roleCode !== ROLES.SUPER_ADMIN;
|
||||||
|
if (isPresentationMandatory) {
|
||||||
|
if (uploadedFiles.length === 0) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'At least one Presentation (.ppt or .pptx) is mandatory to initiate a termination request.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const hasAtLeastOnePpt = uploadedFiles.some(isPptFile);
|
||||||
|
if (!hasAtLeastOnePpt) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'At least one PowerPoint file (.ppt or .pptx) is required as the Presentation.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { dealerId, category, reason, proposedLwd, comments } = req.body;
|
const { dealerId, category, reason, proposedLwd, comments } = req.body;
|
||||||
|
|
||||||
const requestId = await NomenclatureService.generateTerminationId();
|
const requestId = await NomenclatureService.generateTerminationId();
|
||||||
@ -81,6 +115,33 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N
|
|||||||
remarks: 'Admin initiated termination request'
|
remarks: 'Admin initiated termination request'
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
|
if (uploadedFiles.length > 0) {
|
||||||
|
const stage = 'Submitted';
|
||||||
|
for (const file of uploadedFiles) {
|
||||||
|
const documentType = isPptFile(file) ? 'Presentation' : 'Other';
|
||||||
|
const filePath = `/uploads/documents/${file.filename}`;
|
||||||
|
|
||||||
|
await db.TerminationDocument.create({
|
||||||
|
terminationRequestId: termination.id,
|
||||||
|
documentType,
|
||||||
|
fileName: file.originalname,
|
||||||
|
filePath,
|
||||||
|
fileSize: file.size,
|
||||||
|
mimeType: file.mimetype,
|
||||||
|
stage,
|
||||||
|
uploadedBy: req.user.id
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
await db.TerminationAudit.create({
|
||||||
|
userId: req.user.id,
|
||||||
|
terminationRequestId: termination.id,
|
||||||
|
action: AUDIT_ACTIONS.DOCUMENT_UPLOADED,
|
||||||
|
remarks: `${documentType} uploaded`,
|
||||||
|
details: { fileName: file.originalname, stage, documentType }
|
||||||
|
}, { transaction });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
// Add as chat participants (Async)
|
// Add as chat participants (Async)
|
||||||
@ -363,6 +424,32 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workflow-changing actions are gated by role. Only the role assigned to the
|
||||||
|
// current stage can act; Super Admin is the sole role that can bypass any stage.
|
||||||
|
// DD Admin is administrative and intentionally has no approve/reject power.
|
||||||
|
const workflowChangingActions: string[] = [
|
||||||
|
OFFBOARDING_ACTIONS.REJECT,
|
||||||
|
OFFBOARDING_ACTIONS.REVOKE,
|
||||||
|
OFFBOARDING_ACTIONS.SEND_BACK,
|
||||||
|
'sendback',
|
||||||
|
'approve'
|
||||||
|
];
|
||||||
|
const isWorkflowChanging =
|
||||||
|
workflowChangingActions.includes(action) || (action !== 'pushfnf' && action !== OFFBOARDING_ACTIONS.HOLD);
|
||||||
|
if (isWorkflowChanging) {
|
||||||
|
const isSuperAdmin = req.user.roleCode === ROLES.SUPER_ADMIN;
|
||||||
|
const allowed = isSuperAdmin
|
||||||
|
? true
|
||||||
|
: await TerminationWorkflowService.canUserAction(termination, req.user);
|
||||||
|
if (!allowed) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'You are not authorized to approve, reject, send back, or revoke this termination request at the current stage. Only the stage owner or a Super Admin may take this action.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fromStage = termination.currentStage;
|
const fromStage = termination.currentStage;
|
||||||
let approvedToStage: string | null = null;
|
let approvedToStage: string | null = null;
|
||||||
let scheduleLwdFnfReminder = false;
|
let scheduleLwdFnfReminder = false;
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import {
|
|||||||
submitScnResponse, recordPersonalHearing, updateClearance, uploadTerminationDocument, issueScn, uploadScnResponse, finalizeTermination
|
submitScnResponse, recordPersonalHearing, updateClearance, uploadTerminationDocument, issueScn, uploadScnResponse, finalizeTermination
|
||||||
} from './termination.controller.js';
|
} from './termination.controller.js';
|
||||||
import { authenticate } from '../../common/middleware/auth.js';
|
import { authenticate } from '../../common/middleware/auth.js';
|
||||||
import { uploadSingle } from '../../common/middleware/upload.js';
|
import { uploadSingle, uploadMultipleIfMultipart } from '../../common/middleware/upload.js';
|
||||||
|
|
||||||
router.use(authenticate as any);
|
router.use(authenticate as any);
|
||||||
|
|
||||||
router.post('/', createTermination);
|
router.post('/', uploadMultipleIfMultipart, createTermination);
|
||||||
router.get('/', getTerminations);
|
router.get('/', getTerminations);
|
||||||
router.get('/:id', getTerminationById);
|
router.get('/:id', getTerminationById);
|
||||||
router.put('/:id/status', updateTerminationStatus);
|
router.put('/:id/status', updateTerminationStatus);
|
||||||
|
|||||||
@ -108,6 +108,13 @@ const seedTemplates = async () => {
|
|||||||
fileName: 'resignation_approved.html',
|
fileName: 'resignation_approved.html',
|
||||||
placeholders: ['dealerName', 'resignationId', 'lwd']
|
placeholders: ['dealerName', 'resignationId', 'lwd']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
templateCode: 'RESIGNATION_LETTER_DISPATCHED_DEALER',
|
||||||
|
description: 'Notification sent to the dealer when the DD Admin dispatches the resignation acceptance letter (letter is delivered as an email attachment)',
|
||||||
|
subject: 'Resignation Acceptance Letter Dispatched: {{resignationId}}',
|
||||||
|
fileName: 'resignation_letter_dispatched_dealer.html',
|
||||||
|
placeholders: ['dealerName', 'resignationId', 'lwd', 'dispatchedBy', 'link', 'ctaLabel']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
templateCode: 'TERMINATION_INITIATED',
|
templateCode: 'TERMINATION_INITIATED',
|
||||||
description: 'Notification for new Termination request initiation',
|
description: 'Notification for new Termination request initiation',
|
||||||
|
|||||||
@ -137,12 +137,13 @@ export class ResignationWorkflowService {
|
|||||||
*/
|
*/
|
||||||
static calculateProgress(stage: string): number {
|
static calculateProgress(stage: string): number {
|
||||||
const progress: Record<string, number> = {
|
const progress: Record<string, number> = {
|
||||||
[RESIGNATION_STAGES.ASM]: 15,
|
[RESIGNATION_STAGES.ASM]: 12,
|
||||||
[RESIGNATION_STAGES.RBM]: 30,
|
[RESIGNATION_STAGES.RBM]: 25,
|
||||||
[RESIGNATION_STAGES.ZBH]: 40,
|
[RESIGNATION_STAGES.ZBH]: 38,
|
||||||
[RESIGNATION_STAGES.DD_LEAD]: 50,
|
[RESIGNATION_STAGES.DD_LEAD]: 50,
|
||||||
[RESIGNATION_STAGES.NBH]: 65,
|
[RESIGNATION_STAGES.DD_HEAD]: 60,
|
||||||
[RESIGNATION_STAGES.LEGAL]: 80,
|
[RESIGNATION_STAGES.NBH]: 70,
|
||||||
|
[RESIGNATION_STAGES.LEGAL]: 82,
|
||||||
[RESIGNATION_STAGES.DD_ADMIN]: 90,
|
[RESIGNATION_STAGES.DD_ADMIN]: 90,
|
||||||
[RESIGNATION_STAGES.AWAITING_FNF]: 92,
|
[RESIGNATION_STAGES.AWAITING_FNF]: 92,
|
||||||
[RESIGNATION_STAGES.FNF_INITIATED]: 95,
|
[RESIGNATION_STAGES.FNF_INITIATED]: 95,
|
||||||
@ -164,6 +165,7 @@ export class ResignationWorkflowService {
|
|||||||
[RESIGNATION_STAGES.RBM]: [ROLES.RBM, ROLES.DD_ZM],
|
[RESIGNATION_STAGES.RBM]: [ROLES.RBM, ROLES.DD_ZM],
|
||||||
[RESIGNATION_STAGES.ZBH]: ROLES.ZBH,
|
[RESIGNATION_STAGES.ZBH]: ROLES.ZBH,
|
||||||
[RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD,
|
[RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD,
|
||||||
|
[RESIGNATION_STAGES.DD_HEAD]: ROLES.DD_HEAD,
|
||||||
[RESIGNATION_STAGES.NBH]: ROLES.NBH,
|
[RESIGNATION_STAGES.NBH]: ROLES.NBH,
|
||||||
[RESIGNATION_STAGES.LEGAL]: ROLES.LEGAL_ADMIN,
|
[RESIGNATION_STAGES.LEGAL]: ROLES.LEGAL_ADMIN,
|
||||||
[RESIGNATION_STAGES.DD_ADMIN]: ROLES.DD_ADMIN,
|
[RESIGNATION_STAGES.DD_ADMIN]: ROLES.DD_ADMIN,
|
||||||
@ -180,7 +182,7 @@ export class ResignationWorkflowService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates the F&F settlement process for a resignation
|
* Initiates the F&F settlement process for a resignation
|
||||||
* SRS §4.2.2.8 — Standardized trigger mechanism
|
* SRS §4.2.2.9 — Standardized trigger mechanism
|
||||||
*/
|
*/
|
||||||
static async initiateFnF(resignation: any, userId: string, transaction: Transaction) {
|
static async initiateFnF(resignation: any, userId: string, transaction: Transaction) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -8,12 +8,12 @@ import { resolveRecipientsForRoles } from './slaGeographyResolver.js';
|
|||||||
import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.js';
|
import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.js';
|
||||||
import type { WorkflowActivityRequestType } from '../common/utils/workflowWorknote.js';
|
import type { WorkflowActivityRequestType } from '../common/utils/workflowWorknote.js';
|
||||||
import {
|
import {
|
||||||
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
|
|
||||||
claimSlaNotificationDispatch,
|
claimSlaNotificationDispatch,
|
||||||
SlaDispatchKeys,
|
SlaDispatchKeys,
|
||||||
updateSlaDispatchRecipientCount,
|
updateSlaDispatchRecipientCount,
|
||||||
wasSlaNotificationDispatched
|
wasSlaNotificationDispatched
|
||||||
} from './slaNotificationDispatch.service.js';
|
} from './slaNotificationDispatch.service.js';
|
||||||
|
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
|
||||||
|
|
||||||
export type SlaTrackRef = {
|
export type SlaTrackRef = {
|
||||||
entityType: string;
|
entityType: string;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const EMAILS = {
|
|||||||
DD_ZM: 'piyush@royalenfield.com',
|
DD_ZM: 'piyush@royalenfield.com',
|
||||||
ZBH: 'manav@royalenfield.com',
|
ZBH: 'manav@royalenfield.com',
|
||||||
DD_LEAD: 'jaya@royalenfield.com',
|
DD_LEAD: 'jaya@royalenfield.com',
|
||||||
|
DD_HEAD: 'ganesh@royalenfield.com',
|
||||||
NBH: 'yashwin@royalenfield.com',
|
NBH: 'yashwin@royalenfield.com',
|
||||||
LEGAL: 'legal@royalenfield.com',
|
LEGAL: 'legal@royalenfield.com',
|
||||||
FINANCE: 'finance@royalenfield.com',
|
FINANCE: 'finance@royalenfield.com',
|
||||||
@ -164,6 +165,7 @@ async function run() {
|
|||||||
{ stage: 'RBM + DD-ZM Review', name: 'DD-ZM', email: EMAILS.DD_ZM, remarks: 'Joint approval evaluated and verified by DD-ZM.' },
|
{ stage: 'RBM + DD-ZM Review', name: 'DD-ZM', email: EMAILS.DD_ZM, remarks: 'Joint approval evaluated and verified by DD-ZM.' },
|
||||||
{ stage: 'ZBH', name: 'ZBH', email: EMAILS.ZBH, remarks: 'Zone-level resource reallocation planned. Approved.' },
|
{ stage: 'ZBH', name: 'ZBH', email: EMAILS.ZBH, remarks: 'Zone-level resource reallocation planned. Approved.' },
|
||||||
{ stage: 'DD Lead', name: 'DD Lead', email: EMAILS.DD_LEAD, remarks: 'Dealer development criteria satisfied for exit.' },
|
{ stage: 'DD Lead', name: 'DD Lead', email: EMAILS.DD_LEAD, remarks: 'Dealer development criteria satisfied for exit.' },
|
||||||
|
{ stage: 'DD Head', name: 'DD Head', email: EMAILS.DD_HEAD, remarks: 'Dealer development head sign-off recorded. Recommended for NBH.' },
|
||||||
{ stage: 'NBH', name: 'NBH', email: EMAILS.NBH, remarks: 'HQ clearance granted. Proceed with F&F initiation.' },
|
{ stage: 'NBH', name: 'NBH', email: EMAILS.NBH, remarks: 'HQ clearance granted. Proceed with F&F initiation.' },
|
||||||
{ stage: 'Legal', name: 'Legal Admin', email: EMAILS.LEGAL, remarks: 'Legal documentation verified. No active litigation found.' },
|
{ stage: 'Legal', name: 'Legal Admin', email: EMAILS.LEGAL, remarks: 'Legal documentation verified. No active litigation found.' },
|
||||||
{ stage: 'DD Admin', name: 'DD Admin', email: EMAILS.DD_ADMIN, remarks: 'Administrative checks complete. Initiating termination of commercial contract.' }
|
{ stage: 'DD Admin', name: 'DD Admin', email: EMAILS.DD_ADMIN, remarks: 'Administrative checks complete. Initiating termination of commercial contract.' }
|
||||||
@ -176,7 +178,7 @@ async function run() {
|
|||||||
console.log(`Current Stage: ${currentStage}`);
|
console.log(`Current Stage: ${currentStage}`);
|
||||||
|
|
||||||
const stageOrder = [
|
const stageOrder = [
|
||||||
'Request Submitted', 'ASM', 'RBM + DD-ZM Review', 'ZBH', 'DD Lead', 'NBH', 'Legal', 'DD Admin', 'F&F Initiated', 'Completed'
|
'Request Submitted', 'ASM', 'RBM + DD-ZM Review', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal', 'DD Admin', 'F&F Initiated', 'Completed'
|
||||||
];
|
];
|
||||||
|
|
||||||
let startStageIndex = stageOrder.indexOf(currentStage) === -1 ? 0 : stageOrder.indexOf(currentStage);
|
let startStageIndex = stageOrder.indexOf(currentStage) === -1 ? 0 : stageOrder.indexOf(currentStage);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const EMAILS = {
|
|||||||
RBM_L1: 'manish@royalenfield.com',
|
RBM_L1: 'manish@royalenfield.com',
|
||||||
ZM_L1: 'piyush@royalenfield.com',
|
ZM_L1: 'piyush@royalenfield.com',
|
||||||
DD_LEAD: 'jaya@royalenfield.com',
|
DD_LEAD: 'jaya@royalenfield.com',
|
||||||
ZBH: 'laxmanhalaki814@royalenfield.com',
|
ZBH: 'laxmanhalaki814@gmail.com',
|
||||||
NBH: 'yashwin@royalenfield.com',
|
NBH: 'yashwin@royalenfield.com',
|
||||||
DD_HEAD: 'ganesh@royalenfield.com',
|
DD_HEAD: 'ganesh@royalenfield.com',
|
||||||
FDD: 'fdd@royalenfield.com',
|
FDD: 'fdd@royalenfield.com',
|
||||||
@ -252,141 +252,141 @@ async function triggerWorkflow() {
|
|||||||
await delay();
|
await delay();
|
||||||
|
|
||||||
// 5. LEVEL-2 INTERVIEW
|
// 5. LEVEL-2 INTERVIEW
|
||||||
// log(5, 'Scheduling Level 2 Interview...');
|
log(5, 'Scheduling Level 2 Interview...');
|
||||||
|
|
||||||
// const intv2Response = await apiRequest('/assessment/interviews', 'POST', {
|
const intv2Response = await apiRequest('/assessment/interviews', 'POST', {
|
||||||
// applicationId: applicationUUID,
|
applicationId: applicationUUID,
|
||||||
// level: 2,
|
level: 2,
|
||||||
// scheduledAt: new Date(Date.now() + 172800000).toISOString(),
|
scheduledAt: new Date(Date.now() + 172800000).toISOString(),
|
||||||
// type: 'Online',
|
type: 'Online',
|
||||||
// location: 'Teams',
|
location: 'Teams',
|
||||||
// participants: [ddLead.id, zbhUser.id]
|
participants: [ddLead.id, zbhUser.id]
|
||||||
// }, leadToken);
|
}, leadToken);
|
||||||
// const interviewId2 = intv2Response.data.id;
|
const interviewId2 = intv2Response.data.id;
|
||||||
|
|
||||||
// log(5.1, 'DD-Lead Giving Feedback...');
|
log(5.1, 'DD-Lead Giving Feedback...');
|
||||||
// await apiRequest('/assessment/level2-feedback', 'POST', {
|
await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||||
// interviewId: interviewId2,
|
interviewId: interviewId2,
|
||||||
// overallScore: 9.5,
|
overallScore: 9.5,
|
||||||
// feedbackItems: [
|
feedbackItems: [
|
||||||
// { type: 'Strategic Vision', comments: 'Excellent strategic planning.' },
|
{ type: 'Strategic Vision', comments: 'Excellent strategic planning.' },
|
||||||
// { type: 'Management Capabilities', comments: 'Strong team leadership.' },
|
{ type: 'Management Capabilities', comments: 'Strong team leadership.' },
|
||||||
// { type: 'Operational Understanding', comments: 'Knows the local market well.' }
|
{ type: 'Operational Understanding', comments: 'Knows the local market well.' }
|
||||||
// ],
|
],
|
||||||
// recommendation: 'Selected'
|
recommendation: 'Selected'
|
||||||
// }, leadToken);
|
}, leadToken);
|
||||||
|
|
||||||
// log(5.15, 'ZBH Giving Feedback...');
|
log(5.15, 'ZBH Giving Feedback...');
|
||||||
// const zbhToken = await login(zbhUser.email);
|
const zbhToken = await login(zbhUser.email);
|
||||||
// await apiRequest('/assessment/level2-feedback', 'POST', {
|
await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||||
// interviewId: interviewId2,
|
interviewId: interviewId2,
|
||||||
// overallScore: 9.0,
|
overallScore: 9.0,
|
||||||
// feedbackItems: [
|
feedbackItems: [
|
||||||
// { type: 'Strategic Vision', comments: 'Good alignment with brand.' },
|
{ type: 'Strategic Vision', comments: 'Good alignment with brand.' },
|
||||||
// { type: 'Key Strengths', comments: 'Great location proposed.' },
|
{ type: 'Key Strengths', comments: 'Great location proposed.' },
|
||||||
// { type: 'Areas of Concern', comments: 'None at this time.' }
|
{ type: 'Areas of Concern', comments: 'None at this time.' }
|
||||||
// ],
|
],
|
||||||
// recommendation: 'Selected'
|
recommendation: 'Selected'
|
||||||
// }, zbhToken);
|
}, zbhToken);
|
||||||
|
|
||||||
// log(5.2, 'DD-Lead Finalizing Level 2 Decision...');
|
log(5.2, 'DD-Lead Finalizing Level 2 Decision...');
|
||||||
// await apiRequest('/assessment/decision', 'POST', {
|
await apiRequest('/assessment/decision', 'POST', {
|
||||||
// interviewId: interviewId2,
|
interviewId: interviewId2,
|
||||||
// decision: 'Approved',
|
decision: 'Approved',
|
||||||
// remarks: 'Cleared Level 2'
|
remarks: 'Cleared Level 2'
|
||||||
// }, leadToken);
|
}, leadToken);
|
||||||
// log(5, 'Level 2 Complete.');
|
log(5, 'Level 2 Complete.');
|
||||||
// await delay();
|
await delay();
|
||||||
|
|
||||||
// // 6. LEVEL-3 INTERVIEW
|
// 6. LEVEL-3 INTERVIEW
|
||||||
// log(6, 'Scheduling Level 3 Interview...');
|
log(6, 'Scheduling Level 3 Interview...');
|
||||||
// const headUser = users.data.find(u => u.email === EMAILS.DD_HEAD);
|
const headUser = users.data.find(u => u.email === EMAILS.DD_HEAD);
|
||||||
// const nbhUser = users.data.find(u => u.email === EMAILS.NBH);
|
const nbhUser = users.data.find(u => u.email === EMAILS.NBH);
|
||||||
|
|
||||||
// const intv3Response = await apiRequest('/assessment/interviews', 'POST', {
|
const intv3Response = await apiRequest('/assessment/interviews', 'POST', {
|
||||||
// applicationId: applicationUUID,
|
applicationId: applicationUUID,
|
||||||
// level: 3,
|
level: 3,
|
||||||
// scheduledAt: new Date(Date.now() + 259200000).toISOString(),
|
scheduledAt: new Date(Date.now() + 259200000).toISOString(),
|
||||||
// type: 'In-Person',
|
type: 'In-Person',
|
||||||
// location: 'HO',
|
location: 'HO',
|
||||||
// participants: [headUser.id, nbhUser.id]
|
participants: [headUser.id, nbhUser.id]
|
||||||
// }, leadToken);
|
}, leadToken);
|
||||||
// const interviewId3 = intv3Response.data.id;
|
const interviewId3 = intv3Response.data.id;
|
||||||
|
|
||||||
// log(6.1, 'NBH Giving Feedback...');
|
log(6.1, 'NBH Giving Feedback...');
|
||||||
// const nbhToken = await login(EMAILS.NBH);
|
const nbhToken = await login(EMAILS.NBH);
|
||||||
// await apiRequest('/assessment/level2-feedback', 'POST', {
|
await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||||
// interviewId: interviewId3,
|
interviewId: interviewId3,
|
||||||
// overallScore: 10,
|
overallScore: 10,
|
||||||
// feedbackItems: [
|
feedbackItems: [
|
||||||
// { type: 'Business Vision & Strategy', comments: 'Highly recommended for this market.' },
|
{ type: 'Business Vision & Strategy', comments: 'Highly recommended for this market.' },
|
||||||
// { type: 'Leadership & Decision Making', comments: 'Shows great potential.' }
|
{ type: 'Leadership & Decision Making', comments: 'Shows great potential.' }
|
||||||
// ],
|
],
|
||||||
// recommendation: 'Selected'
|
recommendation: 'Selected'
|
||||||
// }, nbhToken);
|
}, nbhToken);
|
||||||
|
|
||||||
// log(6.15, 'DD-Head Giving Feedback...');
|
log(6.15, 'DD-Head Giving Feedback...');
|
||||||
// const headToken = await login(EMAILS.DD_HEAD);
|
const headToken = await login(EMAILS.DD_HEAD);
|
||||||
// await apiRequest('/assessment/level2-feedback', 'POST', {
|
await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||||
// interviewId: interviewId3,
|
interviewId: interviewId3,
|
||||||
// overallScore: 9.5,
|
overallScore: 9.5,
|
||||||
// feedbackItems: [
|
feedbackItems: [
|
||||||
// { type: 'Operational & Financial Readiness', comments: 'Financially sound.' },
|
{ type: 'Operational & Financial Readiness', comments: 'Financially sound.' },
|
||||||
// { type: 'Brand Alignment', comments: 'Understands Royal Enfield ethos perfectly.' }
|
{ type: 'Brand Alignment', comments: 'Understands Royal Enfield ethos perfectly.' }
|
||||||
// ],
|
],
|
||||||
// recommendation: 'Selected'
|
recommendation: 'Selected'
|
||||||
// }, headToken);
|
}, headToken);
|
||||||
|
|
||||||
// log(6.2, 'Head Finalizing Level 3 Decision...');
|
log(6.2, 'Head Finalizing Level 3 Decision...');
|
||||||
// await apiRequest('/assessment/decision', 'POST', {
|
await apiRequest('/assessment/decision', 'POST', {
|
||||||
// interviewId: interviewId3,
|
interviewId: interviewId3,
|
||||||
// decision: 'Approved',
|
decision: 'Approved',
|
||||||
// remarks: 'Cleared Level 3. Moving to FDD.'
|
remarks: 'Cleared Level 3. Moving to FDD.'
|
||||||
// }, headToken);
|
}, headToken);
|
||||||
// log(6, 'Level 3 Complete. Stage is now FDD Verification.');
|
log(6, 'Level 3 Complete. Stage is now FDD Verification.');
|
||||||
// await delay();
|
await delay();
|
||||||
|
|
||||||
// // 6.3 FDD ASSIGNMENT
|
// 6.3 FDD ASSIGNMENT
|
||||||
// log(6.3, 'Admin Assigning Application to FDD Agency...');
|
log(6.3, 'Admin Assigning Application to FDD Agency...');
|
||||||
// const fddUser = users.data.find(u => u.email === EMAILS.FDD);
|
const fddUser = users.data.find(u => u.email === EMAILS.FDD);
|
||||||
// await apiRequest('/fdd/assign', 'POST', {
|
await apiRequest('/fdd/assign', 'POST', {
|
||||||
// applicationId: applicationUUID,
|
applicationId: applicationUUID,
|
||||||
// assignedToAgency: fddUser.id
|
assignedToAgency: fddUser.id
|
||||||
// }, adminToken);
|
}, adminToken);
|
||||||
// log(6.3, 'FDD Agency assigned successfully.');
|
log(6.3, 'FDD Agency assigned successfully.');
|
||||||
// await delay();
|
await delay();
|
||||||
|
|
||||||
// // 7. FDD MILESTONE
|
// 7. FDD MILESTONE
|
||||||
// log(7, 'FDD Agency Discovery & Report Upload...');
|
log(7, 'FDD Agency Discovery & Report Upload...');
|
||||||
// const fddToken = await login(EMAILS.FDD);
|
const fddToken = await login(EMAILS.FDD);
|
||||||
|
|
||||||
// // FETCH ASSIGNMENT ID
|
// FETCH ASSIGNMENT ID
|
||||||
// const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
|
const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
|
||||||
// const assignmentId = assignmentRes.data.id;
|
const assignmentId = assignmentRes.data.id;
|
||||||
// log(7, `Found Assignment ID: ${assignmentId}`);
|
log(7, `Found Assignment ID: ${assignmentId}`);
|
||||||
|
|
||||||
// await apiRequest('/fdd/report', 'POST', {
|
await apiRequest('/fdd/report', 'POST', {
|
||||||
// assignmentId,
|
assignmentId,
|
||||||
// findings: 'Finance records clean.',
|
findings: 'Finance records clean.',
|
||||||
// recommendation: 'Approved'
|
recommendation: 'Approved'
|
||||||
// }, fddToken);
|
}, fddToken);
|
||||||
|
|
||||||
// log(7.1, 'Admin Approving FDD Final Stage...');
|
log(7.1, 'Admin Approving FDD Final Stage...');
|
||||||
// await apiRequest('/assessment/stage-decision', 'POST', {
|
await apiRequest('/assessment/stage-decision', 'POST', {
|
||||||
// applicationId: applicationUUID,
|
applicationId: applicationUUID,
|
||||||
// stageCode: 'FDD_VERIFICATION',
|
stageCode: 'FDD_VERIFICATION',
|
||||||
// decision: 'Approved',
|
decision: 'Approved',
|
||||||
// remarks: 'FDD documents verified.'
|
remarks: 'FDD documents verified.'
|
||||||
// }, adminToken);
|
}, adminToken);
|
||||||
// log(7, 'FDD Milestone Complete.');
|
log(7, 'FDD Milestone Complete.');
|
||||||
// await delay();
|
await delay();
|
||||||
|
|
||||||
// log(7.4, 'Uploading mandatory documents prior to LOI generation...');
|
log(7.4, 'Uploading mandatory documents prior to LOI generation...');
|
||||||
// const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
|
const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
|
||||||
// for (const doc of requiredDocs) {
|
for (const doc of requiredDocs) {
|
||||||
// await mockUploadDocument(applicationUUID, adminToken, doc);
|
await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||||
// }
|
}
|
||||||
// await delay(1000);
|
await delay(1000);
|
||||||
|
|
||||||
// // 7.5 LOI APPROVAL
|
// // 7.5 LOI APPROVAL
|
||||||
// log(7.5, 'LOI Generation & Approval...');
|
// log(7.5, 'LOI Generation & Approval...');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user