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, };