Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Feature ID: PM-03 Status: πŸ“ Planned Spec Reference: PM-03-appointment-scheduling.md Version: 1.1 Last Updated: 2026-02-19 Last Verified: N/A (Planned β€” update to ISO date after Phase 1 deployment) Constitution Reference: Β§1.3 Integration Patterns, Β§2.1 Integration documentation

Quick Reference

ItemValue
Owning CorePM (Practice Management)
DB Tablespm_appointments, pm_appointment_reminders, pm_waitlist, pm_encounters
Key Events Publishedappointment_scheduled, appointment_cancelled, appointment_checked_in, appointment_completed, appointment_no_show, encounter_completed
Key Events Consumedpm_eligibility_verified (PM-02 β†’ display coverage status)
Platform Layers UsedPF-10 (notifications/reminders), PF-15 (appointment_type picklist), PF-30 (permissions)
Auth GuardRequirePermission permission="pm.scheduling.view"
RLS Helperpf_has_org_access(organization_id, auth.uid())
Double-Book Overridepm.scheduling.override_double_booking (category: approve)
Recurrence SchemaiCal RRULE RFC 5545 stored as JSONB β€” see GAP-2 Errata in spec
Telehealth URLPhase 1: nullable plaintext (manual entry only); Phase 2 (PM-13): on-demand generation + access logging
Appointment TypesPF-15 picklist pm_appointment_types; seed defaults: individual, family, intake, medication_management, telehealth, walk_in
PM-05 Prerequisitepm_provider_schedules migration MUST exist before T9.2 double-booking check can query availability

Integration Points

DependencyTypeDirectionNotes
PM-01DataPM-03 readsPatient linked to appointments via pm_patients(id). No separate contract.
PM-05Data / APIPM-03 readsProvider availability for scheduling; provider_id β†’ pf_profiles(id). PM-05 Phase 1 migration is a hard prerequisite for T9.2.
PM-07EventPM-03 emitsCompleted appointments β†’ encounter_completed β†’ charge capture. See CL-PM-ENCOUNTER-TO-BILLING.
PM-13Data / WorkflowPM-03 defersTelehealth URL generation and consent. Phase 1: telehealth_url is manual plaintext only. See CL-PM-TELEHEALTH.
PF-10Platform LayerPM-03 invokesAppointment reminders (SMS, email, voice). Invoke via useNotifications / sendNotification from @/platform/notifications. Store confirmation in pm_appointment_reminders.
PF-15Platform LayerPM-03 readsappointment_type values sourced from picklist pm_appointment_types. Org-customizable.
PF-30Platform LayerPM-03 checkspm.scheduling.view, pm.scheduling.override_double_booking permissions enforced at route and mutation level.
CL-04EventPM-03 emitsappointment_checked_in β†’ CL-04 creates draft progress note. encounter_completed β†’ CL-04 links note to encounter. See CL-PM-ENCOUNTER-TO-BILLING.

Event Contracts

All PM-03 events are registered in fw_workflow_events (seeded by migration 20260219124331_...). Publish via publishEvent() from @/platform/events.

Published Events

EventTriggerConsumersChannel
appointment_scheduledAppointment INSERT with status scheduledCL (encounter context), PF-10 (reminder scheduling)cl_pm_events
appointment_cancelledStatus transition β†’ cancelledCL (cancel pending note), PF-10 (cancellation notice)cl_pm_events
appointment_checked_inStatus transition β†’ checked_in or in_progressCL-04 (create draft note), PM-07 (open charge)cl_pm_events
appointment_completedStatus transition β†’ completedPM-07 (close charge), CL-04 (prompt finalize note)cl_pm_events
appointment_no_showStatus transition β†’ no_showCL-04 (cancel pending note), PM-07 (no-show charge if applicable)cl_pm_events
encounter_completedpm_encounters.status β†’ documented or charge_capturedPM-07 (trigger billing), CL-04 (confirm note linked)cl_pm_events

Canonical Payload: appointment_scheduled

DOC-1 fix: All field names match EVENT_CONTRACTS.md canonical schema β€” use appointment_id and start_datetime/end_datetime, not entity_id or start_at/end_at.
{
  "event_type": "appointment_scheduled",
  "organization_id": "org-uuid",
  "appointment_id": "appointment-uuid",
  "actor_id": "scheduler-user-uuid",
  "patient_id": "patient-uuid",
  "provider_id": "provider-uuid",
  "appointment_type": "individual",
  "start_datetime": "2026-03-01T09:00:00Z",
  "end_datetime": "2026-03-01T10:00:00Z",
  "is_telehealth": false,
  "status": "scheduled",
  "timestamp": "2026-02-19T12:00:00Z"
}

Canonical Payload: encounter_completed

{
  "event_type": "encounter_completed",
  "organization_id": "org-uuid",
  "encounter_id": "encounter-uuid",
  "appointment_id": "appointment-uuid",
  "patient_id": "patient-uuid",
  "provider_id": "provider-uuid",
  "encounter_type": "outpatient",
  "place_of_service": "11",
  "timestamp": "2026-02-19T12:00:00Z"
}

API / Data Contracts

PM-05: Provider Availability (READ)

Pattern: Direct Supabase query (PM-03 reads pm_provider_schedules owned by PM-05)
// Check provider availability before scheduling
const { data: blocks } = await supabase
  .from('pm_provider_schedules')
  .select('start_datetime, end_datetime, schedule_type')
  .eq('organization_id', orgId)
  .eq('provider_id', providerId)
  .gte('start_datetime', windowStart)
  .lte('end_datetime', windowEnd);
Prerequisite: pm_provider_schedules table must exist (PM-05 Phase 1 migration). If not present, double-booking check MUST be stubbed with a TODO: PM-05 comment β€” do not silently skip.

PF-10: Reminder Delivery (INVOKE)

Pattern: Platform Integration Layer β€” @/platform/notifications
import { sendNotification } from '@/platform/notifications';

// Schedule appointment reminder
await sendNotification({
  userId: patientPortalUserId,          // or provider userId
  type: 'info',
  title: 'Appointment Reminder',
  message: `Your appointment is on ${formatDate(startDatetime)}`,
  channels: ['sms', 'email'],           // per pm_module_settings config
  scheduledAt: reminderScheduledAt,
});

// Store confirmation in pm_appointment_reminders
await supabase.from('pm_appointment_reminders').insert({
  organization_id: appointment.organization_id,
  appointment_id: appointment.id,
  reminder_type: channel,
  scheduled_at: reminderScheduledAt,
  delivery_status: 'pending',
});
Fallback: PF-10 failure MUST NOT block appointment CRUD. Wrap in try/catch; log error; set delivery_status = 'failed' in pm_appointment_reminders.

Decision Trees

Appointment Type Decision

Is the organization behavioral health (standard service types)?
β”œβ”€β”€ YES β†’ Use PF-15 picklist 'pm_appointment_types' (org-customizable)
β”‚         Seed defaults: individual, family, intake, medication_management,
β”‚                        telehealth, walk_in
β”‚         No CHECK constraint β€” picklist enforces at application layer
└── NO  β†’ (Not applicable: all Encore Health OS orgs are behavioral health)

Telehealth URL Decision (Phase 1 vs Phase 2)

Is PM-13 available?
β”œβ”€β”€ NO (Phase 1) β†’
β”‚     telehealth_url is nullable TEXT
β”‚     Allow manual entry only
β”‚     COMMENT ON COLUMN documents lifecycle intent
β”‚     Do NOT store long-lived production video links
β”‚     Audit trail: is_telehealth flag + created_by + updated_at
└── YES (Phase 2) β†’
      On-demand URL generation at check-in
      URL expiry enforcement (max 2h)
      Access logging in pm_encounters
      Encryption at rest via Supabase Vault

Recurrence Rule Decision

Does the appointment have is_recurring = true?
β”œβ”€β”€ YES β†’ recurrence_rule JSONB MUST follow iCal RRULE RFC 5545
β”‚         Example: { "FREQ": "WEEKLY", "BYDAY": "MO,WE,FR", "COUNT": 12 }
β”‚         series_id MUST be set (UUID shared across all instances)
β”‚         Use rrule npm library for client-side parsing/generation
└── NO  β†’ recurrence_rule = NULL, series_id = NULL

Double-Booking Prevention

User schedules appointment for provider at time T:
β”œβ”€β”€ Query pm_appointments WHERE provider_id = X AND start_datetime <= T+duration AND end_datetime >= T AND deleted_at IS NULL
β”‚   └── Query pm_provider_schedules (PM-05) for availability blocks
β”‚
β”œβ”€β”€ Conflict found?
β”‚   β”œβ”€β”€ User has pm.scheduling.override_double_booking permission?
β”‚   β”‚   β”œβ”€β”€ YES β†’ Show conflict warning; allow save with confirmation
β”‚   β”‚   └── NO  β†’ Block save; show next available slots
β”‚   └── NO conflict β†’ Proceed normally
└── PM-05 tables missing? β†’ Stub check; log TODO; allow save

Pre-Flight Checklist

Use this checklist before beginning each PM-03 implementation phase.

Before Phase 1 (Database)

  • PM-05 Phase 1 migration confirmed deployed (pm_provider_schedules exists in DB)
  • pf_has_org_access function confirmed in DB (SELECT proname FROM pg_proc WHERE proname = 'pf_has_org_access')
  • pm_has_org_access function confirmed in DB
  • pm_set_updated_at trigger function confirmed in DB (from PM-01 β€” do NOT recreate, just reference)
  • fw_workflow_events rows confirmed for all six PM-03 events (migration 20260219124331 applied)
  • pf_module_permissions rows confirmed for pm.scheduling.view, pm.scheduling.override_double_booking
  • appointment_scheduled and encounter_completed present in KnownEventName type (src/platform/events/types.ts)
  • PM_PERMISSIONS.SCHEDULING_VIEW and PM_PERMISSIONS.SCHEDULING_OVERRIDE_DOUBLE_BOOKING present in constants.ts

Before Phase 2 (Hooks & API)

  • All Phase 1 tables confirmed in live DB (pm_appointments, pm_appointment_reminders, pm_waitlist, pm_encounters)
  • FORCE ROW LEVEL SECURITY confirmed on all four tables
  • RLS cross-tenant test passes (org1 cannot read org2 appointments)
  • @/platform/notifications integration pattern confirmed (or PF-10 contract stub documented)

Before Phase 3 (UI)

  • All hooks from Phase 2 returning correct data shapes
  • PF-15 picklist pm_appointment_types seeded with defaults
  • Calendar library selected and installed (recommend @fullcalendar/react or react-big-calendar)
  • Dialog size standards confirmed: Create/Edit = sm:max-w-lg, Waitlist = sm:max-w-md, Reminder/No-show = sm:max-w-sm

Before Phase 4 (Testing & Docs)

  • All RLS tests authored for pm_appointments, pm_appointment_reminders, pm_waitlist, pm_encounters
  • E2E test covers: schedule β†’ confirm β†’ check-in β†’ complete flow
  • Performance tested: schedule view p95 < 1s, creation p95 < 1s, availability search p95 < 2s
  • pm.admin placeholder in /pm/settings route cleaned up (replaced with real permission key)

Common Mistakes

MistakeCorrect Pattern
Using entity_id in event payloadUse appointment_id (matches EVENT_CONTRACTS.md canonical schema)
Using start_at/end_at in event payloadUse start_datetime/end_datetime
Querying pf_profiles directly in RLS policyUse pf_has_org_access(organization_id, auth.uid()) SECURITY DEFINER helper
ENABLE ROW LEVEL SECURITY without FORCEAlways pair: ENABLE then FORCE ROW LEVEL SECURITY
Hardcoded appointment type stringsUse PF-15 picklist pm_appointment_types via usePicklist
Importing from @/cores/cl/...Use @/platform/clinical or event subscriptions
Allowing PF-10 failure to block appointment saveWrap in try/catch; set delivery_status = 'failed'; do not throw
Storing long-lived telehealth URLs in Phase 1Phase 1: manual entry only; COMMENT ON COLUMN documents this
Using cl_events channel for cross-core eventsCross-core CL↔PM events use cl_pm_events channel
Skipping series_id on recurring appointmentsis_recurring = true requires series_id UUID shared across all series instances
Creating PM_SCHEDULING_PERMISSIONS as separate exportAdd to existing PM_PERMISSIONS object under // PM-03 comment


Fallback / Resilience

Failure ScenarioBehaviorRecovery
PF-10 reminder delivery failsLog error; set delivery_status = 'failed'; do NOT block appointment saveManual retry or cron re-attempt
PM-05 tables missingLog TODO: PM-05; allow scheduling; double-booking check is skippedDeploy PM-05 Phase 1 migration
Telehealth URL generation fails (Phase 2)Retry once; fall back to manual link entry; notify schedulerPM-13 recovery flow
appointment_scheduled event publish failsLog silently; appointment is saved; event is best-effort in Phase 1Implement event retry in Phase 3+