From 6277a2dbc34909086a9a57374f827653f52c6b49 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 30 Jan 2026 19:47:38 +0530 Subject: [PATCH] questinnaire modified similar to the google form ample given with mcq and weightage . opportunity and non -opportunity mapped --- src/database/models/Application.ts | 8 + src/database/models/QuestionnaireOption.ts | 50 +++ src/database/models/QuestionnaireQuestion.ts | 1 + src/database/models/index.ts | 2 + .../onboarding/onboarding.controller.ts | 75 +++- src/modules/onboarding/onboarding.routes.ts | 1 + .../onboarding/questionnaire.controller.ts | 125 +++++- src/scripts/seedQuestionnaire.ts | 387 ++++++++++++++++++ src/scripts/verifyScoring.ts | 109 +++++ verification_result.txt | 1 + 10 files changed, 737 insertions(+), 22 deletions(-) create mode 100644 src/database/models/QuestionnaireOption.ts create mode 100644 src/scripts/seedQuestionnaire.ts create mode 100644 src/scripts/verifyScoring.ts create mode 100644 verification_result.txt diff --git a/src/database/models/Application.ts b/src/database/models/Application.ts index b4899dd..eb90869 100644 --- a/src/database/models/Application.ts +++ b/src/database/models/Application.ts @@ -35,6 +35,7 @@ export interface ApplicationAttributes { zoneId: string | null; regionId: string | null; areaId: string | null; + score: number; documents: any[]; timeline: any[]; } @@ -162,6 +163,11 @@ export default (sequelize: Sequelize) => { type: DataTypes.BOOLEAN, defaultValue: false }, + score: { + type: DataTypes.DECIMAL(10, 2), + allowNull: true, + defaultValue: 0 + }, assignedTo: { type: DataTypes.UUID, allowNull: true, @@ -238,6 +244,8 @@ export default (sequelize: Sequelize) => { as: 'uploadedDocuments', scope: { requestType: 'application' } }); + + Application.hasMany(models.QuestionnaireResponse, { foreignKey: 'applicationId', as: 'questionnaireResponses' }); }; return Application; diff --git a/src/database/models/QuestionnaireOption.ts b/src/database/models/QuestionnaireOption.ts new file mode 100644 index 0000000..933823b --- /dev/null +++ b/src/database/models/QuestionnaireOption.ts @@ -0,0 +1,50 @@ +import { Model, DataTypes, Sequelize } from 'sequelize'; + +export interface QuestionnaireOptionAttributes { + id: string; + questionId: string; + optionText: string; + score: number; + order: number; +} + +export interface QuestionnaireOptionInstance extends Model, QuestionnaireOptionAttributes { } + +export default (sequelize: Sequelize) => { + const QuestionnaireOption = sequelize.define('QuestionnaireOption', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + questionId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'questionnaire_questions', + key: 'id' + } + }, + optionText: { + type: DataTypes.STRING, + allowNull: false + }, + score: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + order: { + type: DataTypes.INTEGER, + defaultValue: 0 + } + }, { + tableName: 'questionnaire_options', + timestamps: true + }); + + (QuestionnaireOption as any).associate = (models: any) => { + QuestionnaireOption.belongsTo(models.QuestionnaireQuestion, { foreignKey: 'questionId', as: 'question' }); + }; + + return QuestionnaireOption; +}; diff --git a/src/database/models/QuestionnaireQuestion.ts b/src/database/models/QuestionnaireQuestion.ts index 1d0fc8c..72c6a8a 100644 --- a/src/database/models/QuestionnaireQuestion.ts +++ b/src/database/models/QuestionnaireQuestion.ts @@ -68,6 +68,7 @@ export default (sequelize: Sequelize) => { (QuestionnaireQuestion as any).associate = (models: any) => { QuestionnaireQuestion.belongsTo(models.Questionnaire, { foreignKey: 'questionnaireId', as: 'questionnaire' }); QuestionnaireQuestion.hasMany(models.QuestionnaireResponse, { foreignKey: 'questionId', as: 'responses' }); + QuestionnaireQuestion.hasMany(models.QuestionnaireOption, { foreignKey: 'questionId', as: 'questionOptions' }); }; return QuestionnaireQuestion; diff --git a/src/database/models/index.ts b/src/database/models/index.ts index 456c27e..a6d0616 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -43,6 +43,7 @@ import createApplicationProgress from './ApplicationProgress.js'; // Batch 3: Questionnaire & Interview Systems import createQuestionnaire from './Questionnaire.js'; import createQuestionnaireQuestion from './QuestionnaireQuestion.js'; +import createQuestionnaireOption from './QuestionnaireOption.js'; import createQuestionnaireResponse from './QuestionnaireResponse.js'; import createQuestionnaireScore from './QuestionnaireScore.js'; import createInterview from './Interview.js'; @@ -147,6 +148,7 @@ db.ApplicationProgress = createApplicationProgress(sequelize); // Batch 3: Questionnaire & Interview Systems db.Questionnaire = createQuestionnaire(sequelize); db.QuestionnaireQuestion = createQuestionnaireQuestion(sequelize); +db.QuestionnaireOption = createQuestionnaireOption(sequelize); db.QuestionnaireResponse = createQuestionnaireResponse(sequelize); db.QuestionnaireScore = createQuestionnaireScore(sequelize); db.Interview = createInterview(sequelize); diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index c586ca3..55fe113 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -172,7 +172,18 @@ export const getApplicationById = async (req: Request, res: Response) => { }, include: [ { model: ApplicationStatusHistory, as: 'statusHistory' }, - { model: ApplicationProgress, as: 'progressTracking' } + { model: ApplicationProgress, as: 'progressTracking' }, + { + model: db.QuestionnaireResponse, + as: 'questionnaireResponses', + include: [ + { + model: db.QuestionnaireQuestion, + as: 'question', + include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }] + } + ] + } ] }); @@ -237,3 +248,65 @@ export const uploadDocuments = async (req: Request, res: Response) => { 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' }); + } +}; diff --git a/src/modules/onboarding/onboarding.routes.ts b/src/modules/onboarding/onboarding.routes.ts index a5c644b..d20ed5d 100644 --- a/src/modules/onboarding/onboarding.routes.ts +++ b/src/modules/onboarding/onboarding.routes.ts @@ -11,6 +11,7 @@ router.post('/apply', onboardingController.submitApplication); router.use(authenticate as any); router.get('/applications', onboardingController.getApplications); +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); diff --git a/src/modules/onboarding/questionnaire.controller.ts b/src/modules/onboarding/questionnaire.controller.ts index d8cbbbe..2d81a7d 100644 --- a/src/modules/onboarding/questionnaire.controller.ts +++ b/src/modules/onboarding/questionnaire.controller.ts @@ -1,8 +1,9 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; -const { Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, Application } = db; +const { Questionnaire, QuestionnaireQuestion, QuestionnaireOption, QuestionnaireResponse, Application, ApplicationStatusHistory } = db; import { v4 as uuidv4 } from 'uuid'; import { AuthRequest } from '../../types/express.types.js'; +import { APPLICATION_STATUS } from '../../common/config/constants.js'; export const getLatestQuestionnaire = async (req: Request, res: Response) => { try { @@ -11,7 +12,12 @@ export const getLatestQuestionnaire = async (req: Request, res: Response) => { include: [{ model: QuestionnaireQuestion, as: 'questions', - order: [['order', 'ASC']] + order: [['order', 'ASC']], + include: [{ + model: QuestionnaireOption, + as: 'questionOptions', + order: [['order', 'ASC']] + }] }], order: [['createdAt', 'DESC']] }); @@ -40,18 +46,28 @@ export const createQuestionnaireVersion = async (req: AuthRequest, res: Response }); if (questions && questions.length > 0) { - const questionRecords = questions.map((q: any, index: number) => ({ - questionnaireId: newQuestionnaire.id, - sectionName: q.sectionName || 'General', - questionText: q.questionText, - inputType: q.inputType || 'text', - options: q.options || null, - weight: q.weight || 0, - order: q.order || index + 1, - isMandatory: q.isMandatory !== false - })); + 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 + }); - await QuestionnaireQuestion.bulkCreate(questionRecords); + 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, { @@ -121,7 +137,12 @@ export const getQuestionnaireById = async (req: Request, res: Response) => { include: [{ model: QuestionnaireQuestion, as: 'questions', - order: [['order', 'ASC']] + order: [['order', 'ASC']], + include: [{ + model: QuestionnaireOption, + as: 'questionOptions', + order: [['order', 'ASC']] + }] }] }); @@ -146,13 +167,27 @@ export const getPublicQuestionnaire = async (req: Request, res: Response) => { 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']] + order: [['order', 'ASC']], + include: [{ + model: QuestionnaireOption, + as: 'questionOptions', + order: [['order', 'ASC']] + }] }] }); @@ -176,19 +211,67 @@ export const submitPublicResponse = async (req: Request, res: Response) => { 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' }); - const responseRecords = responses.map((r: any) => ({ - applicationId: application.id, // Use UUID from database - questionnaireId: questionnaire.id, - questionId: r.questionId, - responseValue: r.value, - attachmentUrl: r.attachmentUrl || null + // 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 + }); + res.json({ success: true, message: 'Responses submitted successfully' }); } catch (error) { console.error('Submit public response error:', error); diff --git a/src/scripts/seedQuestionnaire.ts b/src/scripts/seedQuestionnaire.ts new file mode 100644 index 0000000..f334610 --- /dev/null +++ b/src/scripts/seedQuestionnaire.ts @@ -0,0 +1,387 @@ +import db from '../database/models/index.js'; +import { v4 as uuidv4 } from 'uuid'; + +const seedQuestionnaire = async () => { + try { + console.log('Seeding Questionnaire...'); + console.log('DB Keys:', Object.keys(db)); + console.log('QuestionnaireOption defined?', !!db.QuestionnaireOption); + + // Ensure database schema is up to date + console.log('Syncing database...'); + await db.sequelize.sync({ alter: true }); + + // Deactivate existing questionnaires + await db.Questionnaire.update({ isActive: false }, { where: {} }); + + // Create new questionnaire + const questionnaire = await db.Questionnaire.create({ + id: uuidv4(), + version: 'v1.0', + isActive: true + }); + + console.log(`Created Questionnaire: ${questionnaire.id}`); + + const questions = [ + // Section 1: Basic Information (0 Score) + { + text: "Email", + type: "email", + section: "Basic Information", + options: null, + weight: 0, + order: 1 + }, + { + text: "Name", + type: "text", + section: "Basic Information", + options: null, + weight: 0, + order: 2 + }, + { + text: "Current Location", + type: "text", + section: "Basic Information", + options: null, + weight: 0, + order: 3 + }, + { + text: "Location Applied For", + type: "text", + section: "Basic Information", + options: null, + weight: 0, + order: 4 + }, + { + text: "State (Applied for)", + type: "select", + section: "Basic Information", + options: [ + { text: "Andaman & Nicobar", score: 0 }, + { text: "Andhra Pradesh", score: 0 }, + { text: "Arunachal Pradesh", score: 0 }, + { text: "Assam", score: 0 }, + { text: "Bihar", score: 0 }, + { text: "Chandigarh", score: 0 }, + { text: "Chhattisgarh", score: 0 }, + { text: "Delhi & NCR", score: 0 }, + { text: "Goa", score: 0 }, + { text: "Gujarat", score: 0 }, + { text: "Himachal Pradesh", score: 0 }, + { text: "Haryana", score: 0 }, + { text: "Jammu & Kashmir", score: 0 }, + { text: "Jharkhand", score: 0 }, + { text: "Karnataka", score: 0 }, + { text: "Kerala", score: 0 }, + { text: "Ladakh", score: 0 }, + { text: "Madhya Pradesh", score: 0 }, + { text: "Maharashtra", score: 0 }, + { text: "Mizoram", score: 0 }, + { text: "Meghalaya", score: 0 }, + { text: "Manipur", score: 0 }, + { text: "Nagaland", score: 0 }, + { text: "Odisha", score: 0 }, + { text: "Puducherry", score: 0 }, + { text: "Punjab", score: 0 }, + { text: "Rajasthan", score: 0 }, + { text: "Sikkim", score: 0 }, + { text: "Tamilnadu", score: 0 }, + { text: "Telangana", score: 0 }, + { text: "Tripura", score: 0 }, + { text: "Uttar Pradesh", score: 0 }, + { text: "Uttarakhand", score: 0 }, + { text: "West Bengal", score: 0 } + ], + weight: 0, + order: 5 + }, + { + text: "Contact Number", + type: "text", + section: "Basic Information", + options: null, + weight: 0, + order: 6 + }, + { + text: "Age", + type: "number", + section: "Basic Information", + options: null, + weight: 0, + order: 7 + }, + + // Section 2: Profile & Background (Scoring Starts) + { + text: "Educational Qualification", + type: "radio", + section: "Profile & Background", + options: [ + { text: "Under Graduate", score: 5 }, + { text: "Graduate", score: 10 }, + { text: "Post Graduate", score: 15 } + ], + weight: 15, // Max possible + order: 8 + }, + { + text: "What is your Personal Networth", + type: "select", + section: "Financials", + options: [ + { text: "Less than 2 Crores", score: 5 }, + { text: "Between 2 - 5 Crores", score: 10 }, + { text: "Between 5 - 10 Crores", score: 15 }, + { text: "Between 10 - 15 Crores", score: 20 }, + { text: "Greater than 15 Crores", score: 25 } + ], + weight: 25, + order: 9 + }, + { + text: "Are you a native of the Proposed Location?", + type: "radio", + section: "Location", + options: [ + { text: "Native", score: 10 }, + { text: "Willing to Relocate", score: 5 }, + { text: "Will manage Remotely", score: 0 } + ], + weight: 10, + order: 10 + }, + { + text: "Proposed Location Photos (If any)", + type: "file", + section: "Location", + options: null, + weight: 0, + order: 11 + }, + { + text: "Why do you want to partner with Royal Enfield?", + type: "radio", + section: "Strategy", + options: [ + { text: "Absence of Royal Enfield in the particular location and presence of opportunity", score: 5 }, + { text: "Passionate about the brand", score: 5 }, + { text: "Experience in the automobile business and would like to expand with Royal Enfield", score: 10 } + ], + weight: 10, + order: 12 + }, + { + text: "Who will be the partners in proposed company?", + type: "radio", + section: "Business Structure", + options: [ + { text: "Immediate Family", score: 5 }, + { text: "Extended Family", score: 3 }, + { text: "Friends", score: 2 }, + { text: "Proprietorship", score: 5 } + ], + weight: 5, + order: 13 + }, + { + text: "Who will be managing the Royal Enfield dealership", + type: "radio", + section: "Business Structure", + options: [ + { text: "I will be managing full time", score: 10 }, + { text: "I will be managing with my partners", score: 7 }, + { text: "I will hire a manager full time and oversee the operations", score: 5 } + ], + weight: 10, + order: 14 + }, + { + text: "Proposed Firm Type", + type: "radio", + section: "Business Structure", + options: [ + { text: "Proprietorship", score: 5 }, + { text: "Partnership", score: 5 }, + { text: "Limited Liability partnership", score: 5 }, + { text: "Private Limited Company", score: 10 } + ], + weight: 10, + order: 15 + }, + { + text: "What are you currently doing?", + type: "radio", + section: "Experience", + options: [ + { text: "Running automobile dealership", score: 10 }, + { text: "Running another business not in automobile field", score: 5 }, + { text: "Presently working, willing to resign and handle business", score: 5 }, + { text: "Currently not working and looking for business opportunities", score: 2 } + ], + weight: 10, + order: 16 + }, + { + text: "Do you own a property in proposed location?", + type: "radio", + section: "Location", + options: [ + { text: "Yes", score: 10 }, + { text: "No, will rent a location in the desired location", score: 5 } + ], + weight: 10, + order: 17 + }, + { + text: "How are you planning to invest in the Royal Enfield business", + type: "radio", + section: "Financials", + options: [ + { text: "I will be investing my own funds", score: 10 }, + { text: "I will invest partially and get the rest from the bank", score: 7 }, + { text: "I will be requiring complete funds from the bank", score: 3 } + ], + weight: 10, + order: 18 + }, + { + text: "What are your plans of expansion with RE?", + type: "radio", + section: "Strategy", + options: [ + { text: "Willing to expand with the help of partners", score: 5 }, + { text: "Willing to expand by myself", score: 10 }, + { text: "No plans for expansion", score: 0 } + ], + weight: 10, + order: 19 + }, + { + text: "Will you be expanding to any other automobile OEM in the future?", + type: "radio", + section: "Strategy", + options: [ + { text: "Yes", score: 0 }, + { text: "No", score: 5 } + ], + weight: 5, + order: 20 + }, + { + text: "Do you own a Royal Enfield ?", + type: "radio", + section: "Brand Loyalty", + options: [ + { text: "Yes, it is registered in my name", score: 5 }, + { text: "Yes, it is registered to my immediate family member", score: 3 }, + { text: "Not at the moment but owned it earlier", score: 2 }, + { text: "No", score: 0 } + ], + weight: 5, + order: 21 + }, + { + text: "Do you go for long leisure rides", + type: "radio", + section: "Brand Loyalty", + options: [ + { text: "Yes, with the Royal Enfield riders", score: 5 }, + { text: "Yes, with other brands", score: 3 }, + { text: "No", score: 0 } + ], + weight: 5, + order: 22 + }, + { + text: "What special initiatives do you plan to implement if selected as business partner for Royal Enfield ?", + type: "textarea", + section: "Strategy", + options: null, + weight: 0, + order: 23 + }, + { + text: "Please elaborate your present business/employment.", + type: "textarea", + section: "Experience", + options: null, + weight: 0, + order: 24 + } + ]; + + for (const q of questions) { + const questionId = uuidv4(); + await db.QuestionnaireQuestion.create({ + id: questionId, + questionnaireId: questionnaire.id, + sectionName: q.section, + questionText: q.text, + inputType: q.type, + options: null, // Legacy field + isMandatory: true, + weight: q.weight, + order: q.order + }); + + // If question has options, seed them into QuestionnaireOption table + if (q.options && Array.isArray(q.options) && q.options.length > 0) { + console.log(`Seeding options for question: ${q.text}`); + + if (!db.QuestionnaireOption) { + console.error('CRITICAL: db.QuestionnaireOption is undefined!'); + console.log('Available models:', Object.keys(db)); + } + + const optionRecords = q.options.map((opt, idx) => ({ + id: uuidv4(), + questionId: questionId, + optionText: opt.text, + score: opt.score, + order: idx + 1 + })); + const OptionModel = db.sequelize.models.QuestionnaireOption || db.QuestionnaireOption; + + console.log(`[DEBUG] Question: "${q.text}"`); + console.log(`[DEBUG] OptionModel type: ${typeof OptionModel}`); + console.log(`[DEBUG] OptionModel keys: ${OptionModel ? Object.keys(OptionModel).slice(0, 5) : 'null'}`); + console.log(`[DEBUG] bulkCreate type: ${OptionModel ? typeof OptionModel.bulkCreate : 'undefined'}`); + + if (!OptionModel) { + throw new Error(`QuestionnaireOption model not found in sequelize.models or db object`); + } + + try { + await OptionModel.bulkCreate(optionRecords); + console.log(`[DEBUG] Success seeding options for "${q.text}"`); + } catch (innerErr: any) { + console.error(`[DEBUG] Failed to seed options for "${q.text}"`); + console.error('Error Name:', innerErr.name); + console.error('Error Message:', innerErr.message); + if (innerErr.parent) { + console.error('Parent Error:', innerErr.parent); + } + if (innerErr.original) { + console.error('Original Error:', innerErr.original); + } + throw innerErr; + } + } + } + + console.log(`Seeded questions and options successfully.`); + process.exit(0); + + } catch (error) { + console.error('Error seeding questionnaire:', error); + process.exit(1); + } +}; + +seedQuestionnaire(); diff --git a/src/scripts/verifyScoring.ts b/src/scripts/verifyScoring.ts new file mode 100644 index 0000000..9e1b16c --- /dev/null +++ b/src/scripts/verifyScoring.ts @@ -0,0 +1,109 @@ +import db from '../database/models/index.js'; +import { v4 as uuidv4 } from 'uuid'; +import fs from 'fs'; + +const verifyScoring = async () => { + try { + console.log('Verifying Questionnaire Scoring...'); + + // Sync DB to ensure 'score' column exists + await db.sequelize.sync({ alter: true }); + + // 1. Create a dummy test application + const appId = uuidv4(); + const testApp = await db.Application.create({ + applicationId: `TEST-${Date.now()}`, + applicantName: 'Test Scorer', + email: 'test.scorer@example.com', + phone: '1234567890', + businessType: 'Dealership', + currentStage: 'DD', + overallStatus: 'Questionnaire Pending', + score: 0 + }); + + console.log(`Created Test Application: ${testApp.applicationId} (${testApp.id})`); + + // 2. Fetch Active Questionnaire Questions + const questionnaire = await db.Questionnaire.findOne({ + where: { isActive: true }, + include: [{ model: db.QuestionnaireQuestion, as: 'questions' }] + }); + + if (!questionnaire) throw new Error('No active questionnaire found'); + + // 3. Construct Responses to get a specific score + // We will target: + // - "Educational Qualification": "Post Graduate" (Score: 15) + // - "Do you own a property...": "Yes" (Score: 10) + // - All others: Non-scoring or first option (usually > 0) + + const responses = questionnaire.questions.map((q: any) => { + let value = "Test Answer"; // Default for text/file + + if (q.questionText === "Educational Qualification") { + value = "Post Graduate"; + } else if (q.questionText === "Do you own a property in proposed location?") { + value = "Yes"; + } else if (q.inputType === 'select' || q.inputType === 'radio') { + // Pick first option if not targeting specific score + const opts = typeof q.options === 'string' ? JSON.parse(q.options) : q.options; + if (opts && opts.length > 0) { + value = opts[0].text || opts[0]; + } + } + + return { + questionId: q.id, + value: value + }; + }); + + console.log('Submitting responses...'); + + // 4. Submit to API (assuming local server running on 5000) + try { + const res = await fetch('http://localhost:5000/api/questionnaire/public/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + applicationId: testApp.applicationId, + responses: responses + }) + }); + const data: any = await res.json(); + console.log('API Response:', data); + } catch (apiError: any) { + console.error('API Error:', apiError.message); + fs.writeFileSync('verification_result.txt', `FAILURE: API Error - ${apiError.message}`); + process.exit(1); + } + + // 5. Check Score in DB + const updatedApp = await db.Application.findByPk(testApp.id); + console.log(`New Score: ${updatedApp.score}`); + console.log(`New Status: ${updatedApp.overallStatus}`); + + // Write result to file + if (updatedApp.score >= 25) { + console.log('SUCCESS: Score updated!'); + fs.writeFileSync('verification_result.txt', `SUCCESS: Score ${updatedApp.score} matched expectation (>= 25)`); + } else { + console.error('FAILURE: Score mismatch.'); + fs.writeFileSync('verification_result.txt', `FAILURE: Score ${updatedApp.score} mismatch (expected >= 25)`); + process.exit(1); + } + + // Cleanup + await testApp.destroy(); + + } catch (error: any) { + console.error('Verification failed:', error); + fs.writeFileSync('verification_result.txt', `FAILURE: Script Error - ${error.message}`); + process.exit(1); + } finally { + // await db.sequelize.close(); + } +}; + +verifyScoring(); diff --git a/verification_result.txt b/verification_result.txt new file mode 100644 index 0000000..93fb42e --- /dev/null +++ b/verification_result.txt @@ -0,0 +1 @@ +FAILURE: Script Error - update or delete on table "applications" violates foreign key constraint "questionnaire_responses_applicationId_fkey" on table "questionnaire_responses" \ No newline at end of file