diff --git a/scripts/verify-standardized-offboarding.ts b/scripts/verify-standardized-offboarding.ts index d0dd1dd..232ee60 100644 --- a/scripts/verify-standardized-offboarding.ts +++ b/scripts/verify-standardized-offboarding.ts @@ -35,6 +35,8 @@ console.log('✓ Termination stage resolution passed.'); console.log('Testing getPreviousStage (Resignation)...'); assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.RBM), RESIGNATION_STAGES.ASM); assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.ZBH), RESIGNATION_STAGES.RBM); +assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.FNF_INITIATED), RESIGNATION_STAGES.AWAITING_FNF); +assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.DD_ADMIN), RESIGNATION_STAGES.LEGAL); assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.COMPLETED), RESIGNATION_STAGES.FNF_INITIATED); console.log('✓ Resignation stage resolution passed.'); diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index 67390fa..3998184 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -200,6 +200,8 @@ export const RESIGNATION_STAGES = { DD_LEAD: 'DD Lead', NBH: 'NBH', DD_ADMIN: 'DD Admin', + /** Post DD Admin — workflow paused until an authorized user runs Push to F&F (no automatic F&F). */ + AWAITING_FNF: 'Awaiting F&F', LEGAL: 'Legal', SPARES_CLEARANCE: 'Spares Clearance', SERVICE_CLEARANCE: 'Service Clearance', diff --git a/src/common/utils/offboardingStatus.ts b/src/common/utils/offboardingStatus.ts index 0ff84e6..cd441c0 100644 --- a/src/common/utils/offboardingStatus.ts +++ b/src/common/utils/offboardingStatus.ts @@ -30,6 +30,8 @@ export const getResignationStatusForStage = (stage: string): string => { case RESIGNATION_STAGES.NBH: case RESIGNATION_STAGES.DD_ADMIN: return `${stage} Review`; + case RESIGNATION_STAGES.AWAITING_FNF: + return 'Awaiting F&F — manual initiation'; case RESIGNATION_STAGES.LEGAL: return 'Legal - Resignation Letter'; case RESIGNATION_STAGES.FNF_INITIATED: diff --git a/src/common/utils/offboardingWorkflow.utils.ts b/src/common/utils/offboardingWorkflow.utils.ts index 169e64c..c409d4c 100644 --- a/src/common/utils/offboardingWorkflow.utils.ts +++ b/src/common/utils/offboardingWorkflow.utils.ts @@ -39,9 +39,10 @@ export const getPreviousStage = (requestType: string, currentStage: string): str [RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.RBM, [RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.ZBH, [RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_LEAD, - [RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.NBH, - [RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.DD_ADMIN, - [RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.LEGAL, + [RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.NBH, + [RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL, + [RESIGNATION_STAGES.AWAITING_FNF]: RESIGNATION_STAGES.DD_ADMIN, + [RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.AWAITING_FNF, [RESIGNATION_STAGES.COMPLETED]: RESIGNATION_STAGES.FNF_INITIATED }; return flow[currentStage] || null; diff --git a/src/common/utils/workflow-email-notifications.ts b/src/common/utils/workflow-email-notifications.ts index ec35138..9100f3d 100644 --- a/src/common/utils/workflow-email-notifications.ts +++ b/src/common/utils/workflow-email-notifications.ts @@ -247,6 +247,7 @@ export async function resolveNextActors(requestId: string, requestType: string, // --- Resignation Specific --- 'DD Admin': [ROLES.DD_ADMIN], + 'Awaiting F&F': [ROLES.DD_LEAD, ROLES.DD_HEAD, ROLES.NBH, ROLES.DD_ADMIN], 'Spares Clearance': [ROLES.SPARES_MANAGER], 'Service Clearance': [ROLES.SERVICE_MANAGER], 'Accounts Clearance': [ROLES.ACCOUNTS_MANAGER], diff --git a/src/modules/self-service/resignation.controller.ts b/src/modules/self-service/resignation.controller.ts index 7ca1a4d..888f172 100644 --- a/src/modules/self-service/resignation.controller.ts +++ b/src/modules/self-service/resignation.controller.ts @@ -397,7 +397,8 @@ export const approveResignation = async (req: AuthRequest, res: Response, next: [RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.NBH, [RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.LEGAL, [RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.DD_ADMIN, - [RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.FNF_INITIATED, // DD Admin approval moves to F&F initiation + // DD Admin approval completes internal review; F&F is started only via explicit Push to F&F. + [RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.AWAITING_FNF, [RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED }; @@ -407,16 +408,16 @@ export const approveResignation = async (req: AuthRequest, res: Response, next: return res.status(400).json({ success: false, message: 'Cannot move to next stage from current state' }); } - // Guard before transition: F&F initiation is allowed only on/after LWD as per SRS §4.2.2.8 + // 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). let shouldTriggerFnF = false; - if (nextStage === RESIGNATION_STAGES.FNF_INITIATED) { + if (nextStage === RESIGNATION_STAGES.FNF_INITIATED && targetOverride) { const today = new Date(); const lwdString = resignation.lastOperationalDateServices || resignation.lastOperationalDateSales; const { force } = req.body; - + const lwd = lwdString ? new Date(lwdString) : null; if (lwd) { - // Clear time for date-only comparison today.setHours(0, 0, 0, 0); lwd.setHours(0, 0, 0, 0); } @@ -535,9 +536,14 @@ export const approveResignation = async (req: AuthRequest, res: Response, next: await transaction.commit(); - const message = (sourceStage === RESIGNATION_STAGES.LEGAL && nextStage === RESIGNATION_STAGES.LEGAL) - ? 'Legal stage approved successfully. Use Push to F&F to initiate settlement as per LWD rules.' - : 'Resignation approved successfully'; + let message = 'Resignation approved successfully'; + if (nextStage === RESIGNATION_STAGES.AWAITING_FNF) { + message = + 'DD Admin approval recorded. Use Push to F&F when ready to create the Full & Final settlement (Last Working Day rules apply).'; + } else if (sourceStage === RESIGNATION_STAGES.LEGAL && nextStage === RESIGNATION_STAGES.DD_ADMIN) { + message = + 'Legal stage approved successfully. After DD Admin review, use Push to F&F to start settlement when ready.'; + } res.json({ success: true, message, nextStage, resignation }); } catch (error) { @@ -608,6 +614,7 @@ export const withdrawResignation = async (req: AuthRequest, res: Response, next: const restrictedStages = [ RESIGNATION_STAGES.NBH, RESIGNATION_STAGES.DD_ADMIN, + RESIGNATION_STAGES.AWAITING_FNF, RESIGNATION_STAGES.LEGAL, RESIGNATION_STAGES.FNF_INITIATED, RESIGNATION_STAGES.COMPLETED @@ -1047,10 +1054,15 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n } // SRS-aligned gate: F&F can start only after Legal completion artifacts. - if (resignation.currentStage !== RESIGNATION_STAGES.LEGAL) { + const pushAllowedStages = [ + RESIGNATION_STAGES.AWAITING_FNF, + // Legacy rows: acceptance letter uploaded at Legal before DD Admin step completed in older builds + RESIGNATION_STAGES.LEGAL + ]; + if (!pushAllowedStages.includes(resignation.currentStage as any)) { return res.status(400).json({ success: false, - message: `Cannot trigger F&F from ${resignation.currentStage}. Move request to Legal stage first.` + message: `Cannot trigger F&F from ${resignation.currentStage}. Complete DD Admin review first (stage must be Awaiting F&F), or use Legal only for legacy cases.` }); } diff --git a/src/modules/termination/termination.controller.ts b/src/modules/termination/termination.controller.ts index 6f797c4..1e04d3d 100644 --- a/src/modules/termination/termination.controller.ts +++ b/src/modules/termination/termination.controller.ts @@ -618,24 +618,18 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n transaction }); - // If Terminated, trigger F&F initiation via Workflow Service - // SRS REQUIREMENT: F&F settlement process is triggered only on the Last Working Day (LWD) + // F&F is never started automatically on termination; authorized users run Push to F&F when ready. if (nextStage === TERMINATION_STAGES.TERMINATED) { const today = new Date(); const lwd = new Date(termination.proposedLwd); - - // Clear time components for date-only comparison today.setHours(0, 0, 0, 0); lwd.setHours(0, 0, 0, 0); - - if (today >= lwd) { - logger.info(`[TerminationController] LWD reached or passed (${termination.proposedLwd}). Initiating F&F.`); - await TerminationWorkflowService.initiateFnF(termination, req.user.id, transaction); - } else { - logger.info(`[TerminationController] Termination approved but LWD (${termination.proposedLwd}) not yet reached. F&F will be triggered on LWD.`); - // Keep parent status aligned while waiting for LWD-triggered F&F - await termination.update({ status: 'Awaiting F&F (LWD Pending)' }, { transaction }); - } + const statusAfterTerm = + today < lwd ? 'Awaiting F&F (LWD Pending)' : 'Awaiting F&F'; + await termination.update({ status: statusAfterTerm }, { transaction }); + logger.info( + `[TerminationController] Termination reached TERMINATED. F&F must be started manually (Push to F&F). LWD=${termination.proposedLwd}, status=${statusAfterTerm}` + ); } } diff --git a/src/services/ResignationWorkflowService.ts b/src/services/ResignationWorkflowService.ts index f1fad7a..09faf54 100644 --- a/src/services/ResignationWorkflowService.ts +++ b/src/services/ResignationWorkflowService.ts @@ -135,6 +135,7 @@ export class ResignationWorkflowService { [RESIGNATION_STAGES.NBH]: 65, [RESIGNATION_STAGES.LEGAL]: 80, [RESIGNATION_STAGES.DD_ADMIN]: 90, + [RESIGNATION_STAGES.AWAITING_FNF]: 92, [RESIGNATION_STAGES.FNF_INITIATED]: 95, [RESIGNATION_STAGES.COMPLETED]: 100, [RESIGNATION_STAGES.REJECTED]: 100 @@ -157,6 +158,7 @@ export class ResignationWorkflowService { [RESIGNATION_STAGES.NBH]: ROLES.NBH, [RESIGNATION_STAGES.LEGAL]: ROLES.LEGAL_ADMIN, [RESIGNATION_STAGES.DD_ADMIN]: ROLES.DD_ADMIN, + [RESIGNATION_STAGES.AWAITING_FNF]: [ROLES.DD_LEAD, ROLES.DD_HEAD, ROLES.NBH, ROLES.DD_ADMIN], [RESIGNATION_STAGES.FNF_INITIATED]: ROLES.DD_ADMIN };