> ## 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.

# Appointment Scheduling – Integration

> 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…

**Feature ID:** PM-03
**Status:** 📝 Planned
**Spec Reference:** [PM-03-appointment-scheduling.md](../../../specs/pm/specs/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](./CL-PM-ENCOUNTER-TO-BILLING.md).                                                     |
| **PM-13**  | Data / Workflow | PM-03 defers  | Telehealth URL generation and consent. Phase 1: `telehealth_url` is manual plaintext only. See [CL-PM-TELEHEALTH](./CL-PM-TELEHEALTH.md).                                               |
| **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](./CL-PM-ENCOUNTER-TO-BILLING.md). |

***

## 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`.

```json theme={null}
{
  "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`

```json theme={null}
{
  "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)

```typescript theme={null}
// 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`

```typescript theme={null}
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

```text theme={null}
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)

```text theme={null}
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

```text theme={null}
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

```text theme={null}
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

| 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                   |

***

## Related Integration Docs

* [CL-PM-ENCOUNTER-TO-BILLING](./CL-PM-ENCOUNTER-TO-BILLING.md) – Appointment → encounter → charge capture
* [CL-PM-TELEHEALTH](./CL-PM-TELEHEALTH.md) – Telehealth flag and session linkage
* [CROSS\_CORE\_INTEGRATIONS.md](./CROSS_CORE_INTEGRATIONS.md) – Integration matrix (PM ↔ CL, PF-10)
* [PM-05-provider-schedule-availability-INTEGRATION.md](./provider-schedule-availability-integration.md) – Provider schedule (hard prerequisite)
* [EVENT\_CONTRACTS.md](./EVENT_CONTRACTS.md) – Canonical event payload schemas

***

## 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+ |
