Data Model — 003 Recordatorios de cita
Fuente: spec.md (Key Entities + FR-003/FR-008/FR-010/FR-011/FR-016) + Research D2/D8/D10. Dinero en céntimos INTEGER, tiempo en timestamptz, zona Europe/Madrid, idioma es-ES. Propietario de Cita/Paciente/Clinica: 001 (esta spec solo añade una tabla y una columna de enmienda).
EnvioRecordatorio (nueva, propietario 003)
Un row por envío. La re-ejecución lo detecta por UNIQUE(citaId, huella); el histórico se conserva (nunca DELETE/UPDATE del row salvo vigente derivado por lectura).
Constraints: UNIQUE(citaId, huella) (FR-003, concurrencia); CHECK (origen IN ('auto','manual')). El «vigente» no se persiste: es el row de mayor creadoEn por citaId; la señal FR-011 lo usa para la fecha («enviado el …»).
Huella (configuración recordada, Q6)
huella = sha256(
pacienteId + '|' + normalizarCorreo(email) + '|' +
profesionalId + '|' + servicioId + '|' +
inicio.toISOString() + '|' + fin.toISOString()
)
Cualquier cambio de paciente/email/profesional/servicio/tramo → huella distinta → reenvío si el nuevo tramo cae en ventana (FR-008). Volver a un tramo ya enviado → huella ya existente → cero duplicados (edge spec).
Cita.precioCongeladoCentimos (enmienda menor 001 S-05)
Migración: ADD COLUMN … NOT NULL DEFAULT 0 + backfill UPDATE "Cita" SET "precioCongeladoCentimos" = "Servicio"."precioCentimos" + POST /api/citas lo escribe en la misma transacción. Semilla: prisma/seed.ts lo incluye al crear citas.
Ficheros (no BD)
datos/salida-correo/recordatorio-<citaId>-AAAAMMDD-HHMM.eml— 1:1 conEnvioRecordatorio(SC-008). Jamás sobrescrito ni borrado.datos/salida-correo/INCIDENCIAS-AAAA-MM-DD.txt— append-only:ISO-ejecucion | citaId | motivo(sin-email | email-invalido | duplicado). Sin reintento (FR-016).
Diagrama (texto)
Cita 1──N EnvioRecordatorio (citaId, huella UNIQUE)
Cita N──1 Paciente / Profesional / Servicio (configuración recordada)
EnvioRecordatorio ──1:1── .eml en datos/salida-correo/
Cita en ventana sin email válido ──0:1── línea en INCIDENCIAS-*.txt (sin row, sin .eml)