diff --git a/check_logs.ts b/check_logs.ts new file mode 100644 index 0000000..63d1594 --- /dev/null +++ b/check_logs.ts @@ -0,0 +1,21 @@ +import db from './src/database/models/index.js'; + +async function checkAuditLogs() { + try { + const resignationLogs = await db.ResignationAudit.findAll({ + include: [{ model: db.User, as: 'user', attributes: ['fullName'] }], + limit: 5, + order: [['createdAt', 'DESC']] + }); + + console.log('--- RECENT RESIGNATION AUDIT LOGS ---'); + console.log(JSON.stringify(resignationLogs, null, 2)); + + process.exit(0); + } catch (error) { + console.error('Error checking logs:', error); + process.exit(1); + } +} + +checkAuditLogs(); diff --git a/debug-state.cjs b/debug-state.cjs new file mode 100644 index 0000000..31968eb --- /dev/null +++ b/debug-state.cjs @@ -0,0 +1,45 @@ + +const db = require('./src/database/models/index.js').default; +const { User, Resignation, FnF } = db; + +async function checkState() { + const email = 'ramesh_1776053291272@gmail.com'; + const user = await User.findOne({ where: { email } }); + console.log('--- USER STATE ---'); + if (user) { + console.log(`ID: ${user.id}`); + console.log(`DealerID: ${user.dealerId}`); + console.log(`Status: ${user.status}`); + console.log(`Role: ${user.roleCode}`); + } else { + console.log('User not found'); + } + + const resignation = await Resignation.findOne({ + where: { resignationId: 'RES-2026-7033' }, // From nomenclature or E2E logs if available + // Wait, the id from logs was d4e1a5ed-011d-415d-89b4-bde65cbf5d58 + include: ['fnf'] + }).catch(() => null); + + if (!resignation) { + // Try by UUID + const resByUuid = await Resignation.findByPk('d4e1a5ed-011d-415d-89b4-bde65cbf5d58', { include: ['fnf'] }); + console.log('\n--- RESIGNATION STATE ---'); + if (resByUuid) { + console.log(`ID: ${resByUuid.id}`); + console.log(`Status: ${resByUuid.status}`); + console.log(`Stage: ${resByUuid.currentStage}`); + console.log(`DealerID: ${resByUuid.dealerId}`); + if (resByUuid.fnf) { + console.log(`FnF ID: ${resByUuid.fnf.id}`); + console.log(`FnF Status: ${resByUuid.fnf.status}`); + } + } else { + console.log('Resignation not found'); + } + } + + process.exit(0); +} + +checkState(); diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index e8bf195..2ab038e 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -256,7 +256,7 @@ export const FNF_STATUS = { COMPLETED: 'Completed' } as const; -// F&F Departments (Full list of 16 functional units as per Finance Dashboard) +// F&F Departments (Full list of 16 functional units as per Royal Enfield standards) export const FNF_DEPARTMENTS = [ 'Warranty Department', 'Accessories Department', diff --git a/src/common/utils/nomenclature.ts b/src/common/utils/nomenclature.ts index c66fa9c..0ab0e57 100644 --- a/src/common/utils/nomenclature.ts +++ b/src/common/utils/nomenclature.ts @@ -35,10 +35,12 @@ export class NomenclatureService { } /** - * Generates a Settlement/FnF ID (e.g., SET-2026-9012) + * Generates a Settlement/FnF ID (e.g., FNF-2025-001) */ - static generateSettlementId() { - return `SET-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`; + static generateFnFId() { + const year = new Date().getFullYear(); + const rand = Math.floor(1 + Math.random() * 999); + return `FNF-${year}-${rand.toString().padStart(3, '0')}`; } /** diff --git a/src/database/models/ConstitutionalAudit.ts b/src/database/models/ConstitutionalAudit.ts new file mode 100644 index 0000000..0db2d96 --- /dev/null +++ b/src/database/models/ConstitutionalAudit.ts @@ -0,0 +1,65 @@ +import { Model, DataTypes, Sequelize } from 'sequelize'; + +export interface ConstitutionalAuditAttributes { + id: string; + userId: string | null; + constitutionalChangeId: string; + action: string; + details: any | null; + remarks: string | null; +} + +export interface ConstitutionalAuditInstance extends Model, ConstitutionalAuditAttributes { } + +export default (sequelize: Sequelize) => { + const ConstitutionalAudit = sequelize.define('ConstitutionalAudit', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + userId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'users', + key: 'id' + } + }, + constitutionalChangeId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'constitutional_changes', + key: 'id' + } + }, + action: { + type: DataTypes.STRING, + allowNull: false + }, + details: { + type: DataTypes.JSON, + allowNull: true + }, + remarks: { + type: DataTypes.TEXT, + allowNull: true + } + }, { + tableName: 'constitutional_audit_logs', + timestamps: true, + indexes: [ + { fields: ['constitutionalChangeId'] }, + { fields: ['userId'] }, + { fields: ['action'] } + ] + }); + + (ConstitutionalAudit as any).associate = (models: any) => { + ConstitutionalAudit.belongsTo(models.User, { foreignKey: 'userId', as: 'user' }); + ConstitutionalAudit.belongsTo(models.ConstitutionalChange, { foreignKey: 'constitutionalChangeId', as: 'constitutionalChange' }); + }; + + return ConstitutionalAudit; +}; diff --git a/src/database/models/FnF.ts b/src/database/models/FnF.ts index 39fa13d..cb2adbe 100644 --- a/src/database/models/FnF.ts +++ b/src/database/models/FnF.ts @@ -20,6 +20,7 @@ export interface FnFAttributes { remarks: string | null; clearanceDocuments: any[]; progressPercentage: number; + timeline: any[]; } export interface FnFInstance extends Model, FnFAttributes { } @@ -115,6 +116,10 @@ export default (sequelize: Sequelize) => { progressPercentage: { type: DataTypes.INTEGER, defaultValue: 0 + }, + timeline: { + type: DataTypes.JSON, + defaultValue: [] } }, { tableName: 'fnf_settlements', diff --git a/src/database/models/FnFAudit.ts b/src/database/models/FnFAudit.ts new file mode 100644 index 0000000..eb5de54 --- /dev/null +++ b/src/database/models/FnFAudit.ts @@ -0,0 +1,65 @@ +import { Model, DataTypes, Sequelize } from 'sequelize'; + +export interface FnFAuditAttributes { + id: string; + userId: string | null; + fnfId: string; + action: string; + details: any | null; + remarks: string | null; +} + +export interface FnFAuditInstance extends Model, FnFAuditAttributes { } + +export default (sequelize: Sequelize) => { + const FnFAudit = sequelize.define('FnFAudit', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + userId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'users', + key: 'id' + } + }, + fnfId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'fnf_settlements', + key: 'id' + } + }, + action: { + type: DataTypes.STRING, + allowNull: false + }, + details: { + type: DataTypes.JSON, + allowNull: true + }, + remarks: { + type: DataTypes.TEXT, + allowNull: true + } + }, { + tableName: 'fnf_audit_logs', + timestamps: true, + indexes: [ + { fields: ['fnfId'] }, + { fields: ['userId'] }, + { fields: ['action'] } + ] + }); + + (FnFAudit as any).associate = (models: any) => { + FnFAudit.belongsTo(models.User, { foreignKey: 'userId', as: 'user' }); + FnFAudit.belongsTo(models.FnF, { foreignKey: 'fnfId', as: 'fnf' }); + }; + + return FnFAudit; +}; diff --git a/src/database/models/RelocationAudit.ts b/src/database/models/RelocationAudit.ts new file mode 100644 index 0000000..a7e928c --- /dev/null +++ b/src/database/models/RelocationAudit.ts @@ -0,0 +1,65 @@ +import { Model, DataTypes, Sequelize } from 'sequelize'; + +export interface RelocationAuditAttributes { + id: string; + userId: string | null; + relocationRequestId: string; + action: string; + details: any | null; + remarks: string | null; +} + +export interface RelocationAuditInstance extends Model, RelocationAuditAttributes { } + +export default (sequelize: Sequelize) => { + const RelocationAudit = sequelize.define('RelocationAudit', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + userId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'users', + key: 'id' + } + }, + relocationRequestId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'relocation_requests', + key: 'id' + } + }, + action: { + type: DataTypes.STRING, + allowNull: false + }, + details: { + type: DataTypes.JSON, + allowNull: true + }, + remarks: { + type: DataTypes.TEXT, + allowNull: true + } + }, { + tableName: 'relocation_audit_logs', + timestamps: true, + indexes: [ + { fields: ['relocationRequestId'] }, + { fields: ['userId'] }, + { fields: ['action'] } + ] + }); + + (RelocationAudit as any).associate = (models: any) => { + RelocationAudit.belongsTo(models.User, { foreignKey: 'userId', as: 'user' }); + RelocationAudit.belongsTo(models.RelocationRequest, { foreignKey: 'relocationRequestId', as: 'relocationRequest' }); + }; + + return RelocationAudit; +}; diff --git a/src/database/models/ResignationAudit.ts b/src/database/models/ResignationAudit.ts new file mode 100644 index 0000000..16a47eb --- /dev/null +++ b/src/database/models/ResignationAudit.ts @@ -0,0 +1,65 @@ +import { Model, DataTypes, Sequelize } from 'sequelize'; + +export interface ResignationAuditAttributes { + id: string; + userId: string | null; + resignationId: string; + action: string; + details: any | null; + remarks: string | null; +} + +export interface ResignationAuditInstance extends Model, ResignationAuditAttributes { } + +export default (sequelize: Sequelize) => { + const ResignationAudit = sequelize.define('ResignationAudit', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + userId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'users', + key: 'id' + } + }, + resignationId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'resignations', + key: 'id' + } + }, + action: { + type: DataTypes.STRING, + allowNull: false + }, + details: { + type: DataTypes.JSON, + allowNull: true + }, + remarks: { + type: DataTypes.TEXT, + allowNull: true + } + }, { + tableName: 'resignation_audit_logs', + timestamps: true, + indexes: [ + { fields: ['resignationId'] }, + { fields: ['userId'] }, + { fields: ['action'] } + ] + }); + + (ResignationAudit as any).associate = (models: any) => { + ResignationAudit.belongsTo(models.User, { foreignKey: 'userId', as: 'user' }); + ResignationAudit.belongsTo(models.Resignation, { foreignKey: 'resignationId', as: 'resignation' }); + }; + + return ResignationAudit; +}; diff --git a/src/database/models/TerminationAudit.ts b/src/database/models/TerminationAudit.ts new file mode 100644 index 0000000..01adfdd --- /dev/null +++ b/src/database/models/TerminationAudit.ts @@ -0,0 +1,65 @@ +import { Model, DataTypes, Sequelize } from 'sequelize'; + +export interface TerminationAuditAttributes { + id: string; + userId: string | null; + terminationRequestId: string; + action: string; + details: any | null; + remarks: string | null; +} + +export interface TerminationAuditInstance extends Model, TerminationAuditAttributes { } + +export default (sequelize: Sequelize) => { + const TerminationAudit = sequelize.define('TerminationAudit', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + userId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'users', + key: 'id' + } + }, + terminationRequestId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'termination_requests', + key: 'id' + } + }, + action: { + type: DataTypes.STRING, + allowNull: false + }, + details: { + type: DataTypes.JSON, + allowNull: true + }, + remarks: { + type: DataTypes.TEXT, + allowNull: true + } + }, { + tableName: 'termination_audit_logs', + timestamps: true, + indexes: [ + { fields: ['terminationRequestId'] }, + { fields: ['userId'] }, + { fields: ['action'] } + ] + }); + + (TerminationAudit as any).associate = (models: any) => { + TerminationAudit.belongsTo(models.User, { foreignKey: 'userId', as: 'user' }); + TerminationAudit.belongsTo(models.TerminationRequest, { foreignKey: 'terminationRequestId', as: 'termination' }); + }; + + return TerminationAudit; +}; diff --git a/src/database/models/index.ts b/src/database/models/index.ts index f910b92..9fbcda6 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -33,6 +33,11 @@ import createState from './State.js'; import createTerminationScnResponse from './TerminationScnResponse.js'; import createTerminationHearingRecord from './TerminationHearingRecord.js'; import createFffClearance from './FffClearance.js'; +import createResignationAudit from './ResignationAudit.js'; +import createTerminationAudit from './TerminationAudit.js'; +import createFnFAudit from './FnFAudit.js'; +import createConstitutionalAudit from './ConstitutionalAudit.js'; +import createRelocationAudit from './RelocationAudit.js'; import createDealerBankDetail from './DealerBankDetail.js'; // Batch 1: Organizational Hierarchy & User Management @@ -147,6 +152,11 @@ db.State = createState(sequelize); db.TerminationScnResponse = createTerminationScnResponse(sequelize); db.TerminationHearingRecord = createTerminationHearingRecord(sequelize); db.FffClearance = createFffClearance(sequelize); +db.ResignationAudit = createResignationAudit(sequelize); +db.TerminationAudit = createTerminationAudit(sequelize); +db.FnFAudit = createFnFAudit(sequelize); +db.ConstitutionalAudit = createConstitutionalAudit(sequelize); +db.RelocationAudit = createRelocationAudit(sequelize); db.DealerBankDetail = createDealerBankDetail(sequelize); // Batch 1: Organizational Hierarchy & User Management diff --git a/src/modules/audit/audit.controller.ts b/src/modules/audit/audit.controller.ts index 514707f..b824256 100644 --- a/src/modules/audit/audit.controller.ts +++ b/src/modules/audit/audit.controller.ts @@ -52,6 +52,8 @@ const ACTION_DESCRIPTIONS: Record = { PAYMENT_UPDATED: 'Payment record updated', SECURITY_DEPOSIT_UPDATED: 'Security deposit updated', FNF_UPDATED: 'F&F settlement updated', + CLEARANCE_UPDATED: 'Departmental clearance response recorded', + STAKEHOLDER_CLEARANCE_UPDATED: 'F&F stakeholder clearance synced', USER_CREATED: 'User account created', USER_UPDATED: 'User account updated', USER_STATUS_CHANGED: 'User status changed', @@ -71,6 +73,7 @@ const ACTION_DESCRIPTIONS: Record = { export const getAuditLogs = async (req: AuthRequest, res: Response) => { try { const { entityType, entityId, page = '1', limit = '50' } = req.query; + console.log(`[AuditController] Fetching logs for ${entityType} ID: ${entityId}`); if (!entityType || !entityId) { return res.status(400).json({ @@ -83,36 +86,97 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => { const limitNum = Math.min(100, Math.max(1, parseInt(limit as string))); const offset = (pageNum - 1) * limitNum; - const { count, rows: logs } = await AuditLog.findAndCountAll({ - where: { - entityType: entityType as string, - entityId: entityId as string - }, - include: [{ - model: User, - as: 'user', - attributes: ['id', 'fullName', 'email'] - }], - order: [['createdAt', 'DESC']], - limit: limitNum, - offset - }); + let count = 0; + let logs: any[] = []; - // Format the response with human-readable descriptions + // Dynamic Table Switching based on Module + // Case-insensitive entity type routing + const type = (entityType as string).toLowerCase(); + + if (type === 'resignation') { + const result = await db.ResignationAudit.findAndCountAll({ + where: { resignationId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], + order: [['createdAt', 'DESC']], + limit: limitNum, offset + }); + count = result.count; + logs = result.rows; + } else if (type === 'termination') { + const result = await db.TerminationAudit.findAndCountAll({ + where: { terminationRequestId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], + order: [['createdAt', 'DESC']], + limit: limitNum, offset + }); + count = result.count; + logs = result.rows; + } else if (type === 'fnf') { + const result = await db.FnFAudit.findAndCountAll({ + where: { fnfId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], + order: [['createdAt', 'DESC']], + limit: limitNum, offset + }); + count = result.count; + logs = result.rows; + } else if (type === 'constitutional_change') { + const result = await db.ConstitutionalAudit.findAndCountAll({ + where: { constitutionalChangeId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], + order: [['createdAt', 'DESC']], + limit: limitNum, offset + }); + count = result.count; + logs = result.rows; + } else if (type === 'relocation') { + const result = await db.RelocationAudit.findAndCountAll({ + where: { relocationRequestId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], + order: [['createdAt', 'DESC']], + limit: limitNum, offset + }); + count = result.count; + logs = result.rows; + } else { + console.log(`[AuditController] Falling back to global AuditLog for type: ${type}`); + const result = await db.AuditLog.findAndCountAll({ + where: { entityType: entityType as string, entityId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], + order: [['createdAt', 'DESC']], + limit: limitNum, offset + }); + count = result.count; + logs = result.rows; + } + + console.log(`[AuditController] Found ${count} logs for ${entityType}`); + + // Format the response with human-readable descriptions and consistent mapping const formattedLogs = logs.map((log: any) => { - const logData = log.toJSON ? log.toJSON() : log; + const logData = log.get ? log.get({ plain: true }) : log; + const details = logData.details || logData.newData; + + let baseDescription = ACTION_DESCRIPTIONS[logData.action] || + logData.action.split('_').map((w: any) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' '); + + // EXCLUSIVE ADDITION: Enhance description with context if available (Stage/Status/Dept) + if (details) { + if (details.stage) baseDescription += ` - Stage: ${details.stage}`; + else if (details.department) baseDescription += ` - ${details.department}`; + else if (details.status && logData.action === 'UPDATED') baseDescription += ` to ${details.status}`; + } + return { id: logData.id, action: logData.action, - description: ACTION_DESCRIPTIONS[logData.action] || logData.action, - entityType: logData.entityType, - entityId: logData.entityId, + description: baseDescription, + entityType: entityType, + entityId: entityId, userName: logData.user?.fullName || 'System', userEmail: logData.user?.email || null, - oldData: logData.oldData, - newData: logData.newData, - changes: formatChanges(logData.oldData, logData.newData), - ipAddress: logData.ipAddress, + remarks: logData.remarks || logData.newData?.remarks, + newData: details, // Normalize module-specific 'details' to 'newData' for UI timestamp: logData.createdAt }; }); @@ -147,25 +211,54 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => { }); } - const totalLogs = await AuditLog.count({ - where: { - entityType: entityType as string, - entityId: entityId as string - } - }); + let totalLogs = 0; + let latestLog: any = null; + const type = (entityType as string).toLowerCase(); - const latestLog = await AuditLog.findOne({ - where: { - entityType: entityType as string, - entityId: entityId as string - }, - include: [{ - model: User, - as: 'user', - attributes: ['id', 'fullName'] - }], - order: [['createdAt', 'DESC']] - }); + // Dynamic Table Switching + if (type === 'resignation') { + totalLogs = await db.ResignationAudit.count({ where: { resignationId: entityId as string } }); + latestLog = await db.ResignationAudit.findOne({ + where: { resignationId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], + order: [['createdAt', 'DESC']] + }); + } else if (type === 'termination') { + totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: entityId as string } }); + latestLog = await db.TerminationAudit.findOne({ + where: { terminationRequestId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], + order: [['createdAt', 'DESC']] + }); + } else if (type === 'fnf') { + totalLogs = await db.FnFAudit.count({ where: { fnfId: entityId as string } }); + latestLog = await db.FnFAudit.findOne({ + where: { fnfId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], + order: [['createdAt', 'DESC']] + }); + } else if (type === 'constitutional' || type === 'constitutional_change') { + totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: entityId as string } }); + latestLog = await db.ConstitutionalAudit.findOne({ + where: { constitutionalChangeId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], + order: [['createdAt', 'DESC']] + }); + } else if (type === 'relocation' || type === 'relocation_request') { + totalLogs = await db.RelocationAudit.count({ where: { relocationRequestId: entityId as string } }); + latestLog = await db.RelocationAudit.findOne({ + where: { relocationRequestId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], + order: [['createdAt', 'DESC']] + }); + } else { + totalLogs = await db.AuditLog.count({ where: { entityType: entityType as string, entityId: entityId as string } }); + latestLog = await db.AuditLog.findOne({ + where: { entityType: entityType as string, entityId: entityId as string }, + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], + order: [['createdAt', 'DESC']] + }); + } res.json({ success: true, @@ -173,7 +266,7 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => { totalEntries: totalLogs, lastActivity: latestLog ? { action: (latestLog as any).action, - description: ACTION_DESCRIPTIONS[(latestLog as any).action] || (latestLog as any).action, + description: ACTION_DESCRIPTIONS[(latestLog as any).action] || (latestLog as any).action.split('_').map((w: any) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' '), user: (latestLog as any).user?.fullName || 'System', timestamp: (latestLog as any).createdAt } : null diff --git a/src/modules/self-service/resignation.controller.ts b/src/modules/self-service/resignation.controller.ts index 4bed1f5..b2e5b2b 100644 --- a/src/modules/self-service/resignation.controller.ts +++ b/src/modules/self-service/resignation.controller.ts @@ -65,11 +65,11 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N }, { transaction }); await outlet.update({ status: 'Pending Resignation' }, { transaction }); - await db.AuditLog.create({ + await db.ResignationAudit.create({ userId: req.user.id, action: AUDIT_ACTIONS.CREATED, - entityType: 'resignation', - entityId: resignation.id + resignationId: resignation.id, + remarks: 'Dealer submitted resignation request' }, { transaction }); await transaction.commit(); @@ -265,7 +265,7 @@ export const approveResignation = async (req: AuthRequest, res: Response, next: const dealerProfileId = (resignation as any).dealer?.dealerId; const fnf = await db.FnF.create({ - settlementId: NomenclatureService.generateSettlementId(), + settlementId: NomenclatureService.generateFnFId(), resignationId: resignation.id, outletId: resignation.outletId, dealerId: dealerProfileId, // Correctly using the Dealer model ID @@ -483,6 +483,15 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex }] }, { transaction }); + // Record module-specific audit + await db.ResignationAudit.create({ + userId: req.user.id, + resignationId: resignation.id, + action: 'CLEARANCE_UPDATED', + remarks: remarks || `Cleared ${department}`, + details: { department, status, amount } + }, { transaction }); + // Sync with F&F Clearance if settlement exists const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id } }); if (fnf) { @@ -520,6 +529,15 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex }, { transaction }); } + // Record F&F specific audit + await db.FnFAudit.create({ + userId: req.user.id, + fnfId: fnf.id, + action: 'CLEARANCE_UPDATED', + remarks: remarks || `Departmental clearance recorded for ${department}`, + details: { department, status: fnfStatus, source: 'Resignation Workflow' } + }, { transaction }); + // If there's an amount, create/update line item if (amount > 0) { const existingItem = await db.FnFLineItem.findOne({ @@ -594,7 +612,7 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n case 'pushfnf': // Verify if user role is authorized for manual jump to F&F const authorizedRoles = [ROLES.DD_LEAD, ROLES.DD_HEAD, ROLES.NBH, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN]; - if (!authorizedRoles.includes(req.user.roleCode as any)) { + if (!req.user || !authorizedRoles.includes(req.user.roleCode as any)) { return res.status(403).json({ success: false, message: 'You do not have permission to push this request to F&F' }); } // Jump directly to F&F Initiation diff --git a/src/modules/settlement/settlement.controller.ts b/src/modules/settlement/settlement.controller.ts index cdae3e2..5c0456d 100644 --- a/src/modules/settlement/settlement.controller.ts +++ b/src/modules/settlement/settlement.controller.ts @@ -2,7 +2,9 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; const { FinancePayment, FnF, Application, Resignation, User, Outlet, FnFLineItem, TerminationRequest, FffClearance, AuditLog } = db; import { AuthRequest } from '../../types/express.types.js'; -import { FNF_STATUS, AUDIT_ACTIONS, FNF_DEPARTMENTS } from '../../common/config/constants.js'; +import { FNF_STATUS, AUDIT_ACTIONS, FNF_DEPARTMENTS, RESIGNATION_STAGES, TERMINATION_STAGES } from '../../common/config/constants.js'; +import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js'; +import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js'; export const getDepartments = async (req: Request, res: Response) => { try { @@ -92,8 +94,20 @@ export const updateFnF = async (req: AuthRequest, res: Response) => { newData: { status, netAmount: finalSettlementAmount, remarks } }); - // If status is being set to Completed, we might want to trigger additional logic here - // like notifying the dealer or updating the resignation status if it's not already + // If status is being set to Completed, update the parent request status as well + if (status === 'Completed' || status === FNF_STATUS.COMPLETED) { + if (fnf.resignationId) { + await Resignation.update( + { status: 'Completed', stage: 'Completed' }, + { where: { id: fnf.resignationId } } + ); + } else if (fnf.terminationRequestId) { + await TerminationRequest.update( + { status: 'Completed' }, + { where: { id: fnf.terminationRequestId } } + ); + } + } res.json({ success: true, message: 'F&F settlement updated successfully', data: fnf }); } catch (error) { @@ -108,6 +122,7 @@ export const getFnFSettlements = async (req: Request, res: Response) => { include: [ { model: Resignation, as: 'resignation', attributes: ['id', 'resignationId'] }, { model: TerminationRequest, as: 'terminationRequest', attributes: ['id', 'status', 'category'] }, + { model: db.Dealer, as: 'dealer', attributes: ['legalName', 'businessName', 'id'] }, { model: Outlet, as: 'outlet', include: [{ model: User, as: 'dealer', attributes: ['fullName', 'id'] }] }, { model: FnFLineItem, as: 'lineItems' }, { model: FffClearance, as: 'clearances' } @@ -171,7 +186,7 @@ export const addLineItem = async (req: AuthRequest, res: Response) => { }); // Update FnF progress and department statuses - await calculateFnFLogic(id); + await calculateFnFLogic(id as string, req.user?.id); await AuditLog.create({ userId: req.user?.id || null, @@ -196,7 +211,8 @@ export const updateLineItem = async (req: AuthRequest, res: Response) => { await lineItem.update({ description, department, amount }); // Update FnF progress and department statuses - await calculateFnFLogic(lineItem.fnfId); + // Update FnF progress and department statuses + await calculateFnFLogic(lineItem.fnfId, req.user?.id); await AuditLog.create({ userId: req.user?.id || null, @@ -222,7 +238,8 @@ export const deleteLineItem = async (req: AuthRequest, res: Response) => { await lineItem.destroy(); // Update FnF progress and department statuses - await calculateFnFLogic(fnfId); + // Update FnF progress and department statuses + await calculateFnFLogic(fnfId, req.user?.id); await AuditLog.create({ userId: req.user?.id || null, @@ -239,7 +256,7 @@ export const deleteLineItem = async (req: AuthRequest, res: Response) => { }; // Helper to calculate and update FnF progress -const calculateFnFLogic = async (id: string) => { +const calculateFnFLogic = async (id: string, userId: string | null = null) => { const fnf = await FnF.findByPk(id, { include: [{ model: FnFLineItem, as: 'lineItems' }, { model: FffClearance, as: 'clearances' }] }); @@ -302,6 +319,29 @@ const calculateFnFLogic = async (id: string) => { progressPercentage }); + // If status moved to Completed, also update parent resignation/termination + if (newStatus === FNF_STATUS.COMPLETED || newStatus === 'Completed') { + if (fnf.resignationId) { + const resignation = await Resignation.findByPk(fnf.resignationId); + if (resignation) { + await ResignationWorkflowService.transitionResignation(resignation, RESIGNATION_STAGES.COMPLETED, userId, { + action: 'F&F Settlement Completed', + remarks: 'Full & Final settlement process finalized. Transitioning resignation to Completed.', + status: 'Completed' + }); + } + } else if (fnf.terminationRequestId) { + const terminationRequest = await TerminationRequest.findByPk(fnf.terminationRequestId); + if (terminationRequest) { + await TerminationWorkflowService.transitionTermination(terminationRequest, TERMINATION_STAGES.TERMINATED, userId, { + action: 'F&F Settlement Completed', + remarks: 'Full & Final settlement process finalized. Transitioning termination to Terminated.', + status: 'Terminated' + }); + } + } + } + return fnf; }; @@ -322,15 +362,43 @@ export const updateClearance = async (req: AuthRequest, res: Response) => { }); // Automatically update FnF progress - await calculateFnFLogic(id); + console.log(`[SettlementController] Updating clearance for F&F: ${id}`); + const fnfRecord = await calculateFnFLogic(id as string, req.user?.id); - await AuditLog.create({ - userId: req.user?.id || null, - action: AUDIT_ACTIONS.FNF_UPDATED, - entityType: 'fnf', - entityId: id, - newData: { action: 'UPDATE_CLEARANCE', department: clearance.department, status, remarks } - }); + // 1. Local F&F Audit (Dedicated Table: fnf_audit_logs) + try { + console.log(`[SettlementController] Creating FnFAudit for ${id}`); + await db.FnFAudit.create({ + userId: req.user?.id || null, + fnfId: id, + action: 'CLEARANCE_UPDATED', + remarks: remarks || 'No remarks', + details: { department: clearance.department, status } + }); + } catch (auditError) { + console.error('[SettlementController] Local FnFAudit creation failed:', auditError); + } + + // 2. Interconnected Mirror (Dedicated Table: resignation_audit_logs or termination_audit_logs) + if (fnfRecord && (fnfRecord.resignationId || fnfRecord.terminationRequestId)) { + try { + const isResignation = !!fnfRecord.resignationId; + const parentAuditModel = isResignation ? db.ResignationAudit : db.TerminationAudit; + const parentKey = isResignation ? 'resignationId' : 'terminationRequestId'; + const parentId = fnfRecord.resignationId || fnfRecord.terminationRequestId; + + console.log(`[SettlementController] Creating Parent Audit for ${parentId} (${isResignation ? 'resignation' : 'termination'})`); + await parentAuditModel.create({ + userId: req.user?.id || null, + [parentKey]: parentId, + action: 'STAKEHOLDER_CLEARANCE_UPDATED', + remarks: `Automated sync from F&F: ${remarks || 'No remarks'}`, + details: { department: clearance.department, status } + }); + } catch (parentAuditError) { + console.error('[SettlementController] Parent Audit creation failed:', parentAuditError); + } + } res.json({ success: true, message: 'Clearance updated successfully', clearance }); } catch (error) { @@ -342,7 +410,7 @@ export const updateClearance = async (req: AuthRequest, res: Response) => { export const calculateFnF = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const fnf = await calculateFnFLogic(id); + const fnf = await calculateFnFLogic(id as string, req.user?.id); if (!fnf) return res.status(404).json({ success: false, message: 'F&F not found' }); res.json({ success: true, fnf }); diff --git a/src/modules/termination/termination.controller.ts b/src/modules/termination/termination.controller.ts index f7b3961..d34f94d 100644 --- a/src/modules/termination/termination.controller.ts +++ b/src/modules/termination/termination.controller.ts @@ -38,11 +38,11 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N }] }, { transaction }); - await db.AuditLog.create({ + await db.TerminationAudit.create({ userId: req.user.id, action: AUDIT_ACTIONS.CREATED, - entityType: 'termination', - entityId: termination.id + terminationRequestId: termination.id, + remarks: 'Admin initiated termination request' }, { transaction }); await transaction.commit(); @@ -306,14 +306,24 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex ); } - await db.AuditLog.create({ + await db.TerminationAudit.create({ userId: req.user.id, - action: AUDIT_ACTIONS.UPDATED, - entityType: 'termination', - entityId: id, - newData: { department, status, amount } + action: 'CLEARANCE_UPDATED', + terminationRequestId: id, + remarks: remarks || `Cleared ${department}`, + details: { department, status, amount } }, { transaction }); + if (fnf) { + await db.FnFAudit.create({ + userId: req.user.id, + fnfId: fnf.id, + action: 'CLEARANCE_UPDATED', + remarks: remarks || `Departmental clearance recorded for ${department}`, + details: { department, status, source: 'Termination Workflow' } + }, { transaction }); + } + await transaction.commit(); res.json({ success: true, message: `Clearance updated for ${department}`, clearances }); } catch (error) { diff --git a/src/services/ConstitutionalWorkflowService.ts b/src/services/ConstitutionalWorkflowService.ts index 322ac10..505ae92 100644 --- a/src/services/ConstitutionalWorkflowService.ts +++ b/src/services/ConstitutionalWorkflowService.ts @@ -30,11 +30,12 @@ export class ConstitutionalWorkflowService { await request.update(updateData); // Audit Log - await db.AuditLog.create({ + await db.ConstitutionalAudit.create({ userId, + constitutionalChangeId: request.id, action: action === 'Reject' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.UPDATED, - entityType: 'constitutional_change', - entityId: request.id + remarks: remarks || '', + details: { status: updateData.status, stage: targetStage } }); return request; diff --git a/src/services/RelocationWorkflowService.ts b/src/services/RelocationWorkflowService.ts index c89c137..620f570 100644 --- a/src/services/RelocationWorkflowService.ts +++ b/src/services/RelocationWorkflowService.ts @@ -42,12 +42,12 @@ export class RelocationWorkflowService { await request.update({ timeline: updatedTimeline }); // 3. Create Audit Log - await AuditLog.create({ + await db.RelocationAudit.create({ userId: userId, + relocationRequestId: request.id, action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED, - entityType: 'relocation', - entityId: request.id, - newData: { status: targetStatus, stage: stage || request.currentStage, reason } + remarks: reason || '', + details: { status: targetStatus, stage: stage || request.currentStage } }); console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`); diff --git a/src/services/ResignationWorkflowService.ts b/src/services/ResignationWorkflowService.ts index f5be522..1727ab3 100644 --- a/src/services/ResignationWorkflowService.ts +++ b/src/services/ResignationWorkflowService.ts @@ -2,6 +2,7 @@ import db from '../database/models/index.js'; const { AuditLog, User, Worknote } = db; import { AUDIT_ACTIONS, RESIGNATION_STAGES, ROLES } from '../common/config/constants.js'; import { NotificationService } from './NotificationService.js'; +import { Op } from 'sequelize'; import logger from '../common/utils/logger.js'; @@ -41,12 +42,12 @@ export class ResignationWorkflowService { if (action === 'WITHDRAW' || action === 'Withdrawn') auditAction = AUDIT_ACTIONS.UPDATED; if (action === 'SENT_BACK' || action === 'Sent Back') auditAction = AUDIT_ACTIONS.UPDATED; - await AuditLog.create({ + await db.ResignationAudit.create({ userId: userId, + resignationId: resignation.id, action: auditAction, - entityType: 'resignation', - entityId: resignation.id, - newData: { status: updateData.status, stage: targetStage, remarks } + remarks: remarks || '', + details: { status: updateData.status, stage: targetStage } }); // 4. Create Worknote if it's a "Sent Back" action for communication @@ -64,8 +65,10 @@ export class ResignationWorkflowService { // 5. Send Notifications const user = await User.findOne({ where: { - dealerId: resignation.dealerId, - roleCode: ROLES.DEALER + [Op.or]: [ + { id: resignation.dealerId }, + { dealerId: resignation.dealerId } + ] } }); @@ -91,7 +94,7 @@ export class ResignationWorkflowService { }); } } else { - logger.warn(`[ResignationWorkflowService] No user account found with dealerId ${resignation.dealerId} and role ${ROLES.DEALER}`); + logger.warn(`[ResignationWorkflowService] No user account found with dealerId ${resignation.dealerId}`); } return resignation; diff --git a/src/services/TerminationWorkflowService.ts b/src/services/TerminationWorkflowService.ts index a90450a..aa37c90 100644 --- a/src/services/TerminationWorkflowService.ts +++ b/src/services/TerminationWorkflowService.ts @@ -1,4 +1,5 @@ import db from '../database/models/index.js'; +import { Op } from 'sequelize'; const { AuditLog, User, TerminationScnResponse, TerminationHearingRecord, Dealer, FnF, FnFLineItem, FffClearance } = db; import { AUDIT_ACTIONS, TERMINATION_STAGES, ROLES, FNF_DEPARTMENTS } from '../common/config/constants.js'; import { NotificationService } from './NotificationService.js'; @@ -40,19 +41,21 @@ export class TerminationWorkflowService { if (action === 'REJECT' || action === 'Rejected') auditAction = AUDIT_ACTIONS.REJECTED; if (action === 'SCN_SUBMITTED' || action === 'Hearing Recorded') auditAction = AUDIT_ACTIONS.UPDATED; - await AuditLog.create({ + await db.TerminationAudit.create({ userId: userId, + terminationRequestId: termination.id, action: auditAction, - entityType: 'termination', - entityId: termination.id, - newData: { status: updateData.status, stage: targetStage, remarks } + remarks: remarks || '', + details: { status: updateData.status, stage: targetStage } }); // 4. Send Notifications const user = await User.findOne({ where: { - dealerId: termination.dealerId, - roleCode: ROLES.DEALER + [Op.or]: [ + { id: termination.dealerId }, + { dealerId: termination.dealerId } + ] } }); @@ -79,7 +82,7 @@ export class TerminationWorkflowService { }); } } else { - logger.warn(`[TerminationWorkflowService] No user account found with dealerId ${termination.dealerId} and role ${ROLES.DEALER}`); + logger.warn(`[TerminationWorkflowService] No user account found with dealerId ${termination.dealerId}`); } return termination; @@ -104,7 +107,7 @@ export class TerminationWorkflowService { // 2. Create FnF Settlement Record with direct IDs and readable identifier const fnf = await FnF.create({ - settlementId: NomenclatureService.generateSettlementId(), + settlementId: NomenclatureService.generateFnFId(), terminationRequestId: termination.id, dealerId: termination.dealerId, outletId: primaryOutlet?.id || null, diff --git a/sync_audit.ts b/sync_audit.ts new file mode 100644 index 0000000..9871865 --- /dev/null +++ b/sync_audit.ts @@ -0,0 +1,20 @@ +import db from './src/database/models/index.js'; + +async function syncAuditTables() { + try { + console.log('Syncing Module-Specific Audit Tables...'); + await db.ResignationAudit.sync({ alter: true }); + await db.TerminationAudit.sync({ alter: true }); + await db.FnFAudit.sync({ alter: true }); + await db.ConstitutionalAudit.sync({ alter: true }); + await db.RelocationAudit.sync({ alter: true }); + await db.FnF.sync({ alter: true }); + console.log('Sync complete.'); + process.exit(0); + } catch (error) { + console.error('Sync failed:', error); + process.exit(1); + } +} + +syncAuditTables(); diff --git a/trigger-resignation.js b/trigger-resignation.js index 3770e97..1388960 100644 --- a/trigger-resignation.js +++ b/trigger-resignation.js @@ -218,16 +218,27 @@ async function run() { const financeToken = await login(EMAILS.FINANCE); await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', { status: 'Completed', - finalSettlementAmount: 0, - remarks: 'Settlement completed' + finalSettlementAmount: 415173, // Matches your observed amount + paymentMode: 'NEFT / Bank Transfer', + transactionReference: `TXN-${Date.now()}`, + settlementDate: new Date().toISOString(), + remarks: 'Settlement completed and verified via automated script.' }, financeToken); await delay(); // --- FINAL COMPLETION --- - console.log('[STEP 11] Moving Resignation to COMPLETED...'); - await apiRequest(`/self-service/resignations/${resignationId}/approve`, 'PUT', { - remarks: 'Final resignation completion.' - }, adminToken); + console.log('[STEP 11] Verifying Resignation is now COMPLETED (Auto-transitioned)...'); + const finalStatusRes = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken); + if (finalStatusRes.resignation.status === 'Completed') { + log(11, 'Resignation auto-transitioned to Completed successfully.'); + } else { + console.log(`[STEP 11] FAILED: Current status is ${finalStatusRes.resignation.status}`); + // Fallback: manually trigger completion if auto-sync failed to keep script running + await apiRequest(`/self-service/resignations/${resignationId}/approve`, 'PUT', { + remarks: 'Final resignation completion (Manual Fallback).', + force: true + }, adminToken); + } await delay(); // [FINAL STEP] Verification of deactivation @@ -237,9 +248,7 @@ async function run() { const userRes = await apiRequest('/admin/users', 'GET', null, adminToken); // Fetch dealer to get its associated user ID - const dealersRes = await apiRequest('/dealer', 'GET', null, adminToken); - const targetD = dealersRes.data.find(d => d.id === targetOutlet.dealerId); - const dealerU = userRes.data.find(u => u.id === targetD.user?.id); + const dealerU = userRes.data.find(u => u.email === targetApp.email); if (dealerU && (dealerU.status === 'deactivated' || !dealerU.isActive)) { console.log(`[VERIFICATION] SUCCESS: Account ${dealerU.email} is deactivated. Status: ${dealerU.status}`); diff --git a/trigger-termination.js b/trigger-termination.js index 1c071bc..d915f62 100644 --- a/trigger-termination.js +++ b/trigger-termination.js @@ -156,7 +156,7 @@ async function run() { // Fetch user data to verify deactivation const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken); - const dealerUser = userRes.data.find(u => u.id === targetDealer.user?.id); + const dealerUser = userRes.data.find(u => u.dealerId === targetDealer.id); if (dealerUser && !dealerUser.isActive && dealerUser.status === 'deactivated') { console.log(`[VERIFICATION] Account ${dealerUser.email} successfully DEACTIVATED.`); diff --git a/trigger-workflow.js b/trigger-workflow.js index 3231b33..417fac0 100644 --- a/trigger-workflow.js +++ b/trigger-workflow.js @@ -114,7 +114,7 @@ async function prospectLogin(phone) { async function mockUploadDocument(appId, token, docType) { const formData = new FormData(); - const fileBuffer = fs.readFileSync('C:/Users/BACKPACKERS/Pictures/claim_document_type.PNG'); + const fileBuffer = fs.readFileSync('/home/laxman-h/Pictures/Screenshots/Screenshot from 2026-03-26 10-08-00.png'); const blob = new Blob([fileBuffer], { type: 'image/png' }); formData.append('file', blob, 'screenshot.png'); formData.append('documentType', docType); @@ -467,7 +467,7 @@ async function triggerWorkflow() { status: 'Completed', overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.' }, adminToken); - + // Status check const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken); log(11.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`);