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
| Item | Value |
|---|
| Owning Core | PM (Practice Management) |
| DB Tables | pm_appointments, pm_appointment_reminders, pm_waitlist, pm_encounters |
| Key Events Published | appointment_scheduled, appointment_cancelled, appointment_checked_in, appointment_completed, appointment_no_show, encounter_completed |
| Key Events Consumed | pm_eligibility_verified (PM-02 β display coverage status) |
| Platform Layers Used | PF-10 (notifications/reminders), PF-15 (appointment_type picklist), PF-30 (permissions) |
| Auth Guard | RequirePermission permission="pm.scheduling.view" |
| RLS Helper | pf_has_org_access(organization_id, auth.uid()) |
| Double-Book Override | pm.scheduling.override_double_booking (category: approve) |
| Recurrence Schema | iCal RRULE RFC 5545 stored as JSONB β see GAP-2 Errata in spec |
| Telehealth URL | Phase 1: nullable plaintext (manual entry only); Phase 2 (PM-13): on-demand generation + access logging |
| Appointment Types | PF-15 picklist pm_appointment_types; seed defaults: individual, family, intake, medication_management, telehealth, walk_in |
| PM-05 Prerequisite | pm_provider_schedules migration MUST exist before T9.2 double-booking check can query availability |
Integration Points
| Dependency | Type | Direction | Notes |
|---|
| PM-01 | Data | PM-03 reads | Patient linked to appointments via pm_patients(id). No separate contract. |
| PM-05 | Data / API | PM-03 reads | Provider availability for scheduling; provider_id β pf_profiles(id). PM-05 Phase 1 migration is a hard prerequisite for T9.2. |
| PM-07 | Event | PM-03 emits | Completed appointments β encounter_completed β charge capture. See CL-PM-ENCOUNTER-TO-BILLING. |
| PM-13 | Data / Workflow | PM-03 defers | Telehealth URL generation and consent. Phase 1: telehealth_url is manual plaintext only. See CL-PM-TELEHEALTH. |
| PF-10 | Platform Layer | PM-03 invokes | Appointment reminders (SMS, email, voice). Invoke via useNotifications / sendNotification from @/platform/notifications. Store confirmation in pm_appointment_reminders. |
| PF-15 | Platform Layer | PM-03 reads | appointment_type values sourced from picklist pm_appointment_types. Org-customizable. |
| PF-30 | Platform Layer | PM-03 checks | pm.scheduling.view, pm.scheduling.override_double_booking permissions enforced at route and mutation level. |
| CL-04 | Event | PM-03 emits | appointment_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
| Event | Trigger | Consumers | Channel |
|---|
appointment_scheduled | Appointment INSERT with status scheduled | CL (encounter context), PF-10 (reminder scheduling) | cl_pm_events |
appointment_cancelled | Status transition β cancelled | CL (cancel pending note), PF-10 (cancellation notice) | cl_pm_events |
appointment_checked_in | Status transition β checked_in or in_progress | CL-04 (create draft note), PM-07 (open charge) | cl_pm_events |
appointment_completed | Status transition β completed | PM-07 (close charge), CL-04 (prompt finalize note) | cl_pm_events |
appointment_no_show | Status transition β no_show | CL-04 (cancel pending note), PM-07 (no-show charge if applicable) | cl_pm_events |
encounter_completed | pm_encounters.status β documented or charge_captured | PM-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)
Before Phase 2 (Hooks & API)
Before Phase 3 (UI)
Before Phase 4 (Testing & Docs)
Common Mistakes
| Mistake | Correct Pattern |
|---|
Using entity_id in event payload | Use appointment_id (matches EVENT_CONTRACTS.md canonical schema) |
Using start_at/end_at in event payload | Use start_datetime/end_datetime |
Querying pf_profiles directly in RLS policy | Use pf_has_org_access(organization_id, auth.uid()) SECURITY DEFINER helper |
ENABLE ROW LEVEL SECURITY without FORCE | Always pair: ENABLE then FORCE ROW LEVEL SECURITY |
| Hardcoded appointment type strings | Use 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 save | Wrap in try/catch; set delivery_status = 'failed'; do not throw |
| Storing long-lived telehealth URLs in Phase 1 | Phase 1: manual entry only; COMMENT ON COLUMN documents this |
Using cl_events channel for cross-core events | Cross-core CLβPM events use cl_pm_events channel |
Skipping series_id on recurring appointments | is_recurring = true requires series_id UUID shared across all series instances |
Creating PM_SCHEDULING_PERMISSIONS as separate export | Add to existing PM_PERMISSIONS object under // PM-03 comment |
Fallback / Resilience
| Failure Scenario | Behavior | Recovery |
|---|
| PF-10 reminder delivery fails | Log error; set delivery_status = 'failed'; do NOT block appointment save | Manual retry or cron re-attempt |
| PM-05 tables missing | Log TODO: PM-05; allow scheduling; double-booking check is skipped | Deploy PM-05 Phase 1 migration |
| Telehealth URL generation fails (Phase 2) | Retry once; fall back to manual link entry; notify scheduler | PM-13 recovery flow |
appointment_scheduled event publish fails | Log silently; appointment is saved; event is best-effort in Phase 1 | Implement event retry in Phase 3+ |