now chat implementation menhanced ui created for FDD from he can upload the rlevenat documents dedicated documents creted for each service

This commit is contained in:
laxman h 2026-04-03 20:30:20 +05:30
parent 28580b7fb0
commit 115e978eb8
31 changed files with 652 additions and 99 deletions

View File

@ -38,6 +38,13 @@ const policies = [
approvalMode: 'ROLE_MANDATORY',
requiredRoles: ['DD Head', 'NBH'],
isActive: true
},
{
stageCode: 'FDD_VERIFICATION',
minApprovals: 1,
approvalMode: 'ROLE_MANDATORY',
requiredRoles: ['FDD'],
isActive: true
}
];

View File

@ -16,7 +16,9 @@ const rolesToSeed = [
{ roleCode: ROLES.LEGAL_ADMIN, roleName: 'Legal Admin', category: 'DEPARTMENT', description: 'Legal Department' },
{ roleCode: ROLES.NBH, roleName: 'NBH', category: 'NATIONAL', description: 'National Business Head' },
{ roleCode: ROLES.ASM, roleName: 'ASM', category: 'SALES', description: 'Area Sales Manager' },
{ roleCode: ROLES.DEALER, roleName: 'Dealer', category: 'EXTERNAL', description: 'Dealer Principal' }
{ roleCode: ROLES.DEALER, roleName: 'Dealer', category: 'EXTERNAL', description: 'Dealer Principal' },
{ roleCode: ROLES.FDD, roleName: 'FDD Team', category: 'EXTERNAL', description: 'Financial Due Diligence Team' },
{ roleCode: ROLES.ARCHITECTURE, roleName: 'Architecture Team', category: 'DEPARTMENT', description: 'Architecture & Design Team' }
];
async function seedRoles() {

View File

@ -19,7 +19,9 @@ async function seedUsers() {
{ email: 'admin@royalenfield.com', fullName: 'Laxman H', password: hashedPassword, roleCode: ROLES.DD_LEAD, status: 'active' },
{ email: 'yashwin@gmail.com', fullName: 'Yashwin', password: hashedPassword, roleCode: ROLES.ZBH, status: 'active' },
{ email: 'kenil@gmail.com', fullName: 'Kenil', password: hashedPassword, roleCode: ROLES.DD_LEAD, status: 'active' },
{ email: 'lince@gmail.com', fullName: 'Lince', password: hashedPassword, roleCode: ROLES.DD_ADMIN, status: 'active' }
{ email: 'lince@gmail.com', fullName: 'Lince', password: hashedPassword, roleCode: ROLES.DD_ADMIN, status: 'active' },
{ email: 'fdd@royalenfield.com', fullName: 'FDD Partner', password: hashedPassword, roleCode: ROLES.FDD, status: 'active' },
{ email: 'architecture@royalenfield.com', fullName: 'RE Architect', password: hashedPassword, roleCode: ROLES.ARCHITECTURE, status: 'active' }
];
for (const u of usersToSeed) {

View File

@ -26,7 +26,8 @@ async function seed() {
{ roleCode: 'Finance', roleName: 'Finance', category: 'DEPARTMENT' },
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' },
{ roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' },
{ roleCode: 'ARCHITECTURE', roleName: 'Architecture Team', category: 'DEPARTMENT' }
{ roleCode: 'ARCHITECTURE', roleName: 'Architecture Team', category: 'DEPARTMENT' },
{ roleCode: 'FDD', roleName: 'FDD Team', category: 'EXTERNAL' }
];
for (const r of roles) {
@ -98,7 +99,9 @@ async function seed() {
{ email: 'ddhead@royalenfield.com', name: 'Vikram Singh', role: 'DD Head' },
{ email: 'finance@royalenfield.com', name: 'Rahul Verma', role: 'Finance' },
{ email: 'admin@royalenfield.com', name: 'Laxman H', role: 'Super Admin' },
{ email: 'lince@gmail.com', name: 'Lince', role: 'DD Admin' }
{ email: 'lince@gmail.com', name: 'Lince', role: 'DD Admin' },
{ email: 'fdd@royalenfield.com', name: 'FDD Partner', role: 'FDD' },
{ email: 'architecture@royalenfield.com', name: 'RE Architect', role: 'ARCHITECTURE' }
];
for (const u of nationalUsers) {
const [user] = await User.findOrCreate({

View File

@ -14,7 +14,8 @@ export const ROLES = {
ASM: 'ASM',
FINANCE: 'Finance',
DEALER: 'Dealer',
ARCHITECTURE: 'ARCHITECTURE'
ARCHITECTURE: 'ARCHITECTURE',
FDD: 'FDD'
} as const;
// Regions

View File

@ -245,11 +245,9 @@ export default (sequelize: Sequelize) => {
Application.hasMany(models.ApplicationStatusHistory, { foreignKey: 'applicationId', as: 'statusHistory' });
Application.hasMany(models.ApplicationProgress, { foreignKey: 'applicationId', as: 'progressTracking' });
Application.hasMany(models.Document, {
foreignKey: 'requestId',
as: 'uploadedDocuments',
scope: { requestType: 'application' },
constraints: false
Application.hasMany(models.OnboardingDocument, {
foreignKey: 'applicationId',
as: 'uploadedDocuments'
});
Application.hasMany(models.QuestionnaireResponse, { foreignKey: 'applicationId', as: 'questionnaireResponses' });
@ -268,6 +266,7 @@ export default (sequelize: Sequelize) => {
Application.hasMany(models.StageApprovalAction, { foreignKey: 'applicationId', as: 'stageApprovals' });
Application.hasOne(models.Dealer, { foreignKey: 'applicationId', as: 'dealer' });
Application.hasMany(models.SecurityDeposit, { foreignKey: 'applicationId', as: 'securityDeposits' });
Application.hasOne(models.EorChecklist, { foreignKey: 'applicationId', as: 'eorChecklist' });
};
return Application;

View File

@ -93,6 +93,10 @@ export default (sequelize: Sequelize) => {
foreignKey: 'dealerId',
as: 'dealer'
});
ConstitutionalChange.hasMany(models.ConstitutionalDocument, {
foreignKey: 'constitutionalChangeId',
as: 'uploadedDocuments'
});
ConstitutionalChange.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',

View File

@ -0,0 +1,93 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface ConstitutionalDocumentAttributes {
id: string;
constitutionalChangeId: string;
documentType: string;
fileName: string;
filePath: string;
fileSize: number | null;
mimeType: string | null;
stage: string | null;
status: string;
uploadedBy: string | null;
}
export interface ConstitutionalDocumentInstance extends Model<ConstitutionalDocumentAttributes>, ConstitutionalDocumentAttributes { }
export default (sequelize: Sequelize) => {
const ConstitutionalDocument = sequelize.define<ConstitutionalDocumentInstance>('ConstitutionalDocument', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
constitutionalChangeId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'constitutional_changes',
key: 'id'
}
},
documentType: {
type: DataTypes.STRING,
allowNull: false
},
fileName: {
type: DataTypes.STRING,
allowNull: false
},
filePath: {
type: DataTypes.STRING,
allowNull: false
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: true
},
mimeType: {
type: DataTypes.STRING,
allowNull: true
},
stage: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.STRING,
defaultValue: 'active'
},
uploadedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'constitutional_documents',
timestamps: true,
indexes: [
{ fields: ['constitutionalChangeId'] },
{ fields: ['status'] }
]
});
(ConstitutionalDocument as any).associate = (models: any) => {
ConstitutionalDocument.belongsTo(models.User, { foreignKey: 'uploadedBy', as: 'uploader' });
ConstitutionalDocument.belongsTo(models.ConstitutionalChange, { foreignKey: 'constitutionalChangeId', as: 'constitutionalChange' });
ConstitutionalDocument.hasMany(models.DocumentVersion, { foreignKey: 'documentId', as: 'versions', constraints: false });
ConstitutionalDocument.belongsToMany(models.Worknote, {
through: models.WorkNoteAttachment,
foreignKey: 'documentId',
otherKey: 'noteId',
as: 'worknotes',
constraints: false
});
};
return ConstitutionalDocument;
};

View File

@ -77,13 +77,26 @@ export default (sequelize: Sequelize) => {
});
(Dealer as any).associate = (models: any) => {
Dealer.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
Dealer.belongsTo(models.DealerCode, { foreignKey: 'dealerCodeId', as: 'dealerCode' });
const { Application, DealerCode, OnboardingDocument, Resignation, TerminationRequest, User } = models;
Dealer.belongsTo(Application, { foreignKey: 'applicationId', as: 'application' });
Dealer.belongsTo(DealerCode, { foreignKey: 'dealerCodeId', as: 'dealerCode' });
Dealer.hasMany(models.Document, { foreignKey: 'dealerId', as: 'documents' });
Dealer.hasMany(models.Resignation, { foreignKey: 'dealerId', as: 'resignations' });
Dealer.hasMany(models.TerminationRequest, { foreignKey: 'dealerId', as: 'terminationRequests' });
Dealer.hasOne(models.User, { foreignKey: 'dealerId', as: 'user' });
if (OnboardingDocument) {
Dealer.hasMany(OnboardingDocument, { foreignKey: 'dealerId', as: 'onboardingDocuments' });
}
if (Resignation) {
Dealer.hasMany(Resignation, { foreignKey: 'dealerId', as: 'resignations' });
}
if (TerminationRequest) {
Dealer.hasMany(TerminationRequest, { foreignKey: 'dealerId', as: 'terminationRequests' });
}
if (User) {
Dealer.hasOne(User, { foreignKey: 'dealerId', as: 'user' });
}
};
return Dealer;

View File

@ -3,6 +3,7 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface DocumentVersionAttributes {
id: string;
documentId: string;
documentType: string;
versionNumber: number;
filePath: string;
uploadedBy: string | null;
@ -19,11 +20,11 @@ export default (sequelize: Sequelize) => {
},
documentId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'documents',
key: 'id'
}
allowNull: false
},
documentType: {
type: DataTypes.STRING,
allowNull: false
},
versionNumber: {
type: DataTypes.INTEGER,
@ -48,7 +49,6 @@ export default (sequelize: Sequelize) => {
});
(DocumentVersion as any).associate = (models: any) => {
DocumentVersion.belongsTo(models.Document, { foreignKey: 'documentId', as: 'document' });
DocumentVersion.belongsTo(models.User, { foreignKey: 'uploadedBy', as: 'uploader' });
};

View File

@ -45,11 +45,7 @@ export default (sequelize: Sequelize) => {
},
proofDocumentId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'documents',
key: 'id'
}
allowNull: true
}
}, {
tableName: 'eor_checklist_items',
@ -58,7 +54,6 @@ export default (sequelize: Sequelize) => {
(EorChecklistItem as any).associate = (models: any) => {
EorChecklistItem.belongsTo(models.EorChecklist, { foreignKey: 'checklistId', as: 'checklist' });
EorChecklistItem.belongsTo(models.Document, { foreignKey: 'proofDocumentId', as: 'proofDocument' });
};
return EorChecklistItem;

View File

@ -31,7 +31,7 @@ export default (sequelize: Sequelize) => {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'documents',
model: 'onboarding_documents',
key: 'id'
}
},
@ -62,7 +62,7 @@ export default (sequelize: Sequelize) => {
(FddReport as any).associate = (models: any) => {
FddReport.belongsTo(models.FddAssignment, { foreignKey: 'assignmentId', as: 'assignment' });
FddReport.belongsTo(models.Document, { foreignKey: 'reportDocumentId', as: 'reportDocument' });
FddReport.belongsTo(models.OnboardingDocument, { foreignKey: 'reportDocumentId', as: 'reportDocument' });
FddReport.belongsTo(models.User, { foreignKey: 'verifiedBy', as: 'verifier' });
};

View File

@ -29,7 +29,7 @@ export default (sequelize: Sequelize) => {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'documents',
model: 'onboarding_documents',
key: 'id'
}
},
@ -49,7 +49,7 @@ export default (sequelize: Sequelize) => {
(LoaDocumentGenerated as any).associate = (models: any) => {
LoaDocumentGenerated.belongsTo(models.LoaRequest, { foreignKey: 'requestId', as: 'request' });
LoaDocumentGenerated.belongsTo(models.Document, { foreignKey: 'documentId', as: 'document' });
LoaDocumentGenerated.belongsTo(models.OnboardingDocument, { foreignKey: 'documentId', as: 'document' });
LoaDocumentGenerated.hasMany(models.LoaAcknowledgement, { foreignKey: 'loaDocId', as: 'acknowledgements' });
};

View File

@ -29,7 +29,7 @@ export default (sequelize: Sequelize) => {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'documents',
model: 'onboarding_documents',
key: 'id'
}
},
@ -49,7 +49,7 @@ export default (sequelize: Sequelize) => {
(LoiDocumentGenerated as any).associate = (models: any) => {
LoiDocumentGenerated.belongsTo(models.LoiRequest, { foreignKey: 'requestId', as: 'request' });
LoiDocumentGenerated.belongsTo(models.Document, { foreignKey: 'documentId', as: 'document' });
LoiDocumentGenerated.belongsTo(models.OnboardingDocument, { foreignKey: 'documentId', as: 'document' });
LoiDocumentGenerated.hasMany(models.LoiAcknowledgement, { foreignKey: 'loiDocId', as: 'acknowledgements' });
};

View File

@ -17,10 +17,10 @@ export interface DocumentAttributes {
uploadedBy: string | null;
}
export interface DocumentInstance extends Model<DocumentAttributes>, DocumentAttributes { }
export interface OnboardingDocumentInstance extends Model<DocumentAttributes>, DocumentAttributes { }
export default (sequelize: Sequelize) => {
const Document = sequelize.define<DocumentInstance>('Document', {
const OnboardingDocument = sequelize.define<OnboardingDocumentInstance>('OnboardingDocument', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
@ -87,7 +87,7 @@ export default (sequelize: Sequelize) => {
}
}
}, {
tableName: 'documents',
tableName: 'onboarding_documents',
timestamps: true,
indexes: [
{ fields: ['applicationId'] },
@ -96,19 +96,20 @@ export default (sequelize: Sequelize) => {
]
});
(Document as any).associate = (models: any) => {
Document.belongsTo(models.User, { foreignKey: 'uploadedBy', as: 'uploader' });
Document.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
Document.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealer' });
(OnboardingDocument as any).associate = (models: any) => {
OnboardingDocument.belongsTo(models.User, { foreignKey: 'uploadedBy', as: 'uploader' });
OnboardingDocument.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
OnboardingDocument.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealer' });
Document.hasMany(models.DocumentVersion, { foreignKey: 'documentId', as: 'versions' });
Document.belongsToMany(models.Worknote, {
OnboardingDocument.hasMany(models.DocumentVersion, { foreignKey: 'documentId', as: 'versions', constraints: false });
OnboardingDocument.belongsToMany(models.Worknote, {
through: models.WorkNoteAttachment,
foreignKey: 'documentId',
otherKey: 'noteId',
as: 'workNotes'
as: 'workNotes',
constraints: false
});
};
return Document;
return OnboardingDocument;
};

View File

@ -0,0 +1,93 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface RelocationDocumentAttributes {
id: string;
relocationId: string;
documentType: string;
fileName: string;
filePath: string;
fileSize: number | null;
mimeType: string | null;
stage: string | null;
status: string;
uploadedBy: string | null;
}
export interface RelocationDocumentInstance extends Model<RelocationDocumentAttributes>, RelocationDocumentAttributes { }
export default (sequelize: Sequelize) => {
const RelocationDocument = sequelize.define<RelocationDocumentInstance>('RelocationDocument', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
relocationId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'relocation_requests',
key: 'id'
}
},
documentType: {
type: DataTypes.STRING,
allowNull: false
},
fileName: {
type: DataTypes.STRING,
allowNull: false
},
filePath: {
type: DataTypes.STRING,
allowNull: false
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: true
},
mimeType: {
type: DataTypes.STRING,
allowNull: true
},
stage: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.STRING,
defaultValue: 'active'
},
uploadedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'relocation_documents',
timestamps: true,
indexes: [
{ fields: ['relocationId'] },
{ fields: ['status'] }
]
});
(RelocationDocument as any).associate = (models: any) => {
RelocationDocument.belongsTo(models.User, { foreignKey: 'uploadedBy', as: 'uploader' });
RelocationDocument.belongsTo(models.RelocationRequest, { foreignKey: 'relocationId', as: 'request' });
RelocationDocument.hasMany(models.DocumentVersion, { foreignKey: 'documentId', as: 'versions', constraints: false });
RelocationDocument.belongsToMany(models.Worknote, {
through: models.WorkNoteAttachment,
foreignKey: 'documentId',
otherKey: 'noteId',
as: 'workNotes',
constraints: false
});
};
return RelocationDocument;
};

View File

@ -140,11 +140,9 @@ export default (sequelize: Sequelize) => {
scope: { requestType: 'relocation' },
constraints: false
});
RelocationRequest.hasMany(models.Document, {
foreignKey: 'requestId',
as: 'uploadedDocuments',
scope: { requestType: 'relocation' },
constraints: false
RelocationRequest.hasMany(models.RelocationDocument, {
foreignKey: 'relocationId',
as: 'uploadedDocuments'
});
RelocationRequest.hasOne(models.EorChecklist, {
foreignKey: 'relocationId',

View File

@ -134,6 +134,10 @@ export default (sequelize: Sequelize) => {
foreignKey: 'dealerId',
as: 'dealer'
});
Resignation.hasMany(models.ResignationDocument, {
foreignKey: 'resignationId',
as: 'uploadedDocuments'
});
Resignation.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',

View File

@ -0,0 +1,93 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface ResignationDocumentAttributes {
id: string;
resignationId: string;
documentType: string;
fileName: string;
filePath: string;
fileSize: number | null;
mimeType: string | null;
stage: string | null;
status: string;
uploadedBy: string | null;
}
export interface ResignationDocumentInstance extends Model<ResignationDocumentAttributes>, ResignationDocumentAttributes { }
export default (sequelize: Sequelize) => {
const ResignationDocument = sequelize.define<ResignationDocumentInstance>('ResignationDocument', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resignationId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'resignations',
key: 'id'
}
},
documentType: {
type: DataTypes.STRING,
allowNull: false
},
fileName: {
type: DataTypes.STRING,
allowNull: false
},
filePath: {
type: DataTypes.STRING,
allowNull: false
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: true
},
mimeType: {
type: DataTypes.STRING,
allowNull: true
},
stage: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.STRING,
defaultValue: 'active'
},
uploadedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'resignation_documents',
timestamps: true,
indexes: [
{ fields: ['resignationId'] },
{ fields: ['status'] }
]
});
(ResignationDocument as any).associate = (models: any) => {
ResignationDocument.belongsTo(models.User, { foreignKey: 'uploadedBy', as: 'uploader' });
ResignationDocument.belongsTo(models.Resignation, { foreignKey: 'resignationId', as: 'resignation' });
ResignationDocument.hasMany(models.DocumentVersion, { foreignKey: 'documentId', as: 'versions', constraints: false });
ResignationDocument.belongsToMany(models.Worknote, {
through: models.WorkNoteAttachment,
foreignKey: 'documentId',
otherKey: 'noteId',
as: 'worknotes',
constraints: false
});
};
return ResignationDocument;
};

View File

@ -41,7 +41,7 @@ export default (sequelize: Sequelize) => {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'documents',
model: 'onboarding_documents',
key: 'id'
}
},
@ -73,7 +73,7 @@ export default (sequelize: Sequelize) => {
(SecurityDeposit as any).associate = (models: any) => {
SecurityDeposit.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
SecurityDeposit.belongsTo(models.Document, { foreignKey: 'proofDocumentId', as: 'proofDocument' });
SecurityDeposit.belongsTo(models.OnboardingDocument, { foreignKey: 'proofDocumentId', as: 'proofDocument' });
SecurityDeposit.belongsTo(models.User, { foreignKey: 'verifiedBy', as: 'verifier' });
};

View File

@ -0,0 +1,93 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface TerminationDocumentAttributes {
id: string;
terminationRequestId: string;
documentType: string;
fileName: string;
filePath: string;
fileSize: number | null;
mimeType: string | null;
stage: string | null;
status: string;
uploadedBy: string | null;
}
export interface TerminationDocumentInstance extends Model<TerminationDocumentAttributes>, TerminationDocumentAttributes { }
export default (sequelize: Sequelize) => {
const TerminationDocument = sequelize.define<TerminationDocumentInstance>('TerminationDocument', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
terminationRequestId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'termination_requests',
key: 'id'
}
},
documentType: {
type: DataTypes.STRING,
allowNull: false
},
fileName: {
type: DataTypes.STRING,
allowNull: false
},
filePath: {
type: DataTypes.STRING,
allowNull: false
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: true
},
mimeType: {
type: DataTypes.STRING,
allowNull: true
},
stage: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.STRING,
defaultValue: 'active'
},
uploadedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'termination_documents',
timestamps: true,
indexes: [
{ fields: ['terminationRequestId'] },
{ fields: ['status'] }
]
});
(TerminationDocument as any).associate = (models: any) => {
TerminationDocument.belongsTo(models.User, { foreignKey: 'uploadedBy', as: 'uploader' });
TerminationDocument.belongsTo(models.TerminationRequest, { foreignKey: 'terminationRequestId', as: 'terminationRequest' });
TerminationDocument.hasMany(models.DocumentVersion, { foreignKey: 'documentId', as: 'versions', constraints: false });
TerminationDocument.belongsToMany(models.Worknote, {
through: models.WorkNoteAttachment,
foreignKey: 'documentId',
otherKey: 'noteId',
as: 'worknotes',
constraints: false
});
};
return TerminationDocument;
};

View File

@ -80,6 +80,16 @@ export default (sequelize: Sequelize) => {
TerminationRequest.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealer' });
TerminationRequest.belongsTo(models.User, { foreignKey: 'initiatedBy', as: 'initiator' });
TerminationRequest.hasOne(models.FnF, { foreignKey: 'terminationRequestId', as: 'fnfSettlement' });
TerminationRequest.hasMany(models.TerminationDocument, {
foreignKey: 'terminationRequestId',
as: 'uploadedDocuments'
});
TerminationRequest.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: { requestType: 'termination' },
constraints: false
});
};
return TerminationRequest;

View File

@ -4,6 +4,7 @@ export interface WorkNoteAttachmentAttributes {
id: string;
noteId: string;
documentId: string;
documentType: string;
}
export interface WorkNoteAttachmentInstance extends Model<WorkNoteAttachmentAttributes>, WorkNoteAttachmentAttributes { }
@ -25,11 +26,11 @@ export default (sequelize: Sequelize) => {
},
documentId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'documents',
key: 'id'
}
allowNull: false
},
documentType: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'work_note_attachments',
@ -39,7 +40,6 @@ export default (sequelize: Sequelize) => {
(WorkNoteAttachment as any).associate = (models: any) => {
WorkNoteAttachment.belongsTo(models.Worknote, { foreignKey: 'noteId', as: 'workNote' });
WorkNoteAttachment.belongsTo(models.Document, { foreignKey: 'documentId', as: 'document' });
};
return WorkNoteAttachment;

View File

@ -71,12 +71,7 @@ export default (sequelize: Sequelize) => {
Worknote.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
Worknote.hasMany(models.WorkNoteTag, { foreignKey: 'noteId', as: 'tags' });
Worknote.belongsToMany(models.Document, {
through: models.WorkNoteAttachment,
foreignKey: 'noteId',
otherKey: 'documentId',
as: 'attachments'
});
Worknote.hasMany(models.WorkNoteAttachment, { foreignKey: 'noteId', as: 'attachments' });
};
return Worknote;

View File

@ -9,9 +9,13 @@ import createConstitutionalChange from './ConstitutionalChange.js';
import createRelocationRequest from './RelocationRequest.js';
import createOutlet from './Outlet.js';
import createWorknote from './Worknote.js';
import createDocument from './Document.js';
import createOnboardingDocument from './OnboardingDocument.js';
import createAuditLog from './AuditLog.js';
import createFinancePayment from './FinancePayment.js';
import createRelocationDocument from './RelocationDocument.js';
import createResignationDocument from './ResignationDocument.js';
import createConstitutionalDocument from './ConstitutionalDocument.js';
import createTerminationDocument from './TerminationDocument.js';
import createFnF from './FnF.js';
import createFnFLineItem from './FnFLineItem.js';
import createSLAConfiguration from './SLAConfiguration.js';
@ -115,9 +119,13 @@ db.ConstitutionalChange = createConstitutionalChange(sequelize);
db.RelocationRequest = createRelocationRequest(sequelize);
db.Outlet = createOutlet(sequelize);
db.Worknote = createWorknote(sequelize);
db.Document = createDocument(sequelize);
db.OnboardingDocument = createOnboardingDocument(sequelize);
db.AuditLog = createAuditLog(sequelize);
db.FinancePayment = createFinancePayment(sequelize);
db.RelocationDocument = createRelocationDocument(sequelize);
db.ResignationDocument = createResignationDocument(sequelize);
db.ConstitutionalDocument = createConstitutionalDocument(sequelize);
db.TerminationDocument = createTerminationDocument(sequelize);
db.FnF = createFnF(sequelize);
db.FnFLineItem = createFnFLineItem(sequelize);
db.SLAConfiguration = createSLAConfiguration(sequelize);

View File

@ -1,6 +1,9 @@
import { Response } from 'express';
import db from '../../database/models/index.js';
const { Worknote, User, WorkNoteTag, WorkNoteAttachment, Document, DocumentVersion, RequestParticipant, Application, AuditLog } = db;
const {
Worknote, User, WorkNoteTag, WorkNoteAttachment, DocumentVersion, RequestParticipant, Application, AuditLog,
OnboardingDocument, RelocationDocument, ResignationDocument, ConstitutionalDocument, TerminationDocument
} = db;
import { AuthRequest } from '../../types/express.types.js';
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
import * as EmailService from '../../common/utils/email.service.js';
@ -8,6 +11,59 @@ import { getIO } from '../../common/utils/socket.js';
import * as NotificationService from '../../common/utils/notification.service.js';
import logger from '../../common/utils/logger.js';
// --- Helpers ---
const getDocumentModel = (requestType: string) => {
switch (requestType?.toLowerCase()) {
case 'relocation': return RelocationDocument;
case 'resignation': return ResignationDocument;
case 'constitutional': return ConstitutionalDocument;
case 'termination': return TerminationDocument;
case 'onboarding':
case 'application': return OnboardingDocument;
default: return OnboardingDocument;
}
};
const stitchWorknoteAttachments = async (worknotes: any[]) => {
const notePromises = worknotes.map(async (note: any) => {
const noteObj = note.toJSON ? note.toJSON() : note;
if (noteObj.attachments && noteObj.attachments.length > 0) {
// Group by documentType for batch fetching
const typeGroups: Record<string, string[]> = {};
noteObj.attachments.forEach((att: any) => {
const type = att.documentType || 'onboarding';
if (!typeGroups[type]) typeGroups[type] = [];
typeGroups[type].push(att.documentId);
});
const attachmentsWithFiles = [];
for (const [type, ids] of Object.entries(typeGroups)) {
const DocModel = getDocumentModel(type);
if (DocModel) {
const docs = await (DocModel as any).findAll({ where: { id: ids } });
const docsByid = new Map(docs.map((d: any) => [d.id, d.toJSON()]));
attachmentsWithFiles.push(...noteObj.attachments
.filter((att: any) => (att.documentType || 'onboarding') === type)
.map((att: any) => {
const doc = docsByid.get(att.documentId) as any;
return {
...att,
fileName: doc?.fileName || 'Unknown',
filePath: doc?.filePath || '',
mimeType: doc?.mimeType || 'application/octet-stream',
fileSize: doc?.fileSize
};
})
);
}
}
noteObj.attachments = attachmentsWithFiles;
}
return noteObj;
});
return Promise.all(notePromises);
};
// --- Worknotes ---
export const addWorknote = async (req: AuthRequest, res: Response) => {
@ -40,7 +96,11 @@ export const addWorknote = async (req: AuthRequest, res: Response) => {
if (attachmentDocIds && attachmentDocIds.length > 0) {
for (const docId of attachmentDocIds) {
await WorkNoteAttachment.create({ noteId: worknote.id, documentId: docId });
await WorkNoteAttachment.create({
noteId: worknote.id,
documentId: docId,
documentType: requestType || 'onboarding'
});
}
}
@ -64,14 +124,16 @@ export const addWorknote = async (req: AuthRequest, res: Response) => {
include: [
{ model: User, as: 'author', attributes: [['fullName', 'name'], 'email', ['roleCode', 'role']] },
{ model: WorkNoteTag, as: 'tags' },
{ model: Document, as: 'attachments' }
{ model: WorkNoteAttachment, as: 'attachments' }
]
});
const [stitchedNote] = await stitchWorknoteAttachments([fullWorknote]);
// --- Real-time & Notifications ---
try {
const io = getIO();
io.to(requestId).emit('new_worknote', fullWorknote);
io.to(requestId).emit('new_worknote', stitchedNote);
// Handle Mentions/Notifications
const notifiedUserIds = new Set<string>();
@ -131,7 +193,7 @@ export const addWorknote = async (req: AuthRequest, res: Response) => {
newData: { noteType: noteType || 'General', hasAttachments: !!(attachmentDocIds?.length) }
});
res.status(201).json({ success: true, message: 'Worknote added', data: fullWorknote });
res.status(201).json({ success: true, message: 'Worknote added', data: stitchedNote });
} catch (error) {
console.error('Add worknote error:', error);
res.status(500).json({ success: false, message: 'Error adding worknote' });
@ -147,12 +209,13 @@ export const getWorknotes = async (req: AuthRequest, res: Response) => {
include: [
{ model: User, as: 'author', attributes: [['fullName', 'name'], 'email', ['roleCode', 'role']] },
{ model: WorkNoteTag, as: 'tags' },
{ model: Document, as: 'attachments' }
{ model: WorkNoteAttachment, as: 'attachments' }
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, data: worknotes });
const finalWorknotes = await stitchWorknoteAttachments(worknotes);
res.json({ success: true, data: finalWorknotes });
} catch (error) {
res.status(500).json({ success: false, message: 'Error fetching worknotes' });
}
@ -167,9 +230,9 @@ export const uploadWorknoteAttachment = async (req: any, res: Response) => {
return res.status(400).json({ success: false, message: 'No file uploaded' });
}
const document = await Document.create({
requestId: requestId || null,
requestType: requestType || null,
const DocModel = getDocumentModel(requestType);
let createData: any = {
documentType: 'Worknote Attachment',
fileName: file.originalname,
filePath: file.path,
@ -177,11 +240,21 @@ export const uploadWorknoteAttachment = async (req: any, res: Response) => {
fileSize: file.size,
uploadedBy: req.user?.id,
status: 'active'
});
};
// Assign correct FK based on model
if (DocModel === RelocationDocument) createData.relocationId = requestId;
else if (DocModel === ResignationDocument) createData.resignationId = requestId;
else if (DocModel === ConstitutionalDocument) createData.constitutionalChangeId = requestId;
else if (DocModel === TerminationDocument) createData.terminationRequestId = requestId;
else createData.applicationId = requestId;
const document = await DocModel.create(createData);
// Create initial version
await DocumentVersion.create({
documentId: document.id,
documentType: requestType || 'onboarding',
versionNumber: 1,
filePath: file.path,
uploadedBy: req.user?.id,
@ -221,10 +294,10 @@ export const uploadDocument = async (req: AuthRequest, res: Response) => {
try {
const { applicationId, dealerId, docType, fileName, fileUrl, mimeType } = req.body;
const document = await Document.create({
const document = await OnboardingDocument.create({
applicationId,
dealerId,
docType,
documentType: docType,
fileName,
filePath: fileUrl, // Assuming URL from cloud storage
mimeType,
@ -275,7 +348,8 @@ export const uploadNewVersion = async (req: AuthRequest, res: Response) => {
});
// Update main document pointer if needed (usually main doc points to latest or metadata)
await Document.update({ filePath: fileUrl }, { where: { id: documentId } });
// For simplicity assuming onboarding if not specified
await OnboardingDocument.update({ filePath: fileUrl }, { where: { id: documentId } });
res.status(201).json({ success: true, message: 'New version uploaded' });
} catch (error) {

View File

@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import db from '../../database/models/index.js';
const { EorChecklist, EorChecklistItem, Document } = db;
const { EorChecklist, EorChecklistItem, OnboardingDocument, RelocationDocument } = db;
import { AuthRequest } from '../../types/express.types.js';
export const getChecklist = async (req: Request, res: Response) => {
@ -8,7 +8,8 @@ export const getChecklist = async (req: Request, res: Response) => {
const { applicationId, relocationId } = req.params;
let checklist = await EorChecklist.findOne({
where: relocationId ? { relocationId } : { applicationId },
include: [{ model: EorChecklistItem, as: 'items', include: ['proofDocument'] }]
// proofDocument is now polymorphic, would need manual stitch or sub-selects
include: [{ model: EorChecklistItem, as: 'items' }]
});
if (!checklist) {
@ -16,6 +17,27 @@ export const getChecklist = async (req: Request, res: Response) => {
return;
}
const items = checklist.items || [];
const proofDocIds = items.map((i: any) => i.proofDocumentId).filter(Boolean);
if (proofDocIds.length > 0) {
// Find documents from the relevant table
let docs = [];
if (relocationId) {
docs = await RelocationDocument.findAll({ where: { id: proofDocIds } });
} else {
docs = await OnboardingDocument.findAll({ where: { id: proofDocIds } });
}
// Map docs to items
const docsMap = new Map(docs.map((d: any) => [d.id, d]));
checklist = checklist.toJSON();
checklist.items = checklist.items.map((item: any) => ({
...item,
proofDocument: docsMap.get(item.proofDocumentId) || null
}));
}
res.json({ success: true, data: checklist });
} catch (error) {
console.error('Get EOR checklist error:', error);

View File

@ -139,8 +139,8 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
// MANDATORY DOCUMENT CHECK (SRS Requirement)
// Level 2+ requires minimum set of documents uploaded by applicant
if (currentApproval.level === 1 && action === 'Approved') {
const docCount = await db.Document.count({
where: { requestId: request.applicationId, requestType: 'application' }
const docCount = await db.OnboardingDocument.count({
where: { applicationId: request.applicationId }
});
if (docCount < 5) { // SRS requires 18, using 5 for functional demo
return res.status(400).json({

View File

@ -136,6 +136,16 @@ export const getApplications = async (req: AuthRequest, res: Response) => {
whereClause.email = req.user.email;
}
// Security Check: If FDD user, only show applications where they are a participant
if (req.user?.roleCode === 'FDD') {
const participantApps = await db.RequestParticipant.findAll({
where: { userId: req.user.id, requestType: 'application' },
attributes: ['requestId']
});
const appIds = participantApps.map((p: any) => p.requestId);
whereClause.id = { [Op.in]: appIds };
}
const applications = await Application.findAll({
where: whereClause,
include: [
@ -191,9 +201,10 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
include: [{ model: db.User, as: 'user', attributes: ['id', ['fullName', 'name'], 'email', ['roleCode', 'role']] }]
},
{
model: db.Document,
model: db.OnboardingDocument,
as: 'uploadedDocuments',
separate: true,
include: [{ model: db.User, as: 'uploader', attributes: ['fullName', 'roleCode'] }],
order: [['createdAt', 'DESC']]
},
{ model: db.StageApprovalAction, as: 'stageApprovals', separate: true },
@ -206,6 +217,41 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
return res.status(404).json({ success: false, message: 'Application not found' });
}
// Security Check for FDD: If user is FDD, only return restricted data
if (req.user?.roleCode === 'FDD') {
const isParticipant = await db.RequestParticipant.findOne({
where: { requestId: application.id, userId: req.user.id, requestType: 'application' }
});
if (!isParticipant) {
return res.status(403).json({ success: false, message: 'Access denied. You are not assigned to this application.' });
}
// Strip sensitive internal data for FDD
const restrictedData = application.toJSON();
delete (restrictedData as any).questionnaireResponses;
delete (restrictedData as any).stageApprovals;
delete (restrictedData as any).score;
// FDD should only see relevant documents for security
// FDD should only see relevant documents for security
// OR documents they uploaded themselves
const fddRelevantDocs = [
'GST Certificate', 'PAN Card', 'Bank Statement', 'Cancelled Check',
'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA',
'Property Documents', 'Rental Agreement', 'Firm Registration', 'CIBIL Report',
'FDD Final Audit Report', 'FDD Audit Report'
];
if (restrictedData.uploadedDocuments) {
restrictedData.uploadedDocuments = (restrictedData.uploadedDocuments as any[]).filter(
(doc: any) => fddRelevantDocs.includes(doc.documentType) || (req.user && doc.uploadedBy === req.user.id)
);
}
return res.json({ success: true, data: restrictedData });
}
// Security Check: Ensure prospective dealer controls data ownership
if (req.user?.roleCode === 'Prospective Dealer' && application.email !== req.user.email) {
return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' });
@ -268,17 +314,14 @@ export const uploadDocuments = async (req: any, res: Response) => {
}
// Create Document Record
const newDoc = await db.Document.create({
const newDoc = await db.OnboardingDocument.create({
applicationId: application.id,
requestId: application.id,
requestType: 'application',
documentType,
stage: stage || null,
fileName: file.originalname,
filePath: file.path, // Store relative path or full path as needed by your storage strategy
filePath: file.path,
fileSize: file.size,
mimeType: file.mimetype,
// For prospective users (who are applications, not in Users table), set uploadedBy to null to avoid FK violation
uploadedBy: req.user?.roleCode === 'Prospective Dealer' ? null : req.user?.id,
status: 'active'
});
@ -360,10 +403,9 @@ export const getApplicationDocuments = async (req: AuthRequest, res: Response) =
return res.status(404).json({ success: false, message: 'Application not found' });
}
const documents = await db.Document.findAll({
const documents = await db.OnboardingDocument.findAll({
where: {
requestId: application.id,
requestType: 'application',
applicationId: application.id,
status: 'active'
},
include: [

View File

@ -1,7 +1,7 @@
import { Response } from 'express';
import db from '../../database/models/index.js';
import { RelocationWorkflowService } from '../../services/RelocationWorkflowService.js';
const { RelocationRequest, Outlet, User, Worknote, District, Region, Zone, Document } = db;
const { RelocationRequest, Outlet, User, Worknote, District, Region, Zone, RelocationDocument } = db;
import { AUDIT_ACTIONS, ROLES, RELOCATION_STAGES } from '../../common/config/constants.js';
import { Op, Transaction } from 'sequelize';
import { v4 as uuidv4 } from 'uuid';
@ -236,7 +236,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
const userRoleCode = req.user?.roleCode;
// National roles see all requests
const nationalRoles = ['NBH', 'DD Lead', 'DD Head', 'Legal Admin'];
const nationalRoles = ['NBH', 'DD Lead', 'DD Head', 'Legal Admin', 'Super Admin', 'DD Admin'];
if (userRoleCode && nationalRoles.includes(userRoleCode)) {
return true;
}
@ -426,6 +426,7 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
const stageFlow: Record<string, string> = {
[RELOCATION_STAGES.ASM_REVIEW]: RELOCATION_STAGES.RBM_REVIEW,
'DD Admin Review': RELOCATION_STAGES.RBM_REVIEW, // Legacy support
[RELOCATION_STAGES.RBM_REVIEW]: RELOCATION_STAGES.DD_ZM_REVIEW,
[RELOCATION_STAGES.DD_ZM_REVIEW]: RELOCATION_STAGES.ZBH_REVIEW,
[RELOCATION_STAGES.ZBH_REVIEW]: RELOCATION_STAGES.DD_LEAD_REVIEW,
@ -526,9 +527,8 @@ export const uploadDocuments = async (req: AuthRequest, res: Response) => {
}
// Create Document Record
const newDoc = await Document.create({
requestId: request.id,
requestType: 'relocation',
const newDoc = await RelocationDocument.create({
relocationId: request.id,
documentType,
stage: stage || request.currentStage,
fileName: file.originalname,
@ -596,7 +596,7 @@ export const verifyDocument = async (req: AuthRequest, res: Response) => {
}
// Find and update the Document record
const docRecord = await Document.findByPk(documentId);
const docRecord = await RelocationDocument.findByPk(documentId);
if (docRecord) {
await docRecord.update({ status: 'Verified' });
}

View File

@ -66,6 +66,7 @@ export class RelocationWorkflowService {
const stageMapping: Record<string, string> = {
[RELOCATION_STAGES.ASM_REVIEW]: ROLES.ASM,
'DD Admin Review': ROLES.ASM, // Legacy/alias mapping for older requests
[RELOCATION_STAGES.RBM_REVIEW]: ROLES.RBM,
[RELOCATION_STAGES.DD_ZM_REVIEW]: ROLES.DD_ZM,
[RELOCATION_STAGES.ZBH_REVIEW]: ROLES.ZBH,