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

354 lines
14 KiB
TypeScript

import { Request, Response } from 'express';
import db from '../../database/models/index.js';
const { Questionnaire, QuestionnaireQuestion, QuestionnaireOption, QuestionnaireResponse, Application, ApplicationStatusHistory } = db;
import { v4 as uuidv4 } from 'uuid';
import { AuthRequest } from '../../types/express.types.js';
import {
APPLICATION_STATUS,
SYSTEM_AUDIT_MODULES,
SYSTEM_AUDIT_ACTIONS
} from '../../common/config/constants.js';
import { sendQuestionnaireAckEmail } from '../../common/utils/email.service.js';
import { logSystemAudit } from '../../services/systemAuditLog.service.js';
export const getLatestQuestionnaire = async (req: Request, res: Response) => {
try {
const questionnaire = await Questionnaire.findOne({
where: { isActive: true },
include: [{
model: QuestionnaireQuestion,
as: 'questions',
order: [['order', 'ASC']],
include: [{
model: QuestionnaireOption,
as: 'questionOptions',
order: [['order', 'ASC']]
}]
}],
order: [['createdAt', 'DESC']]
});
if (!questionnaire) {
return res.status(404).json({ success: false, message: 'No active questionnaire found' });
}
res.json({ success: true, data: questionnaire });
} catch (error) {
console.error('Get latest questionnaire error:', error);
res.status(500).json({ success: false, message: 'Error fetching questionnaire' });
}
};
export const createQuestionnaireVersion = async (req: AuthRequest, res: Response) => {
try {
const { version, questions } = req.body; // questions is array of { text, type, options, weight, section }
// Deactivate old versions
await Questionnaire.update({ isActive: false }, { where: { isActive: true } });
const newQuestionnaire = await Questionnaire.create({
version,
isActive: true
});
if (questions && questions.length > 0) {
for (const [index, q] of questions.entries()) {
const question = await QuestionnaireQuestion.create({
questionnaireId: newQuestionnaire.id,
sectionName: q.sectionName || 'General',
questionText: q.questionText,
inputType: q.inputType || 'text',
options: null, // Legacy field
weight: q.weight || 0,
order: q.order || index + 1,
isMandatory: q.isMandatory !== false
});
if (q.options && Array.isArray(q.options) && q.options.length > 0) {
const optionRecords = q.options.map((opt: any, idx: number) => ({
questionId: question.id,
optionText: opt.text,
score: opt.score || 0,
order: idx + 1
}));
await QuestionnaireOption.bulkCreate(optionRecords);
}
}
}
const fullQuestionnaire = await Questionnaire.findByPk(newQuestionnaire.id, {
include: [{ model: QuestionnaireQuestion, as: 'questions' }]
});
await logSystemAudit(req, {
module: SYSTEM_AUDIT_MODULES.QUESTIONNAIRE,
entityType: 'questionnaire',
entityId: newQuestionnaire.id,
entityLabel: `Questionnaire ${version}`,
action: SYSTEM_AUDIT_ACTIONS.CREATED,
description: `Published questionnaire version ${version} with ${questions?.length || 0} question(s); previous active version deactivated`,
newData: {
version,
isActive: true,
questionCount: questions?.length || 0
}
});
res.status(201).json({ success: true, data: fullQuestionnaire });
} catch (error) {
console.error('Create questionnaire error:', error);
res.status(500).json({ success: false, message: 'Error creating questionnaire version' });
}
};
export const submitResponse = async (req: AuthRequest, res: Response) => {
try {
const { applicationId, responses } = req.body; // responses: [{ questionId, value }]
const targetId = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
// Verify application
const application = await Application.findOne({
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
// Get active questionnaire to link
const questionnaire = await Questionnaire.findOne({ where: { isActive: true } });
if (!questionnaire) return res.status(400).json({ success: false, message: 'No active questionnaire' });
const responseRecords = responses.map((r: any) => ({
applicationId: application.id,
questionnaireId: questionnaire.id,
questionId: r.questionId,
responseValue: r.value,
attachmentUrl: r.attachmentUrl || null
}));
// Bulk create responses (maybe delete old ones for this app/questionnaire first?)
// For now, straight insert
await QuestionnaireResponse.bulkCreate(responseRecords);
// Calculate Score Logic (Placeholder for ONB-04)
// calculateAndSaveScore(applicationId, questionnaire.id);
await logSystemAudit(req, {
module: SYSTEM_AUDIT_MODULES.QUESTIONNAIRE,
entityType: 'questionnaire_response',
entityId: application.id,
entityLabel: `Application ${application.applicationId || application.id} · ${application.applicantName || 'Applicant'}`,
action: SYSTEM_AUDIT_ACTIONS.SUBMITTED,
description: `Authenticated questionnaire response submitted (${responses.length} answer(s))`,
metadata: {
questionnaireId: questionnaire.id,
applicationId: application.id,
responseCount: responses.length
}
});
res.json({ success: true, message: 'Responses submitted successfully' });
} catch (error) {
console.error('Submit response error:', error);
res.status(500).json({ success: false, message: 'Error submitting responses' });
}
};
export const getAllQuestionnaires = async (req: Request, res: Response) => {
try {
const questionnaires = await Questionnaire.findAll({
order: [['createdAt', 'DESC']],
attributes: ['id', 'version', 'isActive', 'createdAt']
});
res.json({ success: true, data: questionnaires });
} catch (error) {
console.error('Get all questionnaires error:', error);
res.status(500).json({ success: false, message: 'Error fetching questionnaires' });
}
};
export const getQuestionnaireById = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const questionnaire = await Questionnaire.findByPk(id, {
include: [{
model: QuestionnaireQuestion,
as: 'questions',
order: [['order', 'ASC']],
include: [{
model: QuestionnaireOption,
as: 'questionOptions',
order: [['order', 'ASC']]
}]
}]
});
if (!questionnaire) {
return res.status(404).json({ success: false, message: 'Questionnaire not found' });
}
res.json({ success: true, data: questionnaire });
} catch (error) {
console.error('Get questionnaire by id error:', error);
res.status(500).json({ success: false, message: 'Error fetching questionnaire' });
}
};
export const getPublicQuestionnaire = async (req: Request, res: Response) => {
try {
const { applicationId } = req.params;
// Verify valid application exists
const application = await Application.findOne({ where: { applicationId } });
if (!application) {
return res.status(404).json({ success: false, message: 'Invalid Application ID' });
}
// Check if already submitted
if (application.overallStatus !== APPLICATION_STATUS.QUESTIONNAIRE_PENDING) {
return res.status(400).json({
success: false,
message: 'Questionnaire already submitted or link expired',
code: 'ALREADY_SUBMITTED' // Frontend can use this code
});
}
// Fetch active questionnaire
const questionnaire = await Questionnaire.findOne({
where: { isActive: true },
include: [{
model: QuestionnaireQuestion,
as: 'questions',
order: [['order', 'ASC']],
include: [{
model: QuestionnaireOption,
as: 'questionOptions',
order: [['order', 'ASC']]
}]
}]
});
if (!questionnaire) {
return res.status(404).json({ success: false, message: 'No active questionnaire found' });
}
res.json({ success: true, data: { applicationName: application.applicantName, ...questionnaire.toJSON() } });
} catch (error) {
console.error('Get public questionnaire error:', error);
res.status(500).json({ success: false, message: 'Error fetching questionnaire' });
}
};
export const submitPublicResponse = async (req: Request, res: Response) => {
try {
const { applicationId, responses } = req.body;
const application = await Application.findOne({ where: { applicationId } });
if (!application) {
return res.status(404).json({ success: false, message: 'Invalid Application ID' });
}
// Double check status before processing
if (application.overallStatus !== APPLICATION_STATUS.QUESTIONNAIRE_PENDING) {
return res.status(400).json({ success: false, message: 'Questionnaire already submitted' });
}
const questionnaire = await Questionnaire.findOne({ where: { isActive: true } });
if (!questionnaire) return res.status(400).json({ success: false, message: 'No active questionnaire' });
// Calculate Score
let totalScore = 0;
const responseRecords = await Promise.all(responses.map(async (r: any) => {
const question = await QuestionnaireQuestion.findByPk(r.questionId);
let score = 0;
if (question) {
// If question has options (via new table), find score
const option = await QuestionnaireOption.findOne({
where: {
questionId: question.id,
optionText: r.value
}
});
if (option) {
score = option.score || 0;
}
}
totalScore += score;
return {
applicationId: application.id, // Use UUID from database
questionnaireId: questionnaire.id,
questionId: r.questionId,
responseValue: r.value,
attachmentUrl: r.attachmentUrl || null,
score: score // Store individual answer score if needed (requires schema update for QuestionnaireResponse)
};
}));
await QuestionnaireResponse.bulkCreate(responseRecords);
// Update Application Status & Score
const previousStatus = application.overallStatus;
const newStatus = APPLICATION_STATUS.QUESTIONNAIRE_COMPLETED;
await application.update({
overallStatus: newStatus,
score: totalScore,
updatedAt: new Date()
});
// Log Status History
await ApplicationStatusHistory.create({
applicationId: application.id,
previousStatus,
newStatus,
reason: 'Public Questionnaire Submitted',
changedBy: null // System action / Public user
});
try {
const location =
(application as any).preferredLocation ||
(application as any).city ||
'your preferred location';
await sendQuestionnaireAckEmail(
application.email,
application.applicantName || 'Applicant',
location,
application.applicationId || application.id
);
} catch (mailErr) {
console.error('[submitPublicResponse] acknowledgement email:', mailErr);
}
await logSystemAudit(req, {
actorName: application.applicantName || 'Public Applicant',
actorRole: 'Prospective Dealer',
module: SYSTEM_AUDIT_MODULES.QUESTIONNAIRE,
entityType: 'questionnaire_response',
entityId: application.id,
entityLabel: `Application ${application.applicationId || application.id} · ${application.applicantName || 'Applicant'}`,
action: SYSTEM_AUDIT_ACTIONS.SUBMITTED,
description: `Public questionnaire submitted; status ${previousStatus}${newStatus}; total score ${totalScore}`,
oldData: { overallStatus: previousStatus },
newData: { overallStatus: newStatus, score: totalScore },
metadata: {
questionnaireId: questionnaire.id,
applicationId: application.id,
responseCount: responses.length
}
});
res.json({ success: true, message: 'Responses submitted successfully' });
} catch (error) {
console.error('Submit public response error:', error);
res.status(500).json({ success: false, message: 'Error submitting responses' });
}
};