362 lines
9.6 KiB
JavaScript
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,
|
|
};
|