TGICS/backend/src/controllers/surveyController.js

362 lines
9.6 KiB
JavaScript

const { pool } = require('../config/db');
const { validateAnswers } = require('../questionnaire');
const { resolveTgicsDoctor, PARTITION } = require('../services/rapport');
const { checkRateLimit } = require('../middleware/rateLimit');
const MAX_PER_HOUR = Number(process.env.SURVEY_RATE_LIMIT_PER_HOUR || 60);
async function createSurvey(request, h) {
const payload = request.payload || {};
const userId = String(payload.user_id || '').trim();
const contactId = String(payload.contact_id || '').trim();
const answers = payload.answers;
if (!userId || !contactId) {
return h.response({
status: 'error',
message: 'user_id and contact_id are required.',
}).code(400);
}
const ip = request.info.remoteAddress || 'unknown';
const rateKey = `survey:${contactId}:${ip}`;
if (!checkRateLimit(rateKey, MAX_PER_HOUR)) {
return h.response({
status: 'error',
message: 'Too many submissions. Please try again later.',
}).code(429);
}
try {
const doctorResult = await resolveTgicsDoctor(userId, contactId);
if (!doctorResult.ok) {
return h.response({ status: 'error', message: doctorResult.message }).code(doctorResult.statusCode);
}
const validation = validateAnswers(answers);
if (!validation.ok) {
return h.response({ status: 'error', message: validation.message }).code(400);
}
const [result] = await pool.execute(
`INSERT INTO surveys (contact_id, user_id, \`partition\`, has_cough, answers_json)
VALUES (?, ?, ?, ?, ?)`,
[
contactId,
userId,
PARTITION,
validation.hasCough ? 1 : 0,
JSON.stringify(answers),
]
);
try {
await pool.execute(
'DELETE FROM survey_drafts WHERE contact_id = ? AND user_id = ?',
[contactId, userId]
);
} catch (draftErr) {
console.warn('createSurvey draft cleanup failed', draftErr.message);
}
return h.response({
status: 'success',
data: { id: result.insertId },
message: 'Survey submitted successfully.',
}).code(201);
} catch (err) {
const status = err.statusCode || 500;
console.error('createSurvey error', err);
return h.response({
status: 'error',
message: err.message || 'Unable to save survey.',
}).code(status);
}
}
async function getCounts(request, h) {
const raw = String(request.query.contact_ids || '').trim();
if (!raw) {
return h.response({
status: 'success',
data: { counts: {}, total: 0 },
}).code(200);
}
const ids = [...new Set(
raw.split(',')
.map((s) => s.trim())
.filter(Boolean)
)].slice(0, 500);
if (ids.length === 0) {
return h.response({
status: 'success',
data: { counts: {}, total: 0 },
}).code(200);
}
try {
const placeholders = ids.map(() => '?').join(',');
const [rows] = await pool.execute(
`SELECT contact_id, COUNT(*) AS cnt
FROM surveys
WHERE contact_id IN (${placeholders})
GROUP BY contact_id`,
ids
);
const counts = {};
for (const id of ids) {
counts[id] = 0;
}
let total = 0;
for (const row of rows) {
const n = Number(row.cnt) || 0;
counts[row.contact_id] = n;
total += n;
}
return h.response({
status: 'success',
data: { counts, total },
}).code(200);
} catch (err) {
console.error('getCounts error', err);
return h.response({
status: 'error',
message: 'Unable to load survey counts.',
}).code(500);
}
}
async function getCount(request, h) {
const contactId = String(request.query.contact_id || '').trim();
if (!contactId) {
return h.response({
status: 'error',
message: 'contact_id is required.',
}).code(400);
}
try {
const [rows] = await pool.execute(
'SELECT COUNT(*) AS cnt FROM surveys WHERE contact_id = ?',
[contactId]
);
const count = Number(rows[0]?.cnt || 0);
return h.response({
status: 'success',
data: { contact_id: contactId, count },
}).code(200);
} catch (err) {
console.error('getCount error', err);
return h.response({
status: 'error',
message: 'Unable to load survey count.',
}).code(500);
}
}
async function listSurveys(request, h) {
const contactId = String(request.query.contact_id || '').trim();
if (!contactId) {
return h.response({
status: 'error',
message: 'contact_id is required.',
}).code(400);
}
const limit = Math.min(Math.max(Number(request.query.limit) || 100, 1), 500);
const offset = Math.max(Number(request.query.offset) || 0, 0);
try {
const [rows] = await pool.execute(
`SELECT id, contact_id, user_id, has_cough, answers_json, created_at
FROM surveys
WHERE contact_id = ?
ORDER BY created_at DESC, id DESC
LIMIT ${limit} OFFSET ${offset}`,
[contactId]
);
const items = rows.map((row) => {
let answers = row.answers_json;
if (typeof answers === 'string') {
try {
answers = JSON.parse(answers);
} catch {
answers = {};
}
}
answers = answers && typeof answers === 'object' ? answers : {};
return {
id: row.id,
contact_id: row.contact_id,
user_id: row.user_id,
has_cough: Boolean(row.has_cough),
gender: answers.gender || '',
patient_age: answers.patient_age ?? '',
created_at: row.created_at,
};
});
return h.response({
status: 'success',
data: { items, contact_id: contactId },
}).code(200);
} catch (err) {
console.error('listSurveys error', err);
return h.response({
status: 'error',
message: 'Unable to load patients.',
}).code(500);
}
}
function parseDraftAnswers(raw) {
if (raw && typeof raw === 'object') return raw;
try {
const parsed = JSON.parse(raw || '{}');
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
async function getDraft(request, h) {
const contactId = String(request.query.contact_id || '').trim();
const userId = String(request.query.user_id || '').trim();
if (!contactId || !userId) {
return h.response({
status: 'error',
message: 'contact_id and user_id are required.',
}).code(400);
}
try {
const doctorResult = await resolveTgicsDoctor(userId, contactId);
if (!doctorResult.ok) {
return h.response({ status: 'error', message: doctorResult.message }).code(doctorResult.statusCode);
}
const [rows] = await pool.execute(
`SELECT answers_json, updated_at
FROM survey_drafts
WHERE contact_id = ? AND user_id = ?
LIMIT 1`,
[contactId, userId]
);
if (!rows.length) {
return h.response({
status: 'success',
data: { draft: null },
}).code(200);
}
return h.response({
status: 'success',
data: {
draft: {
answers: parseDraftAnswers(rows[0].answers_json),
updated_at: rows[0].updated_at,
},
},
}).code(200);
} catch (err) {
console.error('getDraft error', err);
return h.response({
status: 'error',
message: err.message || 'Unable to load draft.',
}).code(err.statusCode || 500);
}
}
async function saveDraft(request, h) {
const payload = request.payload || {};
const userId = String(payload.user_id || '').trim();
const contactId = String(payload.contact_id || '').trim();
const answers = payload.answers;
if (!userId || !contactId) {
return h.response({
status: 'error',
message: 'user_id and contact_id are required.',
}).code(400);
}
if (!answers || typeof answers !== 'object' || Array.isArray(answers)) {
return h.response({
status: 'error',
message: 'answers object is required.',
}).code(400);
}
try {
const doctorResult = await resolveTgicsDoctor(userId, contactId);
if (!doctorResult.ok) {
return h.response({ status: 'error', message: doctorResult.message }).code(doctorResult.statusCode);
}
await pool.execute(
`INSERT INTO survey_drafts (contact_id, user_id, \`partition\`, answers_json)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
answers_json = VALUES(answers_json),
\`partition\` = VALUES(\`partition\`),
updated_at = CURRENT_TIMESTAMP`,
[contactId, userId, PARTITION, JSON.stringify(answers)]
);
return h.response({
status: 'success',
message: 'Draft saved.',
data: { contact_id: contactId },
}).code(200);
} catch (err) {
console.error('saveDraft error', err);
return h.response({
status: 'error',
message: err.message || 'Unable to save draft.',
}).code(err.statusCode || 500);
}
}
async function deleteDraft(request, h) {
const contactId = String(request.query.contact_id || request.payload?.contact_id || '').trim();
const userId = String(request.query.user_id || request.payload?.user_id || '').trim();
if (!contactId || !userId) {
return h.response({
status: 'error',
message: 'contact_id and user_id are required.',
}).code(400);
}
try {
await pool.execute(
'DELETE FROM survey_drafts WHERE contact_id = ? AND user_id = ?',
[contactId, userId]
);
return h.response({
status: 'success',
message: 'Draft cleared.',
}).code(200);
} catch (err) {
console.error('deleteDraft error', err);
return h.response({
status: 'error',
message: 'Unable to clear draft.',
}).code(500);
}
}
module.exports = {
createSurvey,
getCounts,
getCount,
listSurveys,
getDraft,
saveDraft,
deleteDraft,
};