Scheduling
Tareas recurrentes en background con @fastify/schedule (toad-scheduler) — recordatorios de citas, no-shows, limpieza de ocupación, resumen diario y canales WhatsApp/email.
El API corre tareas de fondo recurrentes con @fastify/schedule (que envuelve
toad-scheduler). Cubren los recordatorios de citas, la detección de no-shows, la limpieza
de sesiones de caja y posiciones de cola olvidadas, y el resumen diario de citas.
El plugin scheduler
Vive en apps/api/src/plugins/scheduler.ts y se registra en apps/api/src/app.ts justo
después de @fastify/schedule. Define las tareas como AsyncTask y las agenda como
SimpleIntervalJob (intervalos) o CronJob (hora fija). Todas se registran en
app.ready().then(...), es decir, una vez que el servidor está listo, y usan
preventOverrun: true para no solaparse consigo mismas.
Las dependencias se importan de forma perezosa (await import(...)) dentro del plugin para
evitar ciclos de importación con los módulos que tocan el scheduler.
Jobs registrados
| Job (id) | Cadencia | Qué hace | Implementación |
|---|---|---|---|
reminder-processor | cada 5 min | Envía los recordatorios de cita pendientes que ya vencen | processPendingReminders |
no-show-detector | cada 10 min | Marca como no_show las citas confirmadas no atendidas | detectNoShows |
occupancy-cleanup | cada 15 min | Cierra sesiones de caja inactivas + libera posiciones/tickets de cola olvidados | autoCloseStaleSessions + autoReleaseStalePositions + autoCancelAbandonedTickets |
daily-summary | cron 0 7 * * * (7:00 AM) | Notifica al staff el conteo de citas del día | sendDailySummary |
Ninguno corre al arrancar (runImmediately: false).
Recordatorios de citas
La lógica vive en apps/api/src/modules/appointments/reminder-service.ts. El flujo:
- Al confirmarse una cita,
generateReminderscrea filas enappointment_reminderspor cada combinación de canal × tiempo de antelación, más un recordatorio de confirmación inmediato. Los defaults salen deappointmentSettings.reminderDefaults:- canales:
['whatsapp', 'email'] - antelaciones:
['24h', '2h'](se parsean con un patrón\d+(h|m)) - Los recordatorios que caerían en el pasado se omiten.
- canales:
processPendingReminders(cada 5 min) toma hasta 50 recordatoriospendingcuyoscheduledFor <= nowy conretryCount < 3, y para cada uno:- Si el canal es whatsapp y el tenant tiene
whatsappConfig(accessToken + phoneNumberId) y el paciente tiene móvil, envía la plantilla (appointment_confirmationoappointment_reminder). - Si el canal es email y el tenant tiene
emailApiKey+emailFromy el paciente tiene correo, envía el email de confirmación o de recordatorio. - Marca el recordatorio como
sent(con suexternalId) o, ante cualquier fallo o config faltante, comofailedincrementandoretryCount(markReminderFailed). Tras 3 intentos el job deja de tomarlo.
- Si el canal es whatsapp y el tenant tiene
No-shows
detectNoShows (cada 10 min) recorre los tenants con noShowPolicyMinutes definido, busca las
citas confirmadas de hoy y, si ya pasó el umbral hora + noShowPolicyMinutes (default 15
min), cambia su estado a no_show y dispara notifyAppointmentNoShow (fire-and-forget) para
avisar al staff.
Resumen diario
sendDailySummary (cron 7:00 AM) cuenta las citas del día por tenant y, si hay alguna, notifica
a los roles admin, doctor y receptionist con el total y cuántas están confirmadas, vía
notifyUsersByRoles.
Canales de salida
Los recordatorios no envían directamente: delegan en dos librerías.
WhatsApp — apps/api/src/lib/whatsapp.ts
Usa la WhatsApp Cloud API (Graph API, v23.0). sendTemplate(config, options) envía una
plantilla pre-aprobada (las plantillas son la única forma de iniciar conversación fuera de
la ventana de 24 h), con bodyParams que rellenan los huecos en orden. La config
(accessToken, phoneNumberId, appSecret) sale de appointmentSettings.whatsappConfig del
tenant. El módulo también soporta verificación de firma de webhooks vía appSecret.
Email — apps/api/src/lib/email-reminders.ts
Envía con Resend (POST https://api.resend.com/emails). Expone
sendAppointmentConfirmationEmail, sendAppointmentReminderEmail y
sendAppointmentCancelledEmail, que construyen el HTML del correo. La config (apiKey,
fromEmail) sale de tenants.settings (emailApiKey / emailFrom).
Nota: Ambos canales son opcionales por tenant. Si la config no está, el recordatorio de ese canal se marca
failedcon un mensaje claro ('WhatsApp not configured'/'Email not configured') en vez de romper el job.
Notificaciones in-app
apps/api/src/modules/appointments/appointment-notifications.ts complementa lo anterior con
notificaciones dentro de la app (la campana). Cubre eventos de ciclo de vida de la cita
(solicitada, confirmada, no-show, etc.) y se entrega vía Socket.IO
(ver Tiempo real).
Siguiente: DGII / Fiscal.
Tiempo real
Socket.IO en el API, namespaces y rooms por tenant/usuario, y los eventos en vivo de cola, notificaciones, chat y agentes de impresión.
DGII / Fiscal
Cumplimiento fiscal dominicano — numeración NCF/eNCF, transmisión de e-CF a la DGII con la librería dgii-ecf, esquema de datos y ticket fiscal.