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' }); } };