module wise audit tables added
This commit is contained in:
parent
6373372eb9
commit
1d885f9d9f
21
check_logs.ts
Normal file
21
check_logs.ts
Normal file
@ -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();
|
||||||
45
debug-state.cjs
Normal file
45
debug-state.cjs
Normal file
@ -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();
|
||||||
@ -256,7 +256,7 @@ export const FNF_STATUS = {
|
|||||||
COMPLETED: 'Completed'
|
COMPLETED: 'Completed'
|
||||||
} as const;
|
} 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 = [
|
export const FNF_DEPARTMENTS = [
|
||||||
'Warranty Department',
|
'Warranty Department',
|
||||||
'Accessories Department',
|
'Accessories Department',
|
||||||
|
|||||||
@ -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() {
|
static generateFnFId() {
|
||||||
return `SET-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
|
const year = new Date().getFullYear();
|
||||||
|
const rand = Math.floor(1 + Math.random() * 999);
|
||||||
|
return `FNF-${year}-${rand.toString().padStart(3, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
65
src/database/models/ConstitutionalAudit.ts
Normal file
65
src/database/models/ConstitutionalAudit.ts
Normal file
@ -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>, ConstitutionalAuditAttributes { }
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
const ConstitutionalAudit = sequelize.define<ConstitutionalAuditInstance>('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;
|
||||||
|
};
|
||||||
@ -20,6 +20,7 @@ export interface FnFAttributes {
|
|||||||
remarks: string | null;
|
remarks: string | null;
|
||||||
clearanceDocuments: any[];
|
clearanceDocuments: any[];
|
||||||
progressPercentage: number;
|
progressPercentage: number;
|
||||||
|
timeline: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FnFInstance extends Model<FnFAttributes>, FnFAttributes { }
|
export interface FnFInstance extends Model<FnFAttributes>, FnFAttributes { }
|
||||||
@ -115,6 +116,10 @@ export default (sequelize: Sequelize) => {
|
|||||||
progressPercentage: {
|
progressPercentage: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: []
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'fnf_settlements',
|
tableName: 'fnf_settlements',
|
||||||
|
|||||||
65
src/database/models/FnFAudit.ts
Normal file
65
src/database/models/FnFAudit.ts
Normal file
@ -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>, FnFAuditAttributes { }
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
const FnFAudit = sequelize.define<FnFAuditInstance>('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;
|
||||||
|
};
|
||||||
65
src/database/models/RelocationAudit.ts
Normal file
65
src/database/models/RelocationAudit.ts
Normal file
@ -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>, RelocationAuditAttributes { }
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
const RelocationAudit = sequelize.define<RelocationAuditInstance>('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;
|
||||||
|
};
|
||||||
65
src/database/models/ResignationAudit.ts
Normal file
65
src/database/models/ResignationAudit.ts
Normal file
@ -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>, ResignationAuditAttributes { }
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
const ResignationAudit = sequelize.define<ResignationAuditInstance>('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;
|
||||||
|
};
|
||||||
65
src/database/models/TerminationAudit.ts
Normal file
65
src/database/models/TerminationAudit.ts
Normal file
@ -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>, TerminationAuditAttributes { }
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
const TerminationAudit = sequelize.define<TerminationAuditInstance>('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;
|
||||||
|
};
|
||||||
@ -33,6 +33,11 @@ import createState from './State.js';
|
|||||||
import createTerminationScnResponse from './TerminationScnResponse.js';
|
import createTerminationScnResponse from './TerminationScnResponse.js';
|
||||||
import createTerminationHearingRecord from './TerminationHearingRecord.js';
|
import createTerminationHearingRecord from './TerminationHearingRecord.js';
|
||||||
import createFffClearance from './FffClearance.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';
|
import createDealerBankDetail from './DealerBankDetail.js';
|
||||||
|
|
||||||
// Batch 1: Organizational Hierarchy & User Management
|
// Batch 1: Organizational Hierarchy & User Management
|
||||||
@ -147,6 +152,11 @@ db.State = createState(sequelize);
|
|||||||
db.TerminationScnResponse = createTerminationScnResponse(sequelize);
|
db.TerminationScnResponse = createTerminationScnResponse(sequelize);
|
||||||
db.TerminationHearingRecord = createTerminationHearingRecord(sequelize);
|
db.TerminationHearingRecord = createTerminationHearingRecord(sequelize);
|
||||||
db.FffClearance = createFffClearance(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);
|
db.DealerBankDetail = createDealerBankDetail(sequelize);
|
||||||
|
|
||||||
// Batch 1: Organizational Hierarchy & User Management
|
// Batch 1: Organizational Hierarchy & User Management
|
||||||
|
|||||||
@ -52,6 +52,8 @@ const ACTION_DESCRIPTIONS: Record<string, string> = {
|
|||||||
PAYMENT_UPDATED: 'Payment record updated',
|
PAYMENT_UPDATED: 'Payment record updated',
|
||||||
SECURITY_DEPOSIT_UPDATED: 'Security deposit updated',
|
SECURITY_DEPOSIT_UPDATED: 'Security deposit updated',
|
||||||
FNF_UPDATED: 'F&F settlement 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_CREATED: 'User account created',
|
||||||
USER_UPDATED: 'User account updated',
|
USER_UPDATED: 'User account updated',
|
||||||
USER_STATUS_CHANGED: 'User status changed',
|
USER_STATUS_CHANGED: 'User status changed',
|
||||||
@ -71,6 +73,7 @@ const ACTION_DESCRIPTIONS: Record<string, string> = {
|
|||||||
export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { entityType, entityId, page = '1', limit = '50' } = req.query;
|
const { entityType, entityId, page = '1', limit = '50' } = req.query;
|
||||||
|
console.log(`[AuditController] Fetching logs for ${entityType} ID: ${entityId}`);
|
||||||
|
|
||||||
if (!entityType || !entityId) {
|
if (!entityType || !entityId) {
|
||||||
return res.status(400).json({
|
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 limitNum = Math.min(100, Math.max(1, parseInt(limit as string)));
|
||||||
const offset = (pageNum - 1) * limitNum;
|
const offset = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
const { count, rows: logs } = await AuditLog.findAndCountAll({
|
let count = 0;
|
||||||
where: {
|
let logs: any[] = [];
|
||||||
entityType: entityType as string,
|
|
||||||
entityId: entityId as string
|
|
||||||
},
|
|
||||||
include: [{
|
|
||||||
model: User,
|
|
||||||
as: 'user',
|
|
||||||
attributes: ['id', 'fullName', 'email']
|
|
||||||
}],
|
|
||||||
order: [['createdAt', 'DESC']],
|
|
||||||
limit: limitNum,
|
|
||||||
offset
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 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 {
|
return {
|
||||||
id: logData.id,
|
id: logData.id,
|
||||||
action: logData.action,
|
action: logData.action,
|
||||||
description: ACTION_DESCRIPTIONS[logData.action] || logData.action,
|
description: baseDescription,
|
||||||
entityType: logData.entityType,
|
entityType: entityType,
|
||||||
entityId: logData.entityId,
|
entityId: entityId,
|
||||||
userName: logData.user?.fullName || 'System',
|
userName: logData.user?.fullName || 'System',
|
||||||
userEmail: logData.user?.email || null,
|
userEmail: logData.user?.email || null,
|
||||||
oldData: logData.oldData,
|
remarks: logData.remarks || logData.newData?.remarks,
|
||||||
newData: logData.newData,
|
newData: details, // Normalize module-specific 'details' to 'newData' for UI
|
||||||
changes: formatChanges(logData.oldData, logData.newData),
|
|
||||||
ipAddress: logData.ipAddress,
|
|
||||||
timestamp: logData.createdAt
|
timestamp: logData.createdAt
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -147,25 +211,54 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalLogs = await AuditLog.count({
|
let totalLogs = 0;
|
||||||
where: {
|
let latestLog: any = null;
|
||||||
entityType: entityType as string,
|
const type = (entityType as string).toLowerCase();
|
||||||
entityId: entityId as string
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const latestLog = await AuditLog.findOne({
|
// Dynamic Table Switching
|
||||||
where: {
|
if (type === 'resignation') {
|
||||||
entityType: entityType as string,
|
totalLogs = await db.ResignationAudit.count({ where: { resignationId: entityId as string } });
|
||||||
entityId: entityId as string
|
latestLog = await db.ResignationAudit.findOne({
|
||||||
},
|
where: { resignationId: entityId as string },
|
||||||
include: [{
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
model: User,
|
order: [['createdAt', 'DESC']]
|
||||||
as: 'user',
|
});
|
||||||
attributes: ['id', 'fullName']
|
} else if (type === 'termination') {
|
||||||
}],
|
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: entityId as string } });
|
||||||
order: [['createdAt', 'DESC']]
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -173,7 +266,7 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => {
|
|||||||
totalEntries: totalLogs,
|
totalEntries: totalLogs,
|
||||||
lastActivity: latestLog ? {
|
lastActivity: latestLog ? {
|
||||||
action: (latestLog as any).action,
|
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',
|
user: (latestLog as any).user?.fullName || 'System',
|
||||||
timestamp: (latestLog as any).createdAt
|
timestamp: (latestLog as any).createdAt
|
||||||
} : null
|
} : null
|
||||||
|
|||||||
@ -65,11 +65,11 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
|
|||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
await outlet.update({ status: 'Pending Resignation' }, { transaction });
|
await outlet.update({ status: 'Pending Resignation' }, { transaction });
|
||||||
await db.AuditLog.create({
|
await db.ResignationAudit.create({
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
action: AUDIT_ACTIONS.CREATED,
|
action: AUDIT_ACTIONS.CREATED,
|
||||||
entityType: 'resignation',
|
resignationId: resignation.id,
|
||||||
entityId: resignation.id
|
remarks: 'Dealer submitted resignation request'
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
@ -265,7 +265,7 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
const dealerProfileId = (resignation as any).dealer?.dealerId;
|
const dealerProfileId = (resignation as any).dealer?.dealerId;
|
||||||
|
|
||||||
const fnf = await db.FnF.create({
|
const fnf = await db.FnF.create({
|
||||||
settlementId: NomenclatureService.generateSettlementId(),
|
settlementId: NomenclatureService.generateFnFId(),
|
||||||
resignationId: resignation.id,
|
resignationId: resignation.id,
|
||||||
outletId: resignation.outletId,
|
outletId: resignation.outletId,
|
||||||
dealerId: dealerProfileId, // Correctly using the Dealer model ID
|
dealerId: dealerProfileId, // Correctly using the Dealer model ID
|
||||||
@ -483,6 +483,15 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
}]
|
}]
|
||||||
}, { transaction });
|
}, { 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
|
// Sync with F&F Clearance if settlement exists
|
||||||
const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id } });
|
const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id } });
|
||||||
if (fnf) {
|
if (fnf) {
|
||||||
@ -520,6 +529,15 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
}, { transaction });
|
}, { 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 there's an amount, create/update line item
|
||||||
if (amount > 0) {
|
if (amount > 0) {
|
||||||
const existingItem = await db.FnFLineItem.findOne({
|
const existingItem = await db.FnFLineItem.findOne({
|
||||||
@ -594,7 +612,7 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n
|
|||||||
case 'pushfnf':
|
case 'pushfnf':
|
||||||
// Verify if user role is authorized for manual jump to F&F
|
// 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];
|
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' });
|
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
|
// Jump directly to F&F Initiation
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import { Request, Response } from 'express';
|
|||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { FinancePayment, FnF, Application, Resignation, User, Outlet, FnFLineItem, TerminationRequest, FffClearance, AuditLog } = db;
|
const { FinancePayment, FnF, Application, Resignation, User, Outlet, FnFLineItem, TerminationRequest, FffClearance, AuditLog } = db;
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
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) => {
|
export const getDepartments = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
@ -92,8 +94,20 @@ export const updateFnF = async (req: AuthRequest, res: Response) => {
|
|||||||
newData: { status, netAmount: finalSettlementAmount, remarks }
|
newData: { status, netAmount: finalSettlementAmount, remarks }
|
||||||
});
|
});
|
||||||
|
|
||||||
// If status is being set to Completed, we might want to trigger additional logic here
|
// If status is being set to Completed, update the parent request status as well
|
||||||
// like notifying the dealer or updating the resignation status if it's not already
|
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 });
|
res.json({ success: true, message: 'F&F settlement updated successfully', data: fnf });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -108,6 +122,7 @@ export const getFnFSettlements = async (req: Request, res: Response) => {
|
|||||||
include: [
|
include: [
|
||||||
{ model: Resignation, as: 'resignation', attributes: ['id', 'resignationId'] },
|
{ model: Resignation, as: 'resignation', attributes: ['id', 'resignationId'] },
|
||||||
{ model: TerminationRequest, as: 'terminationRequest', attributes: ['id', 'status', 'category'] },
|
{ 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: Outlet, as: 'outlet', include: [{ model: User, as: 'dealer', attributes: ['fullName', 'id'] }] },
|
||||||
{ model: FnFLineItem, as: 'lineItems' },
|
{ model: FnFLineItem, as: 'lineItems' },
|
||||||
{ model: FffClearance, as: 'clearances' }
|
{ model: FffClearance, as: 'clearances' }
|
||||||
@ -171,7 +186,7 @@ export const addLineItem = async (req: AuthRequest, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update FnF progress and department statuses
|
// Update FnF progress and department statuses
|
||||||
await calculateFnFLogic(id);
|
await calculateFnFLogic(id as string, req.user?.id);
|
||||||
|
|
||||||
await AuditLog.create({
|
await AuditLog.create({
|
||||||
userId: req.user?.id || null,
|
userId: req.user?.id || null,
|
||||||
@ -196,7 +211,8 @@ export const updateLineItem = async (req: AuthRequest, res: Response) => {
|
|||||||
await lineItem.update({ description, department, amount });
|
await lineItem.update({ description, department, amount });
|
||||||
|
|
||||||
// Update FnF progress and department statuses
|
// 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({
|
await AuditLog.create({
|
||||||
userId: req.user?.id || null,
|
userId: req.user?.id || null,
|
||||||
@ -222,7 +238,8 @@ export const deleteLineItem = async (req: AuthRequest, res: Response) => {
|
|||||||
await lineItem.destroy();
|
await lineItem.destroy();
|
||||||
|
|
||||||
// Update FnF progress and department statuses
|
// Update FnF progress and department statuses
|
||||||
await calculateFnFLogic(fnfId);
|
// Update FnF progress and department statuses
|
||||||
|
await calculateFnFLogic(fnfId, req.user?.id);
|
||||||
|
|
||||||
await AuditLog.create({
|
await AuditLog.create({
|
||||||
userId: req.user?.id || null,
|
userId: req.user?.id || null,
|
||||||
@ -239,7 +256,7 @@ export const deleteLineItem = async (req: AuthRequest, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper to calculate and update FnF progress
|
// 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, {
|
const fnf = await FnF.findByPk(id, {
|
||||||
include: [{ model: FnFLineItem, as: 'lineItems' }, { model: FffClearance, as: 'clearances' }]
|
include: [{ model: FnFLineItem, as: 'lineItems' }, { model: FffClearance, as: 'clearances' }]
|
||||||
});
|
});
|
||||||
@ -302,6 +319,29 @@ const calculateFnFLogic = async (id: string) => {
|
|||||||
progressPercentage
|
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;
|
return fnf;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -322,15 +362,43 @@ export const updateClearance = async (req: AuthRequest, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Automatically update FnF progress
|
// 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({
|
// 1. Local F&F Audit (Dedicated Table: fnf_audit_logs)
|
||||||
userId: req.user?.id || null,
|
try {
|
||||||
action: AUDIT_ACTIONS.FNF_UPDATED,
|
console.log(`[SettlementController] Creating FnFAudit for ${id}`);
|
||||||
entityType: 'fnf',
|
await db.FnFAudit.create({
|
||||||
entityId: id,
|
userId: req.user?.id || null,
|
||||||
newData: { action: 'UPDATE_CLEARANCE', department: clearance.department, status, remarks }
|
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 });
|
res.json({ success: true, message: 'Clearance updated successfully', clearance });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -342,7 +410,7 @@ export const updateClearance = async (req: AuthRequest, res: Response) => {
|
|||||||
export const calculateFnF = async (req: AuthRequest, res: Response) => {
|
export const calculateFnF = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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' });
|
if (!fnf) return res.status(404).json({ success: false, message: 'F&F not found' });
|
||||||
|
|
||||||
res.json({ success: true, fnf });
|
res.json({ success: true, fnf });
|
||||||
|
|||||||
@ -38,11 +38,11 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N
|
|||||||
}]
|
}]
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
await db.AuditLog.create({
|
await db.TerminationAudit.create({
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
action: AUDIT_ACTIONS.CREATED,
|
action: AUDIT_ACTIONS.CREATED,
|
||||||
entityType: 'termination',
|
terminationRequestId: termination.id,
|
||||||
entityId: termination.id
|
remarks: 'Admin initiated termination request'
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
await transaction.commit();
|
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,
|
userId: req.user.id,
|
||||||
action: AUDIT_ACTIONS.UPDATED,
|
action: 'CLEARANCE_UPDATED',
|
||||||
entityType: 'termination',
|
terminationRequestId: id,
|
||||||
entityId: id,
|
remarks: remarks || `Cleared ${department}`,
|
||||||
newData: { department, status, amount }
|
details: { department, status, amount }
|
||||||
}, { transaction });
|
}, { 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();
|
await transaction.commit();
|
||||||
res.json({ success: true, message: `Clearance updated for ${department}`, clearances });
|
res.json({ success: true, message: `Clearance updated for ${department}`, clearances });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -30,11 +30,12 @@ export class ConstitutionalWorkflowService {
|
|||||||
await request.update(updateData);
|
await request.update(updateData);
|
||||||
|
|
||||||
// Audit Log
|
// Audit Log
|
||||||
await db.AuditLog.create({
|
await db.ConstitutionalAudit.create({
|
||||||
userId,
|
userId,
|
||||||
|
constitutionalChangeId: request.id,
|
||||||
action: action === 'Reject' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.UPDATED,
|
action: action === 'Reject' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.UPDATED,
|
||||||
entityType: 'constitutional_change',
|
remarks: remarks || '',
|
||||||
entityId: request.id
|
details: { status: updateData.status, stage: targetStage }
|
||||||
});
|
});
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
|
|||||||
@ -42,12 +42,12 @@ export class RelocationWorkflowService {
|
|||||||
await request.update({ timeline: updatedTimeline });
|
await request.update({ timeline: updatedTimeline });
|
||||||
|
|
||||||
// 3. Create Audit Log
|
// 3. Create Audit Log
|
||||||
await AuditLog.create({
|
await db.RelocationAudit.create({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
relocationRequestId: request.id,
|
||||||
action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED,
|
action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED,
|
||||||
entityType: 'relocation',
|
remarks: reason || '',
|
||||||
entityId: request.id,
|
details: { status: targetStatus, stage: stage || request.currentStage }
|
||||||
newData: { status: targetStatus, stage: stage || request.currentStage, reason }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`);
|
console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import db from '../database/models/index.js';
|
|||||||
const { AuditLog, User, Worknote } = db;
|
const { AuditLog, User, Worknote } = db;
|
||||||
import { AUDIT_ACTIONS, RESIGNATION_STAGES, ROLES } from '../common/config/constants.js';
|
import { AUDIT_ACTIONS, RESIGNATION_STAGES, ROLES } from '../common/config/constants.js';
|
||||||
import { NotificationService } from './NotificationService.js';
|
import { NotificationService } from './NotificationService.js';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
import logger from '../common/utils/logger.js';
|
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 === 'WITHDRAW' || action === 'Withdrawn') auditAction = AUDIT_ACTIONS.UPDATED;
|
||||||
if (action === 'SENT_BACK' || action === 'Sent Back') auditAction = AUDIT_ACTIONS.UPDATED;
|
if (action === 'SENT_BACK' || action === 'Sent Back') auditAction = AUDIT_ACTIONS.UPDATED;
|
||||||
|
|
||||||
await AuditLog.create({
|
await db.ResignationAudit.create({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
resignationId: resignation.id,
|
||||||
action: auditAction,
|
action: auditAction,
|
||||||
entityType: 'resignation',
|
remarks: remarks || '',
|
||||||
entityId: resignation.id,
|
details: { status: updateData.status, stage: targetStage }
|
||||||
newData: { status: updateData.status, stage: targetStage, remarks }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Create Worknote if it's a "Sent Back" action for communication
|
// 4. Create Worknote if it's a "Sent Back" action for communication
|
||||||
@ -64,8 +65,10 @@ export class ResignationWorkflowService {
|
|||||||
// 5. Send Notifications
|
// 5. Send Notifications
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
where: {
|
where: {
|
||||||
dealerId: resignation.dealerId,
|
[Op.or]: [
|
||||||
roleCode: ROLES.DEALER
|
{ id: resignation.dealerId },
|
||||||
|
{ dealerId: resignation.dealerId }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,7 +94,7 @@ export class ResignationWorkflowService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
return resignation;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import db from '../database/models/index.js';
|
import db from '../database/models/index.js';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
const { AuditLog, User, TerminationScnResponse, TerminationHearingRecord, Dealer, FnF, FnFLineItem, FffClearance } = db;
|
const { AuditLog, User, TerminationScnResponse, TerminationHearingRecord, Dealer, FnF, FnFLineItem, FffClearance } = db;
|
||||||
import { AUDIT_ACTIONS, TERMINATION_STAGES, ROLES, FNF_DEPARTMENTS } from '../common/config/constants.js';
|
import { AUDIT_ACTIONS, TERMINATION_STAGES, ROLES, FNF_DEPARTMENTS } from '../common/config/constants.js';
|
||||||
import { NotificationService } from './NotificationService.js';
|
import { NotificationService } from './NotificationService.js';
|
||||||
@ -40,19 +41,21 @@ export class TerminationWorkflowService {
|
|||||||
if (action === 'REJECT' || action === 'Rejected') auditAction = AUDIT_ACTIONS.REJECTED;
|
if (action === 'REJECT' || action === 'Rejected') auditAction = AUDIT_ACTIONS.REJECTED;
|
||||||
if (action === 'SCN_SUBMITTED' || action === 'Hearing Recorded') auditAction = AUDIT_ACTIONS.UPDATED;
|
if (action === 'SCN_SUBMITTED' || action === 'Hearing Recorded') auditAction = AUDIT_ACTIONS.UPDATED;
|
||||||
|
|
||||||
await AuditLog.create({
|
await db.TerminationAudit.create({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
terminationRequestId: termination.id,
|
||||||
action: auditAction,
|
action: auditAction,
|
||||||
entityType: 'termination',
|
remarks: remarks || '',
|
||||||
entityId: termination.id,
|
details: { status: updateData.status, stage: targetStage }
|
||||||
newData: { status: updateData.status, stage: targetStage, remarks }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Send Notifications
|
// 4. Send Notifications
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
where: {
|
where: {
|
||||||
dealerId: termination.dealerId,
|
[Op.or]: [
|
||||||
roleCode: ROLES.DEALER
|
{ id: termination.dealerId },
|
||||||
|
{ dealerId: termination.dealerId }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,7 +82,7 @@ export class TerminationWorkflowService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
return termination;
|
||||||
@ -104,7 +107,7 @@ export class TerminationWorkflowService {
|
|||||||
|
|
||||||
// 2. Create FnF Settlement Record with direct IDs and readable identifier
|
// 2. Create FnF Settlement Record with direct IDs and readable identifier
|
||||||
const fnf = await FnF.create({
|
const fnf = await FnF.create({
|
||||||
settlementId: NomenclatureService.generateSettlementId(),
|
settlementId: NomenclatureService.generateFnFId(),
|
||||||
terminationRequestId: termination.id,
|
terminationRequestId: termination.id,
|
||||||
dealerId: termination.dealerId,
|
dealerId: termination.dealerId,
|
||||||
outletId: primaryOutlet?.id || null,
|
outletId: primaryOutlet?.id || null,
|
||||||
|
|||||||
20
sync_audit.ts
Normal file
20
sync_audit.ts
Normal file
@ -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();
|
||||||
@ -218,16 +218,27 @@ async function run() {
|
|||||||
const financeToken = await login(EMAILS.FINANCE);
|
const financeToken = await login(EMAILS.FINANCE);
|
||||||
await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', {
|
await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', {
|
||||||
status: 'Completed',
|
status: 'Completed',
|
||||||
finalSettlementAmount: 0,
|
finalSettlementAmount: 415173, // Matches your observed amount
|
||||||
remarks: 'Settlement completed'
|
paymentMode: 'NEFT / Bank Transfer',
|
||||||
|
transactionReference: `TXN-${Date.now()}`,
|
||||||
|
settlementDate: new Date().toISOString(),
|
||||||
|
remarks: 'Settlement completed and verified via automated script.'
|
||||||
}, financeToken);
|
}, financeToken);
|
||||||
await delay();
|
await delay();
|
||||||
|
|
||||||
// --- FINAL COMPLETION ---
|
// --- FINAL COMPLETION ---
|
||||||
console.log('[STEP 11] Moving Resignation to COMPLETED...');
|
console.log('[STEP 11] Verifying Resignation is now COMPLETED (Auto-transitioned)...');
|
||||||
await apiRequest(`/self-service/resignations/${resignationId}/approve`, 'PUT', {
|
const finalStatusRes = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken);
|
||||||
remarks: 'Final resignation completion.'
|
if (finalStatusRes.resignation.status === 'Completed') {
|
||||||
}, adminToken);
|
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();
|
await delay();
|
||||||
|
|
||||||
// [FINAL STEP] Verification of deactivation
|
// [FINAL STEP] Verification of deactivation
|
||||||
@ -237,9 +248,7 @@ async function run() {
|
|||||||
const userRes = await apiRequest('/admin/users', 'GET', null, adminToken);
|
const userRes = await apiRequest('/admin/users', 'GET', null, adminToken);
|
||||||
|
|
||||||
// Fetch dealer to get its associated user ID
|
// Fetch dealer to get its associated user ID
|
||||||
const dealersRes = await apiRequest('/dealer', 'GET', null, adminToken);
|
const dealerU = userRes.data.find(u => u.email === targetApp.email);
|
||||||
const targetD = dealersRes.data.find(d => d.id === targetOutlet.dealerId);
|
|
||||||
const dealerU = userRes.data.find(u => u.id === targetD.user?.id);
|
|
||||||
|
|
||||||
if (dealerU && (dealerU.status === 'deactivated' || !dealerU.isActive)) {
|
if (dealerU && (dealerU.status === 'deactivated' || !dealerU.isActive)) {
|
||||||
console.log(`[VERIFICATION] SUCCESS: Account ${dealerU.email} is deactivated. Status: ${dealerU.status}`);
|
console.log(`[VERIFICATION] SUCCESS: Account ${dealerU.email} is deactivated. Status: ${dealerU.status}`);
|
||||||
|
|||||||
@ -156,7 +156,7 @@ async function run() {
|
|||||||
|
|
||||||
// Fetch user data to verify deactivation
|
// Fetch user data to verify deactivation
|
||||||
const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken);
|
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') {
|
if (dealerUser && !dealerUser.isActive && dealerUser.status === 'deactivated') {
|
||||||
console.log(`[VERIFICATION] Account ${dealerUser.email} successfully DEACTIVATED.`);
|
console.log(`[VERIFICATION] Account ${dealerUser.email} successfully DEACTIVATED.`);
|
||||||
|
|||||||
@ -114,7 +114,7 @@ async function prospectLogin(phone) {
|
|||||||
|
|
||||||
async function mockUploadDocument(appId, token, docType) {
|
async function mockUploadDocument(appId, token, docType) {
|
||||||
const formData = new FormData();
|
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' });
|
const blob = new Blob([fileBuffer], { type: 'image/png' });
|
||||||
formData.append('file', blob, 'screenshot.png');
|
formData.append('file', blob, 'screenshot.png');
|
||||||
formData.append('documentType', docType);
|
formData.append('documentType', docType);
|
||||||
@ -467,7 +467,7 @@ async function triggerWorkflow() {
|
|||||||
status: 'Completed',
|
status: 'Completed',
|
||||||
overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.'
|
overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.'
|
||||||
}, adminToken);
|
}, adminToken);
|
||||||
|
|
||||||
// Status check
|
// Status check
|
||||||
const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken);
|
const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken);
|
||||||
log(11.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`);
|
log(11.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user