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:
parent
28580b7fb0
commit
115e978eb8
@ -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
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -14,7 +14,8 @@ export const ROLES = {
|
||||
ASM: 'ASM',
|
||||
FINANCE: 'Finance',
|
||||
DEALER: 'Dealer',
|
||||
ARCHITECTURE: 'ARCHITECTURE'
|
||||
ARCHITECTURE: 'ARCHITECTURE',
|
||||
FDD: 'FDD'
|
||||
} as const;
|
||||
|
||||
// Regions
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
|
||||
93
src/database/models/ConstitutionalDocument.ts
Normal file
93
src/database/models/ConstitutionalDocument.ts
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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' });
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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' });
|
||||
};
|
||||
|
||||
|
||||
@ -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' });
|
||||
};
|
||||
|
||||
|
||||
@ -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' });
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
93
src/database/models/RelocationDocument.ts
Normal file
93
src/database/models/RelocationDocument.ts
Normal 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;
|
||||
};
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
93
src/database/models/ResignationDocument.ts
Normal file
93
src/database/models/ResignationDocument.ts
Normal 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;
|
||||
};
|
||||
@ -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' });
|
||||
};
|
||||
|
||||
|
||||
93
src/database/models/TerminationDocument.ts
Normal file
93
src/database/models/TerminationDocument.ts
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user