enhanced the questionnaraire ui and added upto interview lvel 3 implemented

This commit is contained in:
laxmanhalaki 2026-02-18 20:03:42 +05:30
parent 74202de59b
commit 31109d6109
15 changed files with 615 additions and 26 deletions

View File

@ -0,0 +1,28 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
const updateEnum = async () => {
try {
console.log('>>> STARTING ENUM MIGRATION (Level 3) <<<');
await db.sequelize.authenticate();
console.log('Database connection established.');
try {
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 3 Approved';`);
console.log('Added Level 3 Approved');
} catch (e) {
console.log('Level 3 Approved likely exists or error', e instanceof Error ? e.message : String(e));
}
console.log('>>> SUCCESS: Enum values updated <<<');
await db.sequelize.close();
process.exit(0);
} catch (error) {
console.error('>>> ERROR: Failed to update Enum', error);
process.exit(1);
}
};
updateEnum();

42
scripts/update-enum.ts Normal file
View File

@ -0,0 +1,42 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
const updateEnum = async () => {
try {
console.log('>>> STARTING ENUM MIGRATION <<<');
await db.sequelize.authenticate();
console.log('Database connection established.');
// Raw query to add values to enum
// Note: PostgreSQL cannot remove enum values, only add.
// We will add the "Interview Pending" variations.
const queryInterface = db.sequelize.getQueryInterface();
try {
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 1 Interview Pending';`);
console.log('Added Level 1 Interview Pending');
} catch (e) { console.log('Level 1 Interview Pending likely exists or error', e.message); }
try {
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 2 Interview Pending';`);
console.log('Added Level 2 Interview Pending');
} catch (e) { console.log('Level 2 Interview Pending likely exists or error', e.message); }
try {
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 3 Interview Pending';`);
console.log('Added Level 3 Interview Pending');
} catch (e) { console.log('Level 3 Interview Pending likely exists or error', e.message); }
console.log('>>> SUCCESS: Enum values updated <<<');
await db.sequelize.close();
process.exit(0);
} catch (error) {
console.error('>>> ERROR: Failed to update Enum', error);
process.exit(1);
}
};
updateEnum();

View File

@ -49,14 +49,13 @@ export const APPLICATION_STATUS = {
IN_REVIEW: 'In Review',
APPROVED: 'Approved',
REJECTED: 'Rejected',
SUBMITTED: 'Submitted',
QUESTIONNAIRE_PENDING: 'Questionnaire Pending',
LEVEL_1_PENDING: 'Level 1 Pending',
LEVEL_1_PENDING: 'Level 1 Interview Pending',
LEVEL_1_APPROVED: 'Level 1 Approved',
LEVEL_2_PENDING: 'Level 2 Pending',
LEVEL_2_PENDING: 'Level 2 Interview Pending',
LEVEL_2_APPROVED: 'Level 2 Approved',
LEVEL_2_RECOMMENDED: 'Level 2 Recommended',
LEVEL_3_PENDING: 'Level 3 Pending',
LEVEL_3_PENDING: 'Level 3 Interview Pending',
LEVEL_3_APPROVED: 'Level 3 Approved',
FDD_VERIFICATION: 'FDD Verification',
PAYMENT_PENDING: 'Payment Pending',
LOI_ISSUED: 'LOI Issued',

View File

@ -19,6 +19,44 @@ export const authenticate = async (req: AuthRequest, res: Response, next: NextFu
const token = authHeader.replace('Bearer ', '');
// Mock Prospective User handling
// Mock Prospective User handling
if (token.startsWith('mock-prospective-token-')) {
const appId = token.replace('mock-prospective-token-', '');
// Validate UUID format to prevent DB errors with legacy tokens
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(appId)) {
return res.status(401).json({
success: false,
message: 'Invalid session token. Please login again.'
});
}
// Find application to get real details
const application = await db.Application.findByPk(appId);
if (application) {
req.user = {
id: application.id,
email: application.email,
firstName: application.applicantName ? application.applicantName.split(' ')[0] : 'Prospective',
lastName: application.applicantName ? application.applicantName.split(' ').slice(1).join(' ') : 'User',
fullName: application.applicantName,
roleCode: 'Prospective Dealer',
status: 'active'
} as any;
req.token = token;
return next();
}
// If app not found, fall through or error?
// Let's error to be safe as the token was specific
return res.status(401).json({
success: false,
message: 'Invalid prospective user session.'
});
}
// Verify token
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };

View File

@ -0,0 +1,63 @@
import { Request, Response } from 'express';
export class ProspectiveLoginController {
static async sendOtp(req: Request, res: Response) {
try {
const { phone } = req.body;
if (!phone) {
return res.status(400).json({ message: 'Phone number is required' });
}
// Mock logic: In a real app, we would generate a random OTP and send it via SMS
// For now, we just return success
console.log(`[Mock] OTP request for ${phone}`);
return res.status(200).json({
message: 'OTP sent successfully',
data: {
phone,
// In development surfacing the OTP for easier testing
mockOtp: '123456'
}
});
} catch (error) {
console.error('Send OTP error:', error);
return res.status(500).json({ message: 'Internal server error' });
}
}
static async verifyOtp(req: Request, res: Response) {
try {
const { phone, otp } = req.body;
if (!phone || !otp) {
return res.status(400).json({ message: 'Phone and OTP are required' });
}
// Mock logic: Verify OTP
if (otp === '123456') {
// Mock success response with a fake token
return res.status(200).json({
message: 'OTP verified successfully',
data: {
token: 'mock-prospective-token-' + Date.now(),
user: {
id: 'prospective-user-id',
phone: phone,
role: 'Prospective Dealer'
}
}
});
} else {
return res.status(400).json({ message: 'Invalid OTP' });
}
} catch (error) {
console.error('Verify OTP error:', error);
return res.status(500).json({ message: 'Internal server error' });
}
}
}

View File

@ -8,6 +8,7 @@ export interface InterviewAttributes {
interviewType: string;
linkOrLocation: string | null;
status: string;
scheduledBy: string | null;
}
export interface InterviewInstance extends Model<InterviewAttributes>, InterviewAttributes { }
@ -46,6 +47,14 @@ export default (sequelize: Sequelize) => {
status: {
type: DataTypes.STRING,
defaultValue: 'scheduled'
},
scheduledBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'interviews',
@ -54,6 +63,7 @@ export default (sequelize: Sequelize) => {
(Interview as any).associate = (models: any) => {
Interview.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
Interview.belongsTo(models.User, { foreignKey: 'scheduledBy', as: 'scheduler' });
Interview.hasMany(models.InterviewParticipant, { foreignKey: 'interviewId', as: 'participants' });
Interview.hasMany(models.InterviewEvaluation, { foreignKey: 'interviewId', as: 'evaluations' });
};

View File

@ -10,12 +10,12 @@ import { ROLES } from '../../common/config/constants.js';
router.use(authenticate as any);
// Roles
router.get('/roles', adminController.getRoles);
router.get('/roles', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.getRoles);
router.post('/roles', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.createRole);
router.put('/roles/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateRole);
// Permissions
router.get('/permissions', adminController.getPermissions);
router.get('/permissions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.getPermissions);
// Users (Admin View)
router.post('/users', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.createUser);

View File

@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import db from '../../database/models/index.js';
const {
Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, QuestionnaireScore,
Interview, InterviewEvaluation, InterviewParticipant, AiSummary, User
Interview, InterviewEvaluation, InterviewParticipant, AiSummary, User, RequestParticipant, Role
} = db;
import { AuthRequest } from '../../types/express.types.js';
import { Op } from 'sequelize';
@ -68,25 +68,60 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons
export const scheduleInterview = async (req: AuthRequest, res: Response) => {
try {
console.log('---------------------------------------------------');
console.log('Incoming Schedule Interview Request:', JSON.stringify(req.body, null, 2));
const { applicationId, level, scheduledAt, type, location, participants } = req.body; // participants: [userId]
// Parse level string (e.g., "level1") to integer if necessary
const levelNum = typeof level === 'string' ? parseInt(level.replace(/\D/g, ''), 10) : level;
console.log(`Parsed Level: ${level} -> ${levelNum}`);
console.log('Creating Interview record...');
const interview = await Interview.create({
applicationId,
level,
scheduledAt,
type,
location,
level: levelNum || 1, // Default to 1 if parsing fails
scheduleDate: new Date(scheduledAt),
interviewType: type,
linkOrLocation: location,
status: 'Scheduled',
scheduledBy: req.user?.id
});
console.log('Interview created with ID:', interview.id);
// Update Application Status
const statusMap: any = {
1: 'Level 1 Interview Pending',
2: 'Level 2 Interview Pending',
3: 'Level 3 Interview Pending'
};
const newStatus = statusMap[levelNum] || 'Interview Scheduled';
await db.Application.update({ overallStatus: newStatus }, { where: { id: applicationId } });
if (participants && participants.length > 0) {
console.log(`Processing ${participants.length} participants...`);
for (const userId of participants) {
// 1. Add to Panel
await InterviewParticipant.create({
interviewId: interview.id,
userId,
role: 'Panelist'
});
// 2. Add as Request Participant for Collaboration
console.log(`Adding user ${userId} to RequestParticipant...`);
await RequestParticipant.findOrCreate({
where: {
requestId: applicationId,
requestType: 'application',
userId
},
defaults: {
participantType: 'contributor', // 'interviewer' is not a valid enum value
joinedMethod: 'interview'
}
});
}
}
@ -117,10 +152,13 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
}
}
console.log('Interview scheduling completed successfully.');
res.status(201).json({ success: true, message: 'Interview scheduled successfully', data: interview });
} catch (error) {
console.error('Schedule interview error:', error);
res.status(500).json({ success: false, message: 'Error scheduling interview' });
console.error('CRITICAL ERROR in scheduleInterview:', error);
// Log the full error object for inspection
console.log(JSON.stringify(error, null, 2));
res.status(500).json({ success: false, message: 'Error scheduling interview', error: String(error) });
}
};
@ -152,8 +190,8 @@ export const submitEvaluation = async (req: AuthRequest, res: Response) => {
const evaluation = await InterviewEvaluation.create({
interviewId: id,
evaluatorId: req.user?.id,
ktScore,
feedback,
ktMatrixScore: ktScore,
qualitativeFeedback: feedback,
recommendation
});
@ -169,6 +207,113 @@ export const submitEvaluation = async (req: AuthRequest, res: Response) => {
}
};
export const submitKTMatrix = async (req: AuthRequest, res: Response) => {
try {
const { interviewId, criteriaScores, feedback, recommendation } = req.body;
// criteriaScores: [{ criterionName, score, maxScore, weightage }]
const interview = await Interview.findByPk(interviewId);
if (!interview) return res.status(404).json({ success: false, message: 'Interview not found' });
// Calculate total weighted score
let totalWeightedScore = 0;
const totalWeightage = criteriaScores.reduce((sum: number, item: any) => sum + item.weightage, 0);
const ktMatrixScoresData = criteriaScores.map((item: any) => {
const weightedScore = (item.score / item.maxScore) * item.weightage;
totalWeightedScore += weightedScore;
return {
criterionName: item.criterionName,
score: item.score,
maxScore: item.maxScore,
weightage: item.weightage,
weightedScore
};
});
// Check if evaluation exists for this user and interview, if so update, else create
let evaluation = await InterviewEvaluation.findOne({
where: { interviewId, evaluatorId: req.user?.id }
});
if (evaluation) {
await evaluation.update({
ktMatrixScore: totalWeightedScore,
qualitativeFeedback: feedback,
recommendation
});
// Remove old details to replace with new
await db.KTMatrixScore.destroy({ where: { evaluationId: evaluation.id } });
} else {
evaluation = await InterviewEvaluation.create({
interviewId,
evaluatorId: req.user?.id,
ktMatrixScore: totalWeightedScore,
qualitativeFeedback: feedback,
recommendation
});
}
// Bulk create detailed scores
const scoreRecords = ktMatrixScoresData.map((s: any) => ({
...s,
evaluationId: evaluation?.id
}));
await db.KTMatrixScore.bulkCreate(scoreRecords);
res.status(201).json({ success: true, message: 'KT Matrix submitted successfully', data: evaluation });
} catch (error) {
console.error('Submit KT Matrix error:', error);
res.status(500).json({ success: false, message: 'Error submitting KT Matrix' });
}
};
export const submitLevel2Feedback = async (req: AuthRequest, res: Response) => {
try {
const { interviewId, feedbackItems, recommendation, overallScore } = req.body;
// feedbackItems: [{ type: 'Strategic Vision', comments: '...' }]
const interview = await Interview.findByPk(interviewId);
if (!interview) return res.status(404).json({ success: false, message: 'Interview not found' });
// Check if evaluation exists for this user and interview, if so update, else create
let evaluation = await InterviewEvaluation.findOne({
where: { interviewId, evaluatorId: req.user?.id }
});
if (evaluation) {
await evaluation.update({
ktMatrixScore: overallScore, // Reusing this field for overall score (check if type matches)
recommendation
});
// Remove old details to replace with new
await db.InterviewFeedback.destroy({ where: { evaluationId: evaluation.id } });
} else {
evaluation = await InterviewEvaluation.create({
interviewId,
evaluatorId: req.user?.id,
ktMatrixScore: overallScore,
recommendation
});
}
// Bulk create detailed qualitative feedback
if (feedbackItems && feedbackItems.length > 0) {
const feedbackRecords = feedbackItems.map((item: any) => ({
evaluationId: evaluation?.id,
feedbackType: item.type,
comments: item.comments
}));
await db.InterviewFeedback.bulkCreate(feedbackRecords);
}
res.status(201).json({ success: true, message: 'Level 2 Feedback submitted successfully', data: evaluation });
} catch (error) {
console.error('Submit Level 2 Feedback error:', error);
res.status(500).json({ success: false, message: 'Error submitting Level 2 Feedback' });
}
};
// --- AI Summary ---
export const getAiSummary = async (req: Request, res: Response) => {
@ -186,3 +331,43 @@ export const getAiSummary = async (req: Request, res: Response) => {
res.status(500).json({ success: false, message: 'Error fetching AI summary' });
}
};
export const getInterviews = async (req: Request, res: Response) => {
try {
const { applicationId } = req.params;
const interviews = await Interview.findAll({
where: { applicationId },
include: [
{
model: InterviewParticipant,
as: 'participants',
include: [{ model: User, as: 'user' }] // Assuming association exists
},
{
model: InterviewEvaluation,
as: 'evaluations',
include: [{
model: User,
as: 'evaluator',
attributes: ['id', 'fullName', 'email'],
include: [{ model: Role, as: 'role', attributes: ['roleName', 'roleCode'] }]
},
{
model: db.InterviewFeedback,
as: 'feedbackDetails'
}]
},
{
model: User,
as: 'scheduler',
attributes: ['id', 'fullName', 'email', 'designation']
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, data: interviews });
} catch (error) {
console.error('Get interviews error:', error);
res.status(500).json({ success: false, message: 'Error fetching interviews' });
}
};

View File

@ -13,6 +13,9 @@ router.post('/questionnaire/response', assessmentController.submitQuestionnaireR
router.post('/interviews', assessmentController.scheduleInterview);
router.put('/interviews/:id', assessmentController.updateInterview);
router.post('/interviews/:id/evaluation', assessmentController.submitEvaluation);
router.get('/interviews/:applicationId', assessmentController.getInterviews);
router.post('/kt-matrix', assessmentController.submitKTMatrix);
router.post('/level2-feedback', assessmentController.submitLevel2Feedback);
// AI Summary
router.get('/ai-summary/:applicationId', assessmentController.getAiSummary);

View File

@ -147,6 +147,21 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
return res.status(401).json({ success: false, message: 'Unauthorized' });
}
// Mock Prospective User handling
if (req.user.roleCode === 'Prospective Dealer') {
return res.json({
success: true,
user: {
id: req.user.id,
email: req.user.email,
fullName: req.user.fullName,
role: 'Prospective Dealer',
phone: (req.user as any).mobileNumber || (req.user as any).phone,
createdAt: (req.user as any).createdAt || new Date().toISOString()
}
});
}
const user = await User.findByPk(req.user.id, {
attributes: ['id', 'email', 'fullName', 'roleCode', 'regionId', 'zoneId', 'mobileNumber', 'createdAt']
});

View File

@ -143,10 +143,17 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
}
};
export const getApplications = async (req: Request, res: Response) => {
export const getApplications = async (req: AuthRequest, res: Response) => {
try {
// Add filtering logic here similar to Opportunity
const whereClause: any = {};
// Security Check: If prospective dealer, only show their own application
if (req.user?.roleCode === 'Prospective Dealer') {
whereClause.email = req.user.email;
}
const applications = await Application.findAll({
where: whereClause,
include: [{ model: Opportunity, as: 'opportunity', attributes: ['opportunityType', 'city', 'id'] }],
order: [['createdAt', 'DESC']]
});
@ -158,7 +165,7 @@ export const getApplications = async (req: Request, res: Response) => {
}
};
export const getApplicationById = async (req: Request, res: Response) => {
export const getApplicationById = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
@ -195,6 +202,11 @@ export const getApplicationById = async (req: Request, res: Response) => {
return res.status(404).json({ success: false, message: 'Application not found' });
}
// 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' });
}
res.json({ success: true, data: application });
} catch (error) {
console.error('Get application error:', error);
@ -242,14 +254,93 @@ export const updateApplicationStatus = async (req: AuthRequest, res: Response) =
}
};
export const uploadDocuments = async (req: Request, res: Response) => {
// Existing logic or enhanced to use Document model
// For now, keeping simple or stubbing.
export const uploadDocuments = async (req: any, res: Response) => {
try {
// This should likely use the new Document modules/models later
res.json({ success: true, message: 'Use Document module for uploads' });
const { id } = req.params;
const { documentType } = req.body;
const file = req.file;
if (!file) {
return res.status(400).json({ success: false, message: 'No file uploaded' });
}
if (!documentType) {
return res.status(400).json({ success: false, message: 'Document type is required' });
}
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
// Create Document Record
const newDoc = await db.Document.create({
applicationId: application.id,
requestId: application.id,
requestType: 'application',
documentType,
fileName: file.originalname,
filePath: file.path, // Store relative path or full path as needed by your storage strategy
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'
});
res.status(201).json({
success: true,
message: 'Document uploaded successfully',
data: newDoc
});
} catch (error) {
res.status(500).json({ success: false, message: 'Error' });
console.error('Upload document error:', error);
res.status(500).json({ success: false, message: 'Error uploading document' });
}
};
export const getApplicationDocuments = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
// Resolve ID to primary key if it's an appId string
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
const documents = await db.Document.findAll({
where: {
requestId: application.id,
requestType: 'application',
status: 'active'
},
include: [
{ model: db.User, as: 'uploader', attributes: ['fullName'] }
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, data: documents });
} catch (error) {
console.error('Get documents error:', error);
res.status(500).json({ success: false, message: 'Error fetching documents' });
}
};

View File

@ -3,6 +3,8 @@ const router = express.Router();
import * as onboardingController from './onboarding.controller.js';
import { authenticate } from '../../common/middleware/auth.js';
import { uploadSingle } from '../../common/middleware/upload.js';
// All routes require authentication (or public for submission? Keeping auth for now)
// Public route for application submission
router.post('/apply', onboardingController.submitApplication);
@ -15,7 +17,8 @@ router.post('/applications/shortlist', onboardingController.bulkShortlist);
router.get('/applications/:id', onboardingController.getApplicationById);
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
// router.post('/applications/:id/documents', onboardingController.uploadDocuments); // Moving to DMS module
router.post('/applications/:id/documents', uploadSingle, onboardingController.uploadDocuments);
router.get('/applications/:id/documents', onboardingController.getApplicationDocuments);
// Questionnaire Routes
router.get('/questionnaires', (req, res, next) => {

View File

@ -0,0 +1,101 @@
import { Request, Response } from 'express';
import db from '../../database/models';
import jwt from 'jsonwebtoken';
// Mock secret for now, should be in env
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret';
export class ProspectiveLoginController {
static async sendOtp(req: Request, res: Response) {
try {
const { phone } = req.body;
if (!phone) {
return res.status(400).json({ message: 'Phone number is required' });
}
console.log(`[ProspectiveLogin] Received OTP request for phone: '${phone}'`);
// Check if application exists and is shortlisted
const application = await db.Application.findOne({
where: { phone: phone }
});
console.log(`[ProspectiveLogin] DB Search Result:`, application ? `Found AppId: ${application.id}, Shortlisted: ${application.isShortlisted}, DDLeadShortlisted: ${application.ddLeadShortlisted}` : 'Not Found');
if (!application) {
console.log(`[ProspectiveLogin] Application not found for ${phone}, returning 404`);
return res.status(404).json({ message: 'No application found with this phone number' });
}
if (!application.isShortlisted && !application.ddLeadShortlisted) {
console.log(`[ProspectiveLogin] Application found but not shortlisted`);
return res.status(403).json({ message: 'Your application is under review. You can login only after shortlisting.' });
}
// Mock logic: In a real app, we would generate a random OTP and send it via SMS
console.log(`[Mock] OTP request for ${phone}`);
return res.status(200).json({
message: 'OTP sent successfully',
data: {
phone,
mockOtp: '123456'
}
});
} catch (error) {
console.error('Send OTP error:', error);
return res.status(500).json({ message: 'Internal server error' });
}
}
static async verifyOtp(req: Request, res: Response) {
try {
const { phone, otp } = req.body;
if (!phone || !otp) {
return res.status(400).json({ message: 'Phone and OTP are required' });
}
if (otp === '123456') {
// Fetch application again to get details
const application = await db.Application.findOne({
where: { phone: phone }
});
if (!application) {
return res.status(404).json({ message: 'Application not found' });
}
// Generate a real token or a mock one that Auth middleware accepts
// Using the specific mock token format for now to bypass standard Auth middleware db check
// if it's strict, or we can issue a real JWT if `strategies` allow it.
// Reverting to the mock token format we established:
const token = 'mock-prospective-token-' + application.id;
return res.status(200).json({
message: 'OTP verified successfully',
data: {
token: token,
user: {
id: application.id, // Use application ID as user ID for prospective
name: application.applicantName,
email: application.email,
phone: application.phone,
role: 'Prospective Dealer',
applicationId: application.applicationId
}
}
});
} else {
return res.status(400).json({ message: 'Invalid OTP' });
}
} catch (error) {
console.error('Verify OTP error:', error);
return res.status(500).json({ message: 'Internal server error' });
}
}
}

View File

@ -0,0 +1,9 @@
import express from 'express';
import { ProspectiveLoginController } from './prospective-login.controller.js';
const router = express.Router();
router.post('/send-otp', ProspectiveLoginController.sendOtp);
router.post('/verify-otp', ProspectiveLoginController.verifyOtp);
export default router;

View File

@ -32,6 +32,7 @@ import dealerRoutes from './modules/dealer/dealer.routes.js';
import slaRoutes from './modules/sla/sla.routes.js';
import communicationRoutes from './modules/communication/communication.routes.js';
import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js';
import prospectiveLoginRoutes from './modules/prospective-login/prospective-login.routes.js';
// Import common middleware & utils
import errorHandler from './common/middleware/errorHandler.js';
@ -106,6 +107,7 @@ app.use('/api/dealer', dealerRoutes);
app.use('/api/sla', slaRoutes);
app.use('/api/communication', communicationRoutes);
app.use('/api/questionnaire', questionnaireRoutes);
app.use('/api/prospective-login', prospectiveLoginRoutes);
// Backward Compatibility Aliases
app.use('/api/applications', onboardingRoutes);