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>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Royal Enfield Onboarding</title>
|
||||
<script type="module" crossorigin src="/assets/index-ny6fNePT.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index--4WsqmvE.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Royal Enfield Onboarding</title>
|
||||
<script type="module" crossorigin src="/assets/index-KxZzWQFD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BvWiaLmW.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -229,14 +229,30 @@ mandatory remarks captured for traceability.
|
||||
- The **DD-Lead** consolidates all discussions, documents, and feedback.
|
||||
- Prepares a **Resignation Presentation** with recommendations and supporting data.
|
||||
- Uploads the presentation to the portal.
|
||||
- Forwards the case to **NBH** for final decision.
|
||||
- The resignation request is reviewed by the **DD-Lead and DD-Head**. At this stage, both
|
||||
roles are authorized to **Send Back or Revoke** the resignation request for clarification,
|
||||
correction, or reconsideration. **Send Back actions are communicated through Work**
|
||||
**Notes** , with **mandatory remarks** recorded for audit and traceability.
|
||||
- Forwards the case to **DD-Head** for the next-level Dealer Development review.
|
||||
- At this stage, the DD-Lead is authorized to **Send Back or Revoke** the resignation
|
||||
request for clarification, correction, or reconsideration. **Send Back actions are**
|
||||
**communicated through Work Notes** , with **mandatory remarks** recorded for audit and
|
||||
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.
|
||||
- Adds final remarks with one of the following outcomes:
|
||||
@ -251,7 +267,7 @@ mandatory remarks captured for traceability.
|
||||
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**.
|
||||
- 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
|
||||
dealer.
|
||||
|
||||
@ -334,7 +334,7 @@ describe('Resignation Revoke — Dealer notified on terminal event', () => {
|
||||
|
||||
// ─── 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', () => {
|
||||
const lwd = new Date('2026-01-01'); // in the past
|
||||
const today = new Date();
|
||||
|
||||
@ -200,6 +200,7 @@ export const RESIGNATION_STAGES = {
|
||||
RBM: 'RBM + DD-ZM Review',
|
||||
ZBH: 'ZBH',
|
||||
DD_LEAD: 'DD Lead',
|
||||
DD_HEAD: 'DD Head',
|
||||
NBH: 'NBH',
|
||||
DD_ADMIN: 'DD Admin',
|
||||
/** 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_REVOKED: 'RESIGNATION_REVOKED',
|
||||
RESIGNATION_SENT_BACK: 'RESIGNATION_SENT_BACK',
|
||||
RESIGNATION_LETTER_DISPATCHED: 'RESIGNATION_LETTER_DISPATCHED',
|
||||
TERMINATION_REVOKED: 'TERMINATION_REVOKED',
|
||||
TERMINATION_SENT_BACK: 'TERMINATION_SENT_BACK',
|
||||
RELOCATION_SENT_BACK: 'RELOCATION_SENT_BACK',
|
||||
@ -538,6 +540,7 @@ export const RESIGNATION_DOCUMENT_STAGES = [
|
||||
] as const;
|
||||
|
||||
export const TERMINATION_DOCUMENT_TYPES = [
|
||||
'Presentation',
|
||||
'Termination Recommendation',
|
||||
'Show Cause Notice',
|
||||
'SCN Response',
|
||||
|
||||
@ -108,6 +108,7 @@ const RESIGNATION_SLA: SlaCatalogEntry[] = [
|
||||
d('RESIGNATION', RESIGNATION_STAGES.RBM, 'RBM, DD-ZM', 3),
|
||||
d('RESIGNATION', RESIGNATION_STAGES.ZBH, 'ZBH', 3),
|
||||
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.LEGAL, 'Legal Admin', 7),
|
||||
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
|
||||
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
|
||||
export const handleUploadError = (err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
|
||||
@ -119,11 +119,21 @@ async function resolveEmailTemplate(templateCode: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface SendEmailOptions {
|
||||
attachments?: Array<{
|
||||
filename: string;
|
||||
path?: string;
|
||||
content?: Buffer | string;
|
||||
contentType?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const sendEmail = async (
|
||||
to: string,
|
||||
subject: string,
|
||||
templateCode: string,
|
||||
replacements: Record<string, string>
|
||||
replacements: Record<string, string>,
|
||||
options?: SendEmailOptions
|
||||
) => {
|
||||
try {
|
||||
let finalHtml = '';
|
||||
@ -181,7 +191,8 @@ export const sendEmail = async (
|
||||
from: fromAddress,
|
||||
to,
|
||||
subject: finalSubject,
|
||||
html: finalHtml
|
||||
html: finalHtml,
|
||||
...(options?.attachments?.length ? { attachments: options.attachments } : {})
|
||||
};
|
||||
|
||||
const info = await readyTransporter.sendMail(mail);
|
||||
|
||||
@ -27,6 +27,7 @@ export const getResignationStatusForStage = (stage: string): string => {
|
||||
case RESIGNATION_STAGES.ASM:
|
||||
case RESIGNATION_STAGES.ZBH:
|
||||
case RESIGNATION_STAGES.DD_LEAD:
|
||||
case RESIGNATION_STAGES.DD_HEAD:
|
||||
case RESIGNATION_STAGES.NBH:
|
||||
case RESIGNATION_STAGES.DD_ADMIN:
|
||||
return `${stage} Review`;
|
||||
|
||||
@ -38,7 +38,8 @@ export const getPreviousStage = (requestType: string, currentStage: string): str
|
||||
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ASM,
|
||||
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.RBM,
|
||||
[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.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
|
||||
[RESIGNATION_STAGES.AWAITING_FNF]: RESIGNATION_STAGES.DD_ADMIN,
|
||||
|
||||
@ -3,7 +3,6 @@ import { Op } from 'sequelize';
|
||||
import { sendEmail } from './email.service.js';
|
||||
import { NotificationService } from '../../services/NotificationService.js';
|
||||
import {
|
||||
import { getFrontendBaseUrl } from './frontendUrl.js';
|
||||
APPLICATION_STAGES,
|
||||
TERMINATION_STAGES,
|
||||
CONSTITUTIONAL_STAGES,
|
||||
@ -11,6 +10,7 @@ import { getFrontendBaseUrl } from './frontendUrl.js';
|
||||
REQUEST_TYPES,
|
||||
ROLES
|
||||
} from '../config/constants.js';
|
||||
import { getFrontendBaseUrl } from './frontendUrl.js';
|
||||
|
||||
const { RequestParticipant, User, Outlet, District, Dealer } = db;
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ export const ALLOWED_EMAIL_TEMPLATE_CODES = [
|
||||
'RESIGNATION_RECEIVED',
|
||||
'RESIGNATION_SUBMITTED',
|
||||
'RESIGNATION_UPDATE',
|
||||
'RESIGNATION_LETTER_DISPATCHED_DEALER',
|
||||
'SLA_BREACH_WARNING',
|
||||
'STATUTORY_DOCUMENT_REQUEST',
|
||||
'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 { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.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
|
||||
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.RBM]: RESIGNATION_STAGES.ZBH,
|
||||
[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.LEGAL]: RESIGNATION_STAGES.DD_ADMIN,
|
||||
// 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.
|
||||
// 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;
|
||||
if (nextStage === RESIGNATION_STAGES.FNF_INITIATED && targetOverride) {
|
||||
const lwdString = resignation.lastOperationalDateServices || resignation.lastOperationalDateSales;
|
||||
@ -1098,6 +1102,9 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n
|
||||
case 'assign':
|
||||
return assignResignation(req, res, next);
|
||||
|
||||
case 'dispatch':
|
||||
return dispatchResignationLetter(req, res, next);
|
||||
|
||||
default:
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@ -1109,3 +1116,247 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n
|
||||
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.put('/: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.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 { NotificationService } from '../../services/NotificationService.js';
|
||||
import { getFrontendBaseUrl } from '../../common/utils/frontendUrl.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const portalBase = getFrontendBaseUrl();
|
||||
|
||||
@ -89,31 +90,64 @@ export const uploadFnFDocument = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const fileUrl = req.file ? `/uploads/documents/${req.file.filename}` : req.body.fileUrl;
|
||||
|
||||
|
||||
if (!fileUrl) {
|
||||
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);
|
||||
if (!fnf) {
|
||||
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 = [
|
||||
...(fnf.clearanceDocuments || []),
|
||||
{
|
||||
name: req.file?.originalname || 'Uploaded Document',
|
||||
supportingDocument: fileUrl,
|
||||
department: 'Finance', // Default to Finance for generic F&F docs
|
||||
clearedAt: new Date().toISOString()
|
||||
}
|
||||
newEntry
|
||||
];
|
||||
|
||||
await fnf.update({
|
||||
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) {
|
||||
console.error('Error uploading F&F document:', error);
|
||||
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 requestId = await NomenclatureService.generateTerminationId();
|
||||
@ -81,6 +115,33 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N
|
||||
remarks: 'Admin initiated termination request'
|
||||
}, { 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();
|
||||
|
||||
// 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;
|
||||
let approvedToStage: string | null = null;
|
||||
let scheduleLwdFnfReminder = false;
|
||||
|
||||
@ -5,11 +5,11 @@ import {
|
||||
submitScnResponse, recordPersonalHearing, updateClearance, uploadTerminationDocument, issueScn, uploadScnResponse, finalizeTermination
|
||||
} from './termination.controller.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.post('/', createTermination);
|
||||
router.post('/', uploadMultipleIfMultipart, createTermination);
|
||||
router.get('/', getTerminations);
|
||||
router.get('/:id', getTerminationById);
|
||||
router.put('/:id/status', updateTerminationStatus);
|
||||
|
||||
@ -108,6 +108,13 @@ const seedTemplates = async () => {
|
||||
fileName: 'resignation_approved.html',
|
||||
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',
|
||||
description: 'Notification for new Termination request initiation',
|
||||
|
||||
@ -137,12 +137,13 @@ export class ResignationWorkflowService {
|
||||
*/
|
||||
static calculateProgress(stage: string): number {
|
||||
const progress: Record<string, number> = {
|
||||
[RESIGNATION_STAGES.ASM]: 15,
|
||||
[RESIGNATION_STAGES.RBM]: 30,
|
||||
[RESIGNATION_STAGES.ZBH]: 40,
|
||||
[RESIGNATION_STAGES.ASM]: 12,
|
||||
[RESIGNATION_STAGES.RBM]: 25,
|
||||
[RESIGNATION_STAGES.ZBH]: 38,
|
||||
[RESIGNATION_STAGES.DD_LEAD]: 50,
|
||||
[RESIGNATION_STAGES.NBH]: 65,
|
||||
[RESIGNATION_STAGES.LEGAL]: 80,
|
||||
[RESIGNATION_STAGES.DD_HEAD]: 60,
|
||||
[RESIGNATION_STAGES.NBH]: 70,
|
||||
[RESIGNATION_STAGES.LEGAL]: 82,
|
||||
[RESIGNATION_STAGES.DD_ADMIN]: 90,
|
||||
[RESIGNATION_STAGES.AWAITING_FNF]: 92,
|
||||
[RESIGNATION_STAGES.FNF_INITIATED]: 95,
|
||||
@ -164,6 +165,7 @@ export class ResignationWorkflowService {
|
||||
[RESIGNATION_STAGES.RBM]: [ROLES.RBM, ROLES.DD_ZM],
|
||||
[RESIGNATION_STAGES.ZBH]: ROLES.ZBH,
|
||||
[RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD,
|
||||
[RESIGNATION_STAGES.DD_HEAD]: ROLES.DD_HEAD,
|
||||
[RESIGNATION_STAGES.NBH]: ROLES.NBH,
|
||||
[RESIGNATION_STAGES.LEGAL]: ROLES.LEGAL_ADMIN,
|
||||
[RESIGNATION_STAGES.DD_ADMIN]: ROLES.DD_ADMIN,
|
||||
@ -180,7 +182,7 @@ export class ResignationWorkflowService {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
|
||||
@ -8,12 +8,12 @@ import { resolveRecipientsForRoles } from './slaGeographyResolver.js';
|
||||
import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.js';
|
||||
import type { WorkflowActivityRequestType } from '../common/utils/workflowWorknote.js';
|
||||
import {
|
||||
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
|
||||
claimSlaNotificationDispatch,
|
||||
SlaDispatchKeys,
|
||||
updateSlaDispatchRecipientCount,
|
||||
wasSlaNotificationDispatched
|
||||
} from './slaNotificationDispatch.service.js';
|
||||
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';
|
||||
|
||||
export type SlaTrackRef = {
|
||||
entityType: string;
|
||||
|
||||
@ -17,6 +17,7 @@ const EMAILS = {
|
||||
DD_ZM: 'piyush@royalenfield.com',
|
||||
ZBH: 'manav@royalenfield.com',
|
||||
DD_LEAD: 'jaya@royalenfield.com',
|
||||
DD_HEAD: 'ganesh@royalenfield.com',
|
||||
NBH: 'yashwin@royalenfield.com',
|
||||
LEGAL: 'legal@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: '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 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: '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.' }
|
||||
@ -176,7 +178,7 @@ async function run() {
|
||||
console.log(`Current Stage: ${currentStage}`);
|
||||
|
||||
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);
|
||||
|
||||
@ -23,7 +23,7 @@ const EMAILS = {
|
||||
RBM_L1: 'manish@royalenfield.com',
|
||||
ZM_L1: 'piyush@royalenfield.com',
|
||||
DD_LEAD: 'jaya@royalenfield.com',
|
||||
ZBH: 'laxmanhalaki814@royalenfield.com',
|
||||
ZBH: 'laxmanhalaki814@gmail.com',
|
||||
NBH: 'yashwin@royalenfield.com',
|
||||
DD_HEAD: 'ganesh@royalenfield.com',
|
||||
FDD: 'fdd@royalenfield.com',
|
||||
@ -252,141 +252,141 @@ async function triggerWorkflow() {
|
||||
await delay();
|
||||
|
||||
// 5. LEVEL-2 INTERVIEW
|
||||
// log(5, 'Scheduling Level 2 Interview...');
|
||||
log(5, 'Scheduling Level 2 Interview...');
|
||||
|
||||
// const intv2Response = await apiRequest('/assessment/interviews', 'POST', {
|
||||
// applicationId: applicationUUID,
|
||||
// level: 2,
|
||||
// scheduledAt: new Date(Date.now() + 172800000).toISOString(),
|
||||
// type: 'Online',
|
||||
// location: 'Teams',
|
||||
// participants: [ddLead.id, zbhUser.id]
|
||||
// }, leadToken);
|
||||
// const interviewId2 = intv2Response.data.id;
|
||||
const intv2Response = await apiRequest('/assessment/interviews', 'POST', {
|
||||
applicationId: applicationUUID,
|
||||
level: 2,
|
||||
scheduledAt: new Date(Date.now() + 172800000).toISOString(),
|
||||
type: 'Online',
|
||||
location: 'Teams',
|
||||
participants: [ddLead.id, zbhUser.id]
|
||||
}, leadToken);
|
||||
const interviewId2 = intv2Response.data.id;
|
||||
|
||||
// log(5.1, 'DD-Lead Giving Feedback...');
|
||||
// await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||
// interviewId: interviewId2,
|
||||
// overallScore: 9.5,
|
||||
// feedbackItems: [
|
||||
// { type: 'Strategic Vision', comments: 'Excellent strategic planning.' },
|
||||
// { type: 'Management Capabilities', comments: 'Strong team leadership.' },
|
||||
// { type: 'Operational Understanding', comments: 'Knows the local market well.' }
|
||||
// ],
|
||||
// recommendation: 'Selected'
|
||||
// }, leadToken);
|
||||
log(5.1, 'DD-Lead Giving Feedback...');
|
||||
await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||
interviewId: interviewId2,
|
||||
overallScore: 9.5,
|
||||
feedbackItems: [
|
||||
{ type: 'Strategic Vision', comments: 'Excellent strategic planning.' },
|
||||
{ type: 'Management Capabilities', comments: 'Strong team leadership.' },
|
||||
{ type: 'Operational Understanding', comments: 'Knows the local market well.' }
|
||||
],
|
||||
recommendation: 'Selected'
|
||||
}, leadToken);
|
||||
|
||||
// log(5.15, 'ZBH Giving Feedback...');
|
||||
// const zbhToken = await login(zbhUser.email);
|
||||
// await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||
// interviewId: interviewId2,
|
||||
// overallScore: 9.0,
|
||||
// feedbackItems: [
|
||||
// { type: 'Strategic Vision', comments: 'Good alignment with brand.' },
|
||||
// { type: 'Key Strengths', comments: 'Great location proposed.' },
|
||||
// { type: 'Areas of Concern', comments: 'None at this time.' }
|
||||
// ],
|
||||
// recommendation: 'Selected'
|
||||
// }, zbhToken);
|
||||
log(5.15, 'ZBH Giving Feedback...');
|
||||
const zbhToken = await login(zbhUser.email);
|
||||
await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||
interviewId: interviewId2,
|
||||
overallScore: 9.0,
|
||||
feedbackItems: [
|
||||
{ type: 'Strategic Vision', comments: 'Good alignment with brand.' },
|
||||
{ type: 'Key Strengths', comments: 'Great location proposed.' },
|
||||
{ type: 'Areas of Concern', comments: 'None at this time.' }
|
||||
],
|
||||
recommendation: 'Selected'
|
||||
}, zbhToken);
|
||||
|
||||
// log(5.2, 'DD-Lead Finalizing Level 2 Decision...');
|
||||
// await apiRequest('/assessment/decision', 'POST', {
|
||||
// interviewId: interviewId2,
|
||||
// decision: 'Approved',
|
||||
// remarks: 'Cleared Level 2'
|
||||
// }, leadToken);
|
||||
// log(5, 'Level 2 Complete.');
|
||||
// await delay();
|
||||
log(5.2, 'DD-Lead Finalizing Level 2 Decision...');
|
||||
await apiRequest('/assessment/decision', 'POST', {
|
||||
interviewId: interviewId2,
|
||||
decision: 'Approved',
|
||||
remarks: 'Cleared Level 2'
|
||||
}, leadToken);
|
||||
log(5, 'Level 2 Complete.');
|
||||
await delay();
|
||||
|
||||
// // 6. LEVEL-3 INTERVIEW
|
||||
// log(6, 'Scheduling Level 3 Interview...');
|
||||
// const headUser = users.data.find(u => u.email === EMAILS.DD_HEAD);
|
||||
// const nbhUser = users.data.find(u => u.email === EMAILS.NBH);
|
||||
// 6. LEVEL-3 INTERVIEW
|
||||
log(6, 'Scheduling Level 3 Interview...');
|
||||
const headUser = users.data.find(u => u.email === EMAILS.DD_HEAD);
|
||||
const nbhUser = users.data.find(u => u.email === EMAILS.NBH);
|
||||
|
||||
// const intv3Response = await apiRequest('/assessment/interviews', 'POST', {
|
||||
// applicationId: applicationUUID,
|
||||
// level: 3,
|
||||
// scheduledAt: new Date(Date.now() + 259200000).toISOString(),
|
||||
// type: 'In-Person',
|
||||
// location: 'HO',
|
||||
// participants: [headUser.id, nbhUser.id]
|
||||
// }, leadToken);
|
||||
// const interviewId3 = intv3Response.data.id;
|
||||
const intv3Response = await apiRequest('/assessment/interviews', 'POST', {
|
||||
applicationId: applicationUUID,
|
||||
level: 3,
|
||||
scheduledAt: new Date(Date.now() + 259200000).toISOString(),
|
||||
type: 'In-Person',
|
||||
location: 'HO',
|
||||
participants: [headUser.id, nbhUser.id]
|
||||
}, leadToken);
|
||||
const interviewId3 = intv3Response.data.id;
|
||||
|
||||
// log(6.1, 'NBH Giving Feedback...');
|
||||
// const nbhToken = await login(EMAILS.NBH);
|
||||
// await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||
// interviewId: interviewId3,
|
||||
// overallScore: 10,
|
||||
// feedbackItems: [
|
||||
// { type: 'Business Vision & Strategy', comments: 'Highly recommended for this market.' },
|
||||
// { type: 'Leadership & Decision Making', comments: 'Shows great potential.' }
|
||||
// ],
|
||||
// recommendation: 'Selected'
|
||||
// }, nbhToken);
|
||||
log(6.1, 'NBH Giving Feedback...');
|
||||
const nbhToken = await login(EMAILS.NBH);
|
||||
await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||
interviewId: interviewId3,
|
||||
overallScore: 10,
|
||||
feedbackItems: [
|
||||
{ type: 'Business Vision & Strategy', comments: 'Highly recommended for this market.' },
|
||||
{ type: 'Leadership & Decision Making', comments: 'Shows great potential.' }
|
||||
],
|
||||
recommendation: 'Selected'
|
||||
}, nbhToken);
|
||||
|
||||
// log(6.15, 'DD-Head Giving Feedback...');
|
||||
// const headToken = await login(EMAILS.DD_HEAD);
|
||||
// await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||
// interviewId: interviewId3,
|
||||
// overallScore: 9.5,
|
||||
// feedbackItems: [
|
||||
// { type: 'Operational & Financial Readiness', comments: 'Financially sound.' },
|
||||
// { type: 'Brand Alignment', comments: 'Understands Royal Enfield ethos perfectly.' }
|
||||
// ],
|
||||
// recommendation: 'Selected'
|
||||
// }, headToken);
|
||||
log(6.15, 'DD-Head Giving Feedback...');
|
||||
const headToken = await login(EMAILS.DD_HEAD);
|
||||
await apiRequest('/assessment/level2-feedback', 'POST', {
|
||||
interviewId: interviewId3,
|
||||
overallScore: 9.5,
|
||||
feedbackItems: [
|
||||
{ type: 'Operational & Financial Readiness', comments: 'Financially sound.' },
|
||||
{ type: 'Brand Alignment', comments: 'Understands Royal Enfield ethos perfectly.' }
|
||||
],
|
||||
recommendation: 'Selected'
|
||||
}, headToken);
|
||||
|
||||
// log(6.2, 'Head Finalizing Level 3 Decision...');
|
||||
// await apiRequest('/assessment/decision', 'POST', {
|
||||
// interviewId: interviewId3,
|
||||
// decision: 'Approved',
|
||||
// remarks: 'Cleared Level 3. Moving to FDD.'
|
||||
// }, headToken);
|
||||
// log(6, 'Level 3 Complete. Stage is now FDD Verification.');
|
||||
// await delay();
|
||||
log(6.2, 'Head Finalizing Level 3 Decision...');
|
||||
await apiRequest('/assessment/decision', 'POST', {
|
||||
interviewId: interviewId3,
|
||||
decision: 'Approved',
|
||||
remarks: 'Cleared Level 3. Moving to FDD.'
|
||||
}, headToken);
|
||||
log(6, 'Level 3 Complete. Stage is now FDD Verification.');
|
||||
await delay();
|
||||
|
||||
// // 6.3 FDD ASSIGNMENT
|
||||
// log(6.3, 'Admin Assigning Application to FDD Agency...');
|
||||
// const fddUser = users.data.find(u => u.email === EMAILS.FDD);
|
||||
// await apiRequest('/fdd/assign', 'POST', {
|
||||
// applicationId: applicationUUID,
|
||||
// assignedToAgency: fddUser.id
|
||||
// }, adminToken);
|
||||
// log(6.3, 'FDD Agency assigned successfully.');
|
||||
// await delay();
|
||||
// 6.3 FDD ASSIGNMENT
|
||||
log(6.3, 'Admin Assigning Application to FDD Agency...');
|
||||
const fddUser = users.data.find(u => u.email === EMAILS.FDD);
|
||||
await apiRequest('/fdd/assign', 'POST', {
|
||||
applicationId: applicationUUID,
|
||||
assignedToAgency: fddUser.id
|
||||
}, adminToken);
|
||||
log(6.3, 'FDD Agency assigned successfully.');
|
||||
await delay();
|
||||
|
||||
// // 7. FDD MILESTONE
|
||||
// log(7, 'FDD Agency Discovery & Report Upload...');
|
||||
// const fddToken = await login(EMAILS.FDD);
|
||||
// 7. FDD MILESTONE
|
||||
log(7, 'FDD Agency Discovery & Report Upload...');
|
||||
const fddToken = await login(EMAILS.FDD);
|
||||
|
||||
// // FETCH ASSIGNMENT ID
|
||||
// const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
|
||||
// const assignmentId = assignmentRes.data.id;
|
||||
// log(7, `Found Assignment ID: ${assignmentId}`);
|
||||
// FETCH ASSIGNMENT ID
|
||||
const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
|
||||
const assignmentId = assignmentRes.data.id;
|
||||
log(7, `Found Assignment ID: ${assignmentId}`);
|
||||
|
||||
// await apiRequest('/fdd/report', 'POST', {
|
||||
// assignmentId,
|
||||
// findings: 'Finance records clean.',
|
||||
// recommendation: 'Approved'
|
||||
// }, fddToken);
|
||||
await apiRequest('/fdd/report', 'POST', {
|
||||
assignmentId,
|
||||
findings: 'Finance records clean.',
|
||||
recommendation: 'Approved'
|
||||
}, fddToken);
|
||||
|
||||
// log(7.1, 'Admin Approving FDD Final Stage...');
|
||||
// await apiRequest('/assessment/stage-decision', 'POST', {
|
||||
// applicationId: applicationUUID,
|
||||
// stageCode: 'FDD_VERIFICATION',
|
||||
// decision: 'Approved',
|
||||
// remarks: 'FDD documents verified.'
|
||||
// }, adminToken);
|
||||
// log(7, 'FDD Milestone Complete.');
|
||||
// await delay();
|
||||
log(7.1, 'Admin Approving FDD Final Stage...');
|
||||
await apiRequest('/assessment/stage-decision', 'POST', {
|
||||
applicationId: applicationUUID,
|
||||
stageCode: 'FDD_VERIFICATION',
|
||||
decision: 'Approved',
|
||||
remarks: 'FDD documents verified.'
|
||||
}, adminToken);
|
||||
log(7, 'FDD Milestone Complete.');
|
||||
await delay();
|
||||
|
||||
// log(7.4, 'Uploading mandatory documents prior to LOI generation...');
|
||||
// const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
|
||||
// for (const doc of requiredDocs) {
|
||||
// await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||
// }
|
||||
// await delay(1000);
|
||||
log(7.4, 'Uploading mandatory documents prior to LOI generation...');
|
||||
const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
|
||||
for (const doc of requiredDocs) {
|
||||
await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||
}
|
||||
await delay(1000);
|
||||
|
||||
// // 7.5 LOI APPROVAL
|
||||
// log(7.5, 'LOI Generation & Approval...');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user