diff --git a/apps/web/src/routes/api/me/preferences/+server.ts b/apps/web/src/routes/api/me/preferences/+server.ts index 1cff687..1121764 100644 --- a/apps/web/src/routes/api/me/preferences/+server.ts +++ b/apps/web/src/routes/api/me/preferences/+server.ts @@ -28,3 +28,100 @@ export const GET: RequestHandler = async (event) => { headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } }); }; + +export const POST: RequestHandler = async (event) => { + // Requiere sesión + const userId = event.locals.userId ?? null; + if (!userId) { + return new Response('Unauthorized', { status: 401 }); + } + + let payload: any = null; + try { + payload = await event.request.json(); + } catch { + return new Response('Bad Request', { status: 400 }); + } + + const freqRaw = String(payload?.freq || '').trim().toLowerCase(); + const timeRaw = payload?.time == null ? null : String(payload.time).trim(); + + const allowed = new Set(['off', 'daily', 'weekly', 'weekdays']); + if (!allowed.has(freqRaw)) { + return new Response(JSON.stringify({ error: 'freq inválida' }), { + status: 400, + headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } + }); + } + + function normalizeTime(input: string): string | null { + const m = /^\s*(\d{1,2}):(\d{1,2})\s*$/.exec(input || ''); + if (!m) return null; + const h = Number(m[1]); + const min = Number(m[2]); + if (!Number.isFinite(h) || !Number.isFinite(min)) return null; + if (h < 0 || h > 23 || min < 0 || min > 59) return null; + const hh = String(h).padStart(2, '0'); + const mm = String(min).padStart(2, '0'); + return `${hh}:${mm}`; + } + + const db = await getDb(); + + let timeToSave: string | null = null; + + if (freqRaw === 'off') { + // Hora opcional: si viene, validar/normalizar; si no, conservar la actual o usar '08:30' + if (timeRaw && timeRaw.length > 0) { + const norm = normalizeTime(timeRaw); + if (!norm) { + return new Response(JSON.stringify({ error: 'hora inválida' }), { + status: 400, + headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } + }); + } + timeToSave = norm; + } else { + const row = db + .prepare( + `SELECT reminder_time AS time + FROM user_preferences + WHERE user_id = ? + LIMIT 1` + ) + .get(userId) as any; + timeToSave = row?.time ? String(row.time) : '08:30'; + } + } else { + // daily/weekly/weekdays: si no se especifica hora, usar '08:30' + if (!timeRaw || timeRaw.length === 0) { + timeToSave = '08:30'; + } else { + const norm = normalizeTime(timeRaw); + if (!norm) { + return new Response(JSON.stringify({ error: 'hora inválida' }), { + status: 400, + headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } + }); + } + timeToSave = norm; + } + } + + // Upsert preferencia (mantener last_reminded_on intacto) + db.prepare( + `INSERT INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at) + VALUES (?, ?, ?, (SELECT last_reminded_on FROM user_preferences WHERE user_id = ?), strftime('%Y-%m-%d %H:%M:%f','now')) + ON CONFLICT(user_id) DO UPDATE SET + reminder_freq = excluded.reminder_freq, + reminder_time = excluded.reminder_time, + updated_at = excluded.updated_at` + ).run(userId, freqRaw, timeToSave, userId); + + const responseBody = { freq: freqRaw, time: timeToSave }; + + return new Response(JSON.stringify(responseBody), { + status: 200, + headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } + }); +}; diff --git a/tests/web/api.me.preferences.test.ts b/tests/web/api.me.preferences.test.ts index da1fe42..29356d7 100644 --- a/tests/web/api.me.preferences.test.ts +++ b/tests/web/api.me.preferences.test.ts @@ -74,4 +74,93 @@ describe('Web API - GET /api/me/preferences', () => { const json = await res.json(); expect(json).toEqual({ freq: 'off', time: '08:30' }); }); + + it('POST /api/me/preferences - flujo completo', async () => { + const sid = 'sid-test-pref'; + const base = server!.baseUrl; + + // 1) daily sin hora -> usa default '08:30' + let res = await fetch(`${base}/api/me/preferences`, { + method: 'POST', + headers: { 'content-type': 'application/json', Cookie: `sid=${sid}` }, + body: JSON.stringify({ freq: 'daily' }) + }); + expect(res.status).toBe(200); + let json = await res.json(); + expect(json).toEqual({ freq: 'daily', time: '08:30' }); + + // GET refleja lo guardado + res = await fetch(`${base}/api/me/preferences`, { headers: { Cookie: `sid=${sid}` } }); + expect(res.status).toBe(200); + json = await res.json(); + expect(json).toEqual({ freq: 'daily', time: '08:30' }); + + // 2) weekly con hora '7:5' → normaliza a '07:05' + res = await fetch(`${base}/api/me/preferences`, { + method: 'POST', + headers: { 'content-type': 'application/json', Cookie: `sid=${sid}` }, + body: JSON.stringify({ freq: 'weekly', time: '7:5' }) + }); + expect(res.status).toBe(200); + json = await res.json(); + expect(json).toEqual({ freq: 'weekly', time: '07:05' }); + + // 3) weekdays con hora inválida → 400 + res = await fetch(`${base}/api/me/preferences`, { + method: 'POST', + headers: { 'content-type': 'application/json', Cookie: `sid=${sid}` }, + body: JSON.stringify({ freq: 'weekdays', time: '25:00' }) + }); + expect(res.status).toBe(400); + + // 4) freq inválida → 400 + res = await fetch(`${base}/api/me/preferences`, { + method: 'POST', + headers: { 'content-type': 'application/json', Cookie: `sid=${sid}` }, + body: JSON.stringify({ freq: 'foo', time: '08:00' }) + }); + expect(res.status).toBe(400); + + // 5) off sin hora → conserva última ('07:05') + res = await fetch(`${base}/api/me/preferences`, { + method: 'POST', + headers: { 'content-type': 'application/json', Cookie: `sid=${sid}` }, + body: JSON.stringify({ freq: 'off' }) + }); + expect(res.status).toBe(200); + json = await res.json(); + expect(json).toEqual({ freq: 'off', time: '07:05' }); + + // GET refleja off con hora conservada + res = await fetch(`${base}/api/me/preferences`, { headers: { Cookie: `sid=${sid}` } }); + expect(res.status).toBe(200); + json = await res.json(); + expect(json).toEqual({ freq: 'off', time: '07:05' }); + + // 6) weekdays con hora válida + res = await fetch(`${base}/api/me/preferences`, { + method: 'POST', + headers: { 'content-type': 'application/json', Cookie: `sid=${sid}` }, + body: JSON.stringify({ freq: 'weekdays', time: '18:45' }) + }); + expect(res.status).toBe(200); + json = await res.json(); + expect(json).toEqual({ freq: 'weekdays', time: '18:45' }); + + // 7) daily con hora '6:7' -> normaliza '06:07' + res = await fetch(`${base}/api/me/preferences`, { + method: 'POST', + headers: { 'content-type': 'application/json', Cookie: `sid=${sid}` }, + body: JSON.stringify({ freq: 'daily', time: '6:7' }) + }); + expect(res.status).toBe(200); + json = await res.json(); + expect(json).toEqual({ freq: 'daily', time: '06:07' }); + + // GET final + res = await fetch(`${base}/api/me/preferences`, { headers: { Cookie: `sid=${sid}` } }); + expect(res.status).toBe(200); + json = await res.json(); + expect(json).toEqual({ freq: 'daily', time: '06:07' }); + }); });