Tasks: Recordatorios de cita (003)
Input: specs/003-recordatorios-cita/spec.md (US1/US2/US3 + FR-001…FR-017) + plan.md + research.md + data-model.md + contracts/ + quickstart.md.
Tests: incluidos porque SC-001…SC-009 exigen medición automatizada (recuentos, diffs, bordes con reloj controlado, grep de alcance).
Organización: por historia de usuario (US1 → US2 → US3) para entrega incremental; cada historia es testeable sola.
Format: [ID] [P?] [Story] Description
Phase 1: Setup (infraestructura compartida)
- T001 Crear estructura
specs/003-recordatorios-cita/contracts/+ dirsdatos/salida-correo/.gitkeepy guionscripts/recordatorio-diario.tsvacío - T002 [P] Añadir script
recordatoriosenpackage.json(tsx scripts/recordatorio-diario.ts) y envRECORDATORIOS_HORA,APP_URLen.env.example - T003 [P] Ignorar
datos/salida-correo/*.emleINCIDENCIAS-*en.gitignore(mantener.gitkeep)
Phase 2: Foundational (bloqueante: BD + precio congelado)
⚠️ CRITICAL: ninguna historia empieza sin esta fase (enmienda 001 S-05 + tabla de envíos).
- T004 Migración 003:
EnvioRecordatorio(campos data-model.md +UNIQUE(citaId, huella)) +Cita.precioCongeladoCentimos INT NOT NULL DEFAULT 0 CHECK (>=0)+ backfill desdeServicio.precioCentimos - T005 Escribir
precioCongeladoCentimosenPOST /api/citas(misma transacción) y enprisma/seed.tsal crear citas - T006 [P]
ESQUEMA_EJECUTAR_RECORDATORIOSenlib/validacion.ts({ejecucion?}ISO opcional solo tests)
Checkpoint: npx prisma migrate deploy en verde; citas nuevas traen precio congelado; backfill verificado por SQL.
Phase 3: US1 — Recuerda sin duplicar (P1) 🎯 MVP
Goal: proceso diario 24–48 h con un envío por configuración recordada (FR-001/002/003/008/015/016).
Independent Test: semilla 001 → ejecutar → 1 envío por reservada en ventana, 0 fuera/finales; re-ejecutar N veces (auto+manual) → 0 nuevos (SC-001/SC-002).
Tests US1
- T007 [P] [US1] Unit
tests/unit/test_recordatorios.test.ts: huella cambia con paciente/email/profesional/servicio/tramo; nombre.emldeterministaAAAAMMDD-HHMM; asunto con clínica+fecha - T008 [P] [US1] Integración
tests/integration/test_recordatorios_cita.test.ts: ventana (30 h sí, 5 días no, bordes 24 h/48 h sí, 23:59 no, <24 h no, finales no, día siguiente completo a las 00:00), re-ejecución sin duplicados, cita movida reenvía + histórico, servicio/email cambiados reenvían, sin-email → incidencias sin.eml, concurrentePromise.all→ 1 envío
Implementation US1
- T009 [P] [US1]
lib/recordatorios.ts:huellaConfiguracion,nombreFicheroEml,componerCorreo,generarRecordatorios({ejecucion?, clinicaId?, origen})(selecciónreservada+enVentanaRecordatorios+ puerta 007 +createconP2002→skip+ FileTransport + incidencias append-only) - T010 [US1]
POST /api/recordatorios/ejecutar/route.ts(auth recepción, llama agenerarRecordatoriosorigenmanual) - T011 [US1]
scripts/recordatorio-diario.ts(cron 00:00 Europe/Madrid,RECORDATORIOS_HORAconfigurable, origenauto) (depende de T009)
Checkpoint: US1 funciona y se prueba sola aunque el email aún no tenga página de cancelar.
Phase 4: US2 — Qué, dónde y cómo cancelar (P1)
Goal: email es-ES completo + página de confirmación que ejecuta cancelarCita (FR-004/005/006/007/009/012/017).
Independent Test: abrir .eml de la semilla (clínica, tramo, profesional, servicio, 40,00 €, teléfono, enlace, aviso no reenviar) + cancelar en plazo sin llamar y fuera de plazo con teléfono (SC-003/SC-004/SC-005).
Tests US2
- T012 [P] [US2] Integración (mismo fichero): contenido
.eml(cabeceras To/Subject/Date + cuerpo es-ES +40,00 €+ teléfono + enlace con token + aviso no reenviar + cero SMS/WhatsApp); cancelar en plazo vía token →cancelada+ tramo libre RN1; visitar sin confirmar → siguereservada; <24 h → fuera de plazo + siguereservada; bordes 24 h justas aplica / 23:59 rechaza; doble confirmación →YA_CANCELADA;completada/no_asistida→ rechazo sin mutar
Implementation US2
- T013 [US2]
app/cancelar/[id]/page.tsx(GET ver datos vigentes + POST confirmar →PATCH /api/citas/[id]/cancelar {origen:'email', token}; textos es-ES con teléfono) (depende de T009) - T014 [US2]
npm run comprobar:esen verde para los textos nuevos +grep -ri 'sms\|whatsapp' app lib→ 0 (SC-007)
Checkpoint: US1 + US2 funcionan de extremo a extremo sin llamar.
Phase 5: US3 — Simulado .eml revisable (P2)
Goal: sin SMTP → un .eml por envío, histórico intacto, incidencias en fichero (FR-010/016 → SC-006/SC-008/SC-009).
Independent Test: sin SMTP_HOST → 3 citas en ventana = 3 .eml legibles; re-ejecución → 0 nuevos; tramo movido → .eml nuevo + anterior intacto; ls | wc -l == count envíos (depende de US1/US2).
Tests US3
- T015 [P] [US3] Integración (mismo fichero): conteo
.eml== envíos tras secuencia de movimientos (SC-008); sin SMTP no hay intento de red (solo fs); cita sin email → 0.eml+ línea enINCIDENCIAS-*.txt+ estado intacto (SC-009)
Implementation US3
- T016 [US3] FileTransport final en
lib/recordatorios.ts: detección porSMTP_HOSTausente (no por fallo de red),warnsi presente (transporte real fuera de v1), escritura atómica (writeFileSynccon flagwx→ nunca sobrescribe)
Checkpoint: las tres historias funcionan de forma independiente y conjunta.
Phase 6: Polish & Cross-Cutting
- T017 [P] Señal FR-011:
GET /api/citas/[id]/recordatorio/route.ts+ texto encomponents/tarjeta-cita.tsx(«Recordatorio enviado el … a las …» / «Recordatorio pendiente», sin rutas ni hashes) - T018
quickstart.mdvalidado de extremo a extremo (seed → ejecutar →.eml→ cancelar → señal → incidencias) - T019
npm test+typecheck+lint+format:check+comprobar:esen verde;git statuslimpio de.eml(gitignored)
Dependencies & Execution Order
- Setup (T001–T003) → Foundational (T004–T006, bloquea todo) → US1 tests (T007–T008 primero, deben FALLAR) → US1 impl (T009–T011) → US2 test (T012, FALLAR) → US2 impl (T013–T014) → US3 test (T015, FALLAR) → US3 impl (T016) → Polish (T017–T019).
- Paralelo: T002+T003; T006 con T004/T005; T007+T008; T009 independiente de tests (pero sin merge hasta verlos fallar y pasar).