Appointments — Scheduling and Calendar
Audiences: doctor, clinical-buyer, developer
Dudoxx HMS manages the full appointment lifecycle — booking, conflict detection, check-in, no-show tracking, recurring schedules, and FHIR synchronization — through a layered NestJS service stack that is the entry point for every clinical encounter.
Business Purpose
Scheduling is the operational heartbeat of a clinic: every revenue event, every FHIR record, and every clinical visit originates with an appointment. Dudoxx HMS provides: (1) real-time conflict detection so double-bookings cannot happen; (2) dual-mode appointments (on-site vs. telemed with automatic Jitsi room creation); (3) FHIR R4 Appointment resource synchronization so the data lives in a standards-compliant store usable by external systems; (4) automated reminders and waitlist management to reduce no-shows and fill cancellation slots. Clinical buyers see fewer scheduling errors and lower administrative burden; investors see a defensible workflow moat that goes beyond basic CRUD.
Audiences
- Investor: Scheduling with FHIR sync is a prerequisite for any hospital integration or insurance claim workflow. Dudoxx owning the appointment lifecycle closes the EHR loop.
- Clinical buyer (doctor/nurse/receptionist): Receptionists book, reschedule, and cancel through the portal; doctors see their daily schedule with patient context; nurses manage check-in and status updates. All changes are audited.
- Developer/partner: REST endpoints grouped by concern (CRUD, scheduling, dashboard, automation). FHIR sync is via
ResourceFacade<AppointmentResource>(NF2.1 migration) — injectAPPOINTMENT_FACADEtoken, never callFhirServicedirectly. - Internal (ops/support):
NoShowDetectionServiceruns a cron to auto-mark no-shows;AppointmentRemindersServicequeues SMS/email reminders. Both run via@nestjs/schedule— no BullMQ.
Architecture
The appointment domain lives in ddx-api/src/clinical/appointments/ and is structured in three tiers:
Tier 1 — Controllers (thin, 3 sub-controllers):
| Controller | Routes |
|---|---|
AppointmentsCrudController | POST/GET/PUT/DELETE /appointments |
AppointmentsScheduleController | /appointments/:id/status, /reschedule, /check-in |
AppointmentsQueryController | /appointments/dashboard, /export |
Tier 2 — AppointmentsService (orchestrator facade — backward-compatible API):
| Collaborator | Responsibility |
|---|---|
AppointmentCrudService | DB CRUD, pagination, export |
AppointmentSchedulingService | Conflict detection, slot reservation |
FhirAppointmentService | FHIR R4 CRUD via APPOINTMENT_FACADE |
AppointmentNotificationHandlerService | Email/SMS dispatch |
Tier 3 — BookingOrchestratorService (single booking entry point since v2.0.0),
ordered steps:
| # | Step | Owner |
|---|---|---|
| 1 | Generate appointment number | AppointmentNumberingService |
| 2 | Create FHIR Appointment | FhirAppointmentSyncService → APPOINTMENT_FACADE |
| 3 | Create Prisma Appointment record | — |
| 4 | Create Jitsi video meeting if mode=ONLINE | VideoService |
| 5 | Send notifications | AppointmentNotificationService |
| 6 | Emit audit log | AuditService |
APPOINTMENT_FACADE is a ResourceFacade<AppointmentResource> token registered in AppointmentsModule. The facade handles the Prisma ↔ FHIR ↔ MinIO storage pattern and upserts fhir_resource_link rows after every successful write (NF2.1 migration).
Appointment status is a separate AppointmentStatusService that enforces status-transition business rules (e.g. sets cancelledAt/checkedInAt timestamps and writes audit records).
Tech Stack & Choices
| Concern | Choice | Rationale |
|---|---|---|
| DB storage | prisma-main Appointment model | Postgres UUID primary keys, indexed by organizationId + startTime for schedule queries |
| FHIR sync | ResourceFacade<AppointmentResource> via APPOINTMENT_FACADE token | NF2.1 migration away from deprecated FhirService; facade handles link-row upserts |
| FHIR resource | FHIR R4 Appointment with Participant array | Maps practitioner, patient, and location FHIR references |
| Video meetings | Jitsi (port 15800) via VideoService | On-premise video, no external SaaS cost; room created only when mode=ONLINE |
| Reminders | AppointmentRemindersService via @nestjs/schedule | Polling cron; no BullMQ; Email + SMS via MailModule + SmsModule |
| No-show detection | NoShowDetectionService cron | Auto-marks appointments past end time still in CONFIRMED status |
| Conflict detection | AppointmentSchedulingService | Checks practitioner slots and overlapping DB records before write |
| Appointment numbering | AppointmentNumberingService | Generates human-readable sequential numbers per organization |
Data Flow
On-site booking (happy path):
- Receptionist POSTs
CreateAppointmentDtotoAppointmentsCrudController.create(). AppointmentsServicedelegates toBookingOrchestratorService.bookAppointment().AppointmentNumberingServicegenerates a sequential display number for the org.FhirAppointmentSyncService.createFhirAppointment()builds the FHIR R4 resource and callsAPPOINTMENT_FACADE.create()→ HAPI FHIR at port 18080.- Prisma
appointment.create()record written inddx_api_main. - If
mode=ONLINE:VideoService.createMeeting()provisions a Jitsi room; meeting URL stored on the appointment record. AppointmentNotificationService.notifyCreated()dispatches confirmation email/SMS.AuditService.log()writes toaudit_trailinddx_api_log.ResponseTransformInterceptorwraps result in{ data, meta }envelope.
Status transitions (check-in / cancel / no-show):
- PUT
/:id/status→AppointmentStatusService.updateStatus()→ validates transition, sets timestamp fields, writes audit record.
Implicated Code
ddx-api/src/clinical/appointments/booking-orchestrator.service.ts:48—BookingOrchestratorService; unified 7-step booking flow; v2.0.0 single entry point for all booking operationsddx-api/src/clinical/appointments/appointments.service.ts:46—AppointmentsServiceorchestrator facade; delegates to 4 domain services (crud, scheduling, fhir, notification)ddx-api/src/clinical/appointments/fhir-appointment-sync.service.ts:56—FhirAppointmentSyncService; NF2.1 migration toAPPOINTMENT_FACADE; builds FHIR R4Appointmentresourceddx-api/src/clinical/appointments/appointment-status.service.ts:17—AppointmentStatusService; status transition rules,cancelledAt/checkedInAttimestamps, audit logging at:60ddx-api/src/clinical/appointments/appointments.module.ts:1— module wiring;APPOINTMENT_FACADEtoken provided asResourceFacade<AppointmentResource>;forwardRefcycle withVideoModuleandVisitsModuleddx-api/src/clinical/appointments/no-show-detection.service.ts—NoShowDetectionServicecronddx-api/src/clinical/appointments/helpers/fhir-appointment-mapper.helper.ts— FHIR resource builder (maps Prisma fields to FHIR R4Participantarray)ddx-api/src/clinical/appointments/helpers/appointment-validator.helper.ts— business-rule guard (conflict detection pre-checks)
Operational Notes
- FHIR dependency: Booking will fail if HAPI FHIR (port 18080) is unreachable —
FhirAppointmentSyncServicesurfacesInternalServerErrorExceptioncarryingerror.kind. CheckHAPI_FHIR_BASE_URLenv var. - Appointment → Visit link: Completing an appointment (status
COMPLETED) triggersVisitAppointmentLinkingServiceto create or associate a clinical visit record. This is the bridge to the visits domain. - Recurring appointments: Handled by
RecurringAppointmentsModule(separate module atddx-api/src/clinical/recurring-appointments/) — creates a series of individual appointment records. - Waitlist:
WaitlistServicein this module manages the per-practitioner waitlist; when a slot opens (cancellation), waitlisted patients are notified. forwardRefcycles:AppointmentsModulehas circular imports withVideoModuleandVisitsModule— both resolved viaforwardRef(); do not refactor without validating the DI cycle still resolves.- On-site vs. telemed split:
AppointmentModeenum (IN_PERSON/ONLINE) drives video room creation.mode=IN_PERSONskipsVideoServiceentirely.
Related Topics
- Clinical Visits — appointment completion triggers visit creation via
VisitAppointmentLinkingService - FHIR Partitions & Tenancy — FHIR
Appointmentresources are stored in the tenant's HAPI partition - Audit Logging — every status change and booking event is written to
audit_trail