Dealer_Onboarding_Backend/src/modules/onboarding/onboarding.controller.ts

313 lines
12 KiB
TypeScript

import { Request, Response } from 'express';
import db from '../../database/models/index.js';
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, Region, Zone, Area } = db;
import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js';
import { v4 as uuidv4 } from 'uuid';
import { Op } from 'sequelize';
import { AuthRequest } from '../../types/express.types.js';
import { sendOpportunityEmail, sendNonOpportunityEmail } from '../../common/utils/email.service.js';
export const submitApplication = async (req: AuthRequest, res: Response) => {
try {
const {
opportunityId,
applicantName, email, phone, businessType, locationType,
preferredLocation, city, state, experienceYears, investmentCapacity,
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode
} = req.body;
// Check for duplicate email
const existingApp = await Application.findOne({ where: { email } });
if (existingApp) {
return res.status(400).json({ success: false, message: 'Application with this email already exists' });
}
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
// Fetch hierarchy from Auto-detected Area
let zoneId, regionId, areaId;
let isOpportunityAvailable = false;
// Auto-detect Area from District
if (req.body.district) {
const districtName = req.body.district;
// 1. Find District ID by Name
const districtRecord = await District.findOne({
where: { districtName: { [Op.iLike]: districtName } }
});
if (districtRecord) {
// 2. Find Active Area for this District
const today = new Date();
const validArea = await Area.findOne({
where: {
districtId: districtRecord.id,
isActive: true,
[Op.and]: [
{
[Op.or]: [
{ activeFrom: { [Op.eq]: null } },
{ activeFrom: { [Op.lte]: today } }
]
},
{
[Op.or]: [
{ activeTo: { [Op.eq]: null } },
{ activeTo: { [Op.gte]: today } }
]
}
]
}
});
if (validArea) {
areaId = validArea.id;
zoneId = validArea.zoneId;
regionId = validArea.regionId;
isOpportunityAvailable = true;
console.log(`[Auto-Match] Found Active Area ${validArea.areaName} for District: ${districtName}`);
}
}
}
// Determine Initial Status
let initialStatus = isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED;
// Auto-assign Zone/Region from District if still null (even if no opportunity found)
if (!zoneId && req.body.district) {
const districtRecord = await District.findOne({
where: { districtName: { [Op.iLike]: req.body.district } },
include: [{ model: Region, as: 'region' }, { model: Zone, as: 'zone' }]
});
if (districtRecord) {
regionId = districtRecord.regionId;
zoneId = districtRecord.zoneId;
}
}
const application = await Application.create({
opportunityId: null, // De-coupled from Opportunity table as per user request
applicationId,
applicantName,
email,
phone,
businessType,
preferredLocation,
city,
state,
experienceYears,
investmentCapacity,
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType,
currentStage: APPLICATION_STAGES.DD,
overallStatus: initialStatus,
progressPercentage: isOpportunityAvailable ? 10 : 0,
zoneId,
regionId,
areaId // Link to Area
});
// Log Status History
await ApplicationStatusHistory.create({
applicationId: application.id,
previousStatus: null,
newStatus: initialStatus,
changedBy: req.user?.id || null,
reason: 'Initial Submission'
});
// Send Email (Async)
if (isOpportunityAvailable) {
sendOpportunityEmail(email, applicantName, city || preferredLocation, applicationId)
.catch(err => console.error('Error sending opportunity email', err));
} else {
sendNonOpportunityEmail(email, applicantName, city || preferredLocation)
.catch(err => console.error('Error sending non-opportunity email', err));
}
await AuditLog.create({
userId: req.user?.id,
action: AUDIT_ACTIONS.CREATED,
entityType: 'application',
entityId: application.id
});
res.status(201).json({
success: true,
message: 'Application submitted successfully',
data: application
});
} catch (error) {
console.error('Submit application error:', error);
res.status(500).json({ success: false, message: 'Error submitting application' });
}
};
export const getApplications = async (req: Request, res: Response) => {
try {
// Add filtering logic here similar to Opportunity
const applications = await Application.findAll({
include: [{ model: Opportunity, as: 'opportunity', attributes: ['opportunityType', 'city', 'id'] }],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, data: applications });
} catch (error) {
console.error('Get applications error:', error);
res.status(500).json({ success: false, message: 'Error fetching applications' });
}
};
export const getApplicationById = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
},
include: [
{ model: ApplicationStatusHistory, as: 'statusHistory' },
{ model: ApplicationProgress, as: 'progressTracking' },
{
model: db.QuestionnaireResponse,
as: 'questionnaireResponses',
include: [
{
model: db.QuestionnaireQuestion,
as: 'question',
include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }]
}
]
}
]
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
res.json({ success: true, data: application });
} catch (error) {
console.error('Get application error:', error);
res.status(500).json({ success: false, message: 'Error fetching application' });
}
};
export const updateApplicationStatus = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const { status, stage, reason } = req.body;
const application = await Application.findByPk(id);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
const previousStatus = application.overallStatus;
await application.update({
overallStatus: status,
currentStage: stage || application.currentStage,
updatedAt: new Date()
});
// Log Status History
await ApplicationStatusHistory.create({
applicationId: application.id,
previousStatus,
newStatus: status,
changedBy: req.user?.id,
reason
});
await AuditLog.create({
userId: req.user?.id,
action: AUDIT_ACTIONS.UPDATED,
entityType: 'application',
entityId: application.id,
newData: { status, stage }
});
res.json({ success: true, message: 'Application status updated successfully' });
} catch (error) {
console.error('Update application status error:', error);
res.status(500).json({ success: false, message: 'Error updating application status' });
}
};
export const uploadDocuments = async (req: Request, res: Response) => {
// Existing logic or enhanced to use Document model
// For now, keeping simple or stubbing.
try {
// This should likely use the new Document modules/models later
res.json({ success: true, message: 'Use Document module for uploads' });
} catch (error) {
res.status(500).json({ success: false, message: 'Error' });
}
};
export const bulkShortlist = async (req: AuthRequest, res: Response) => {
try {
const { applicationIds, assignedTo, remarks } = req.body;
if (!applicationIds || !Array.isArray(applicationIds) || applicationIds.length === 0) {
return res.status(400).json({ success: false, message: 'No applications selected' });
}
// assignedTo is expected to be an array of User IDs from frontend now
// But database only supports single UUID for assignedTo.
// Strategy: Assign the first user as primary assignee.
const primaryAssigneeId = Array.isArray(assignedTo) && assignedTo.length > 0 ? assignedTo[0] : null;
// Verify primaryAssigneeId is a valid UUID if strictly enforced by DB, but Sequelize might handle null.
// Update Applications
const updateData: any = {
ddLeadShortlisted: true,
overallStatus: 'Shortlisted',
updatedAt: new Date(),
};
if (primaryAssigneeId) {
updateData.assignedTo = primaryAssigneeId;
}
await Application.update(updateData, {
where: {
id: { [Op.in]: applicationIds }
}
});
// Create Status History Entries
const historyEntries = applicationIds.map(appId => ({
applicationId: appId,
previousStatus: 'Questionnaire Completed',
newStatus: 'Shortlisted',
changedBy: req.user?.id,
reason: remarks ? `${remarks} (Assignees: ${Array.isArray(assignedTo) ? assignedTo.join(', ') : assignedTo})` : 'Bulk Shortlist'
}));
await ApplicationStatusHistory.bulkCreate(historyEntries);
// Audit Log
const auditEntries = applicationIds.map(appId => ({
userId: req.user?.id,
action: AUDIT_ACTIONS.UPDATED,
entityType: 'application',
entityId: appId,
newData: { ddLeadShortlisted: true, assignedTo: primaryAssigneeId, remarks }
}));
await AuditLog.bulkCreate(auditEntries);
res.json({
success: true,
message: `Successfully shortlisted ${applicationIds.length} application(s)`
});
} catch (error) {
console.error('Bulk shortlist error:', error);
res.status(500).json({ success: false, message: 'Error processing shortlist' });
}
};