Appointments & Operatory Scheduling

v0.1.0

Atomic multi-resource scheduling (chair, dentist, assistant), automated aerosol turnover disinfection buffers, and conflict detection.

Appointments & Operatory Scheduling

The Appointments & Operatory Scheduling bounded context coordinates clinic workflow logistics. It enforces atomic multi-resource reservations, automated aerosol room turnover buffers, and conflict detection across dental operatories, dentists, and dental assistants.


Atomic Multi-Resource Scheduling

Unlike standard medical appointments that book only a clinician, dental procedures require the concurrent coordination of three physical and human resources:

flowchart TD
    APT["Appointment Booking"]
    
    subgraph MultiResource["Atomic Multi-Resource Reservation"]
        CHAIR["1. Operatory Chair<br/>(Physical dental operatory)"]
        DENTIST["2. Attending Dentist<br/>(Clinician / Surgeon)"]
        ASST["3. Dental Assistant<br/>(Chairside auxiliary)"]
    end
    
    BUFFER["Automated Aerosol Turnover Buffer<br/>(10-15 min room disinfection)"]
    
    APT --> CHAIR
    APT --> DENTIST
    APT --> ASST
    CHAIR --> BUFFER

If any single resource is unavailable or has an overlapping reservation, the entire transaction rolls back and throws a SchedulingConflictException.


Aerosol Room Turnover Buffer Invariant

Aerosol-generating dental procedures (e.g., high-speed air turbines, ultrasonic scalers, air polishers) release bio-aerosols into the operatory atmosphere.

To satisfy infection control mandates (CDC & OSHA), Obelawium Dental automatically computes and appends an aerosol disinfection buffer (configured via scheduling.aerosol_buffer_minutes, default 15 minutes) to the operatory chair schedule:

$$\text{Operatory Release Time} = \text{Appointment End Time} + \text{Aerosol Buffer Minutes}$$

  • The attending dentist and assistant are released at end_time so they can attend to other patients.
  • The operatory chair remains locked until aerosol_buffer_end_time to allow room air evacuation and surface disinfection.
use Obelaw\Ium\Dental\Exceptions\SchedulingConflictException;

// Attempting to book an operatory during an active aerosol turnover buffer
// throws: SchedulingConflictException: Resource 'Operatory Chair' #1 is conflicted.

Appointment Lifecycle

use Obelaw\Ium\Dental\Enums\AppointmentStatus;

enum AppointmentStatus: string
{
    case SCHEDULED = 'scheduled';     // Appointment booked on calendar
    case CONFIRMED = 'confirmed';     // Confirmed via automated SMS / phone call
    case CHECKED_IN = 'checked_in';   // Patient arrived in clinic waiting area
    case IN_PROGRESS = 'in_progress'; // Seated chairside; treatment underway
    case COMPLETED = 'completed';     // Dismissed; procedures finalized
    case CANCELLED = 'cancelled';     // Cancelled prior to visit
    case NO_SHOW = 'no_show';         // Patient failed to attend without notice
}

Fluent Service Operations

Scheduling workflows are orchestrated across ium()->dental()->scheduling() and ium()->dental()->appointments():

1. Booking an Appointment with Aerosol Buffer

use Obelaw\Ium\Dental\Data\BookAppointmentDto;
use DateTimeImmutable;

$appointment = ium()->dental()->scheduling()->bookAppointment(BookAppointmentDto::from([
    'patient_id' => $patient->id,
    'operatory_id' => '1',                // Operatory 1 (South Wing)
    'dentist_id' => 'DR-MARTINEZ',        // Attending Dentist
    'assistant_id' => 'ASST-JONES',       // Chairside Assistant
    'start' => new DateTimeImmutable('2026-10-15 10:00:00'),
    'end' => new DateTimeImmutable('2026-10-15 11:00:00'),
    'reason' => 'Root Canal Therapy on tooth 19',
    'is_aerosol_generating' => true,      // Automatically adds 15 min disinfection buffer
    'aerosol_buffer_minutes' => 15,
    'actor_id' => 'USER-RECEPTION',
]));

// Dentist released at: 11:00:00
// Operatory chair released at: 11:15:00

2. Confirming and Checking In Patients

// Patient confirms via SMS reminder
ium()->dental()->appointments()->confirm($appointment->id);

// Patient arrives at front desk
ium()->dental()->appointments()->checkIn($appointment->id);

3. Starting Encounter with Chairside Autoclave Verification

When seating the patient, the assistant scans the sterile instrument cassette barcode. The appointment can only transition to IN_PROGRESS if the cassette passes sterilization validation:

use Obelaw\Ium\Dental\ValueObjects\AutoclaveBarcode;

$cassetteBarcode = AutoclaveBarcode::from('CAS-2026-0941');

$activeAppointment = ium()->dental()->appointments()->start(
    appointmentId: $appointment->id,
    cassetteBarcode: $cassetteBarcode
);

4. Completing the Appointment

ium()->dental()->appointments()->complete($appointment->id);

5. Blocking Operatories for Maintenance or Lunch

$block = ium()->dental()->scheduling()->blockSlot(
    resourceId: '1',
    resourceType: 'operatory',
    start: new DateTimeImmutable('2026-10-15 13:00:00'),
    end: new DateTimeImmutable('2026-10-15 14:00:00'),
    reason: 'Dental unit waterline shock chlorination and filter maintenance.'
);

6. Checking Available Resources

$available = ium()->dental()->scheduling()->availableResources(
    start: new DateTimeImmutable('2026-10-15 14:00:00'),
    end: new DateTimeImmutable('2026-10-15 15:00:00')
);

// Returns list of free operatories, available dentists, and free assistants

Emitted Domain Events

Event ClassTriggerPayload
AppointmentScheduledFired when all resources are successfully reserved.Appointment $appointment
AppointmentCancelledFired when an appointment is cancelled and slots released.Appointment $appointment