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

# E-Prescribing (EPCS) — Integration Contract

> Feature ID: CL-06 Status: \U0001F6A7 In Progress -e-prescribing-epcs.md Last Updated: 2026-02-18 Last Verified: 2026-02-18

**Feature ID:** CL-06\
**Status:** 🚧 In Progress\
**Spec:** [CL-06-e-prescribing-epcs.md](../../../specs/cl/specs/CL-06-e-prescribing-epcs.md)\
**Last Updated:** 2026-02-18\
**Last Verified:** 2026-02-18

***

## Table of Contents

1. [Overview](#overview)
2. [Quick Reference](#quick-reference)
3. [Integration Points](#integration-points)
4. [API / Data Contracts](#api--data-contracts)
5. [Event Contracts](#event-contracts)
6. [Edge Functions](#edge-functions)
7. [Decision Trees](#decision-trees)
8. [Pattern Library](#pattern-library)
9. [Security and RLS](#security-and-rls)
10. [Common Mistakes](#common-mistakes)
11. [Pre-Flight Checklist](#pre-flight-checklist)
12. [Related Docs](#related-docs)

***

## Overview

CL-06 defines electronic prescribing (e-prescribing) and EPCS (electronic prescribing for controlled substances): Surescripts integration, NCPDP SCRIPT 2023011, RTPB, and integration with medication list (CL-05), clinical decision support (CL-08), and Arizona CSPMP (CL-17).

Prescriptions surface within the **Medications tab** of PatientChartPage (embedded in `ChartMedicationsSection`) via a permission-gated "New Prescription" button. A standalone `/cl/pharmacies` route manages the org-level pharmacy directory.

***

## Quick Reference

| Category                         | Name                                   | Location                                                                                |
| -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------- |
| **Table**                        | `cl_prescriptions`                     | Business entity (PHI); org + chart scoped; `site_id` (nullable)                         |
| **Table**                        | `cl_pharmacies`                        | Reference data; org-scoped only (no `site_id` — deliberate, see Clarifications)         |
| **Event (outbound)**             | `prescription_sent`                    | Published by `cl-transmit-prescription` edge function after Surescripts acknowledgement |
| **Edge Function**                | `cl-transmit-prescription`             | `verify_jwt: true`; handles Surescripts stub transmit + EPCS audit                      |
| **Permission (view)**            | `cl.prescriptions.view`                | View prescription history for a chart                                                   |
| **Permission (create)**          | `cl.prescriptions.create`              | Create and send prescriptions                                                           |
| **Permission (cancel)**          | `cl.prescriptions.cancel`              | Cancel or void prescriptions                                                            |
| **Permission (pharmacy view)**   | `cl.pharmacies.view`                   | View org pharmacy directory                                                             |
| **Permission (pharmacy manage)** | `cl.pharmacies.manage`                 | Add/edit pharmacies                                                                     |
| **RLS helper**                   | `cl_has_org_access_for_epcs(p_org_id)` | Used for SELECT/INSERT/UPDATE/DELETE policies (calls `pf_has_org_access`)               |
| **Trigger**                      | `cl_pharmacies_set_updated_at()`       | Table-specific trigger on `cl_pharmacies`                                               |
| **Trigger**                      | `cl_prescriptions_set_updated_at()`    | Table-specific trigger on `cl_prescriptions`                                            |

***

## Integration Points

| Dependency                        | Pattern  | Purpose                                                                    |
| --------------------------------- | -------- | -------------------------------------------------------------------------- |
| CL-01 (Patient Chart)             | Direct   | Prescriptions scoped to chart; `chart_id` FK                               |
| CL-05 (Medication Management)     | Direct   | `medication_id` FK links prescription to medication list entry; optional   |
| CL-08 (Clinical Decision Support) | Direct   | Drug interaction and allergy alerts during prescribing (future phase)      |
| CL-17 (Arizona CSPMP)             | Direct   | PDMP query before controlled substance prescribing (CL-17 scope)           |
| Surescripts                       | External | Prescription routing, RTPB, formulary (stub in MVP)                        |
| HTI-4 / NCPDP SCRIPT 2023011      | Standard | Message format, RTPB certification                                         |
| PF-10 (Notifications)             | Event    | Receives `prescription_sent` event to notify prescriber of acknowledgement |
| PM-07 (Billing)                   | Event    | Receives `prescription_sent` for charge-capture correlation (future phase) |

***

## API / Data Contracts

### `cl_prescriptions`

* **Scope:** `organization_id` + `chart_id`; also has `site_id` (nullable) for site-level context
* **PHI:** Yes — full RLS enforcement + FORCE ROW LEVEL SECURITY
* **Key FK columns:** `chart_id → cl_patient_charts(id)`, `prescriber_id → pf_profiles(id)`, `pharmacy_id → cl_pharmacies(id)`
* **Transmission status lifecycle:** `pending → sent → acknowledged | error | cancelled`
* **EPCS columns:** `is_controlled`, `dea_schedule`, `epcs_token` (TEXT — set when EPCS two-factor completed)
* **RTPB columns:** `rtpb_checked_at` (TIMESTAMPTZ), `rtpb_formulary_status`, `rtpb_patient_cost`
* **Other columns:** `medication_name`, `ndc_code`, `strength`, `form`, `route`, `quantity`, `quantity_unit`, `days_supply`, `refills`, `sig`, `transmitted_at`, `error_message`
* **Soft delete:** `deleted_at TIMESTAMPTZ` — all application queries must filter `deleted_at IS NULL`
* **Custom fields:** `custom_fields JSONB NOT NULL DEFAULT '{}'`

### `cl_pharmacies`

* **Scope:** `organization_id` only (no `site_id` — org-wide pharmacy directory; see CL-06 Clarifications)
* **Address columns:** `address_line_1`, `address_line_2`, `city`, `state`, `zip_code` (no single `address` column)
* **EPCS flag:** `is_surescripts_enabled BOOLEAN` — picker filters this for controlled-substance prescriptions
* **NCPDP ID:** `ncpdp_id` (TEXT); application enforces uniqueness per org
* **Soft delete:** `deleted_at TIMESTAMPTZ` — pharmacy picker queries filter `WHERE deleted_at IS NULL AND is_active = true`
* **Custom fields:** `custom_fields JSONB NOT NULL DEFAULT '{}'` (migration includes it)

### Privileged Operations (Edge Function Only)

* **Prescription transmit to Surescripts:** Edge Function `cl-transmit-prescription`; service role client; validates auth before transmit
* **EPCS signing/token validation:** Edge Function only (FIPS path delegated to certified vendor: DrFirst or DoseSpot)
* **SECURITY DEFINER** functions used only for DB operations that bypass RLS (audit writes, webhook callbacks)

***

## Event Contracts

### Outbound: `prescription_sent`

| Field                           | Value                                                                                       |
| ------------------------------- | ------------------------------------------------------------------------------------------- |
| **Event name**                  | `prescription_sent`                                                                         |
| **Owning core**                 | `cl`                                                                                        |
| **Category**                    | `operational`                                                                               |
| **Publisher**                   | `cl-transmit-prescription` edge function (service client after Surescripts acknowledgement) |
| **Channel**                     | `cl_pm_events` (domain events channel)                                                      |
| **Subscribers**                 | PM-07 (billing correlation), PF-10 (prescriber acknowledgement notification)                |
| **Seeded in migration**         | Yes — `fw_workflow_events` ON CONFLICT DO NOTHING                                           |
| **`KnownEventName` registered** | Yes — `src/platform/events/types.ts`                                                        |

**Payload schema:**

```json theme={null}
{
  "prescription_id": "string (UUID)",
  "chart_id": "string (UUID)",
  "organization_id": "string (UUID)",
  "prescriber_id": "string (UUID)",
  "pharmacy_id": "string (UUID or null)",
  "medication_name": "string",
  "is_controlled": "boolean",
  "schedule": "string or null (II/III/IV/V)",
  "sent_at": "string (ISO 8601)"
}
```

> **Note:** `prescription_sent` is published by the edge function using `createServiceClient()`, not from React client code. The event fires only after Surescripts acknowledgement (status transitions to `acknowledged`) or on MVP stub success.

### No Inbound Events

CL-06 does not subscribe to events from other cores for MVP. Future: CL-17 PDMP result may trigger a workflow event once CL-17 is implemented.

***

## Edge Functions

### `cl-transmit-prescription`

| Attribute           | Value                                                                        |
| ------------------- | ---------------------------------------------------------------------------- |
| **Function name**   | `cl-transmit-prescription`                                                   |
| **`verify_jwt`**    | `true` (user-authenticated prescriber action)                                |
| **Auth pattern**    | `validateAuth()` from `_shared/auth.ts`                                      |
| **Client**          | `createServiceClient()` from `_shared/supabase.ts`                           |
| **Logging**         | `createLogger('cl-transmit-prescription')`                                   |
| **CORS**            | `getCorsHeaders(origin)` from `_shared/cors.ts`                              |
| **Error responses** | `createErrorResponse()` / `createValidationError()` from `_shared/errors.ts` |
| **Serve API**       | `Deno.serve()` (not deprecated `serve()`)                                    |
| **Supabase import** | `npm:@supabase/supabase-js@2` (not `esm.sh`)                                 |

**Request body:**

```json theme={null}
{
  "prescription_id": "string (UUID)",
  "organization_id": "string (UUID)"
}
```

**Responsibilities:**

1. Validate JWT + org access (`validateAuth`, `verifyOrgAccess`)
2. Fetch prescription from `cl_prescriptions` (verify ownership)
3. Call Surescripts stub (MVP) or real Surescripts API (production)
4. Update `transmission_status` and `surescripts_message_id` on success
5. Write audit log entry for controlled-substance transmissions (`pf_audit_logs`)
6. Publish `prescription_sent` event via `fw_domain_events` insert
7. Return `{ success: true, transmission_status: 'acknowledged', correlationId }`

**EPCS path (controlled substances):**

* Validate `epcs_token` is set on the prescription record before transmitting (EPCS two-factor completed)
* Delegate FIPS 140-2 / token validation to certified vendor (DrFirst/DoseSpot stub in MVP)

***

## Decision Trees

### When to Use Edge Function vs. Client Mutation

```
Prescription operation:
│
├── Is this a transmission to Surescripts?
│   └── YES → Use cl-transmit-prescription edge function
│
├── Is this an EPCS signature / 2FA token validation?
│   └── YES → Edge function (vendor stub in MVP)
│
├── Is this create/update/cancel (non-transmission)?
│   └── YES → Direct Supabase mutation from React hook
│       (still filter by organization_id for defense-in-depth)
│
└── Is this a PDMP check (CL-17)?
    └── YES → CL-17 scope; not CL-06 responsibility
```

### EPCS Flow (Controlled Substances)

```
Prescriber creates Schedule II–V prescription:
│
├── Drug interaction / allergy check (CL-08 / CL-05 service)
│
├── RTPB check (if rtpb_checked_at not set)
│   └── Show cost / formulary alternatives panel
│
├── CSPMP/PDMP check (CL-17 linkage — out of scope for CL-06)
│
├── EPCSConfirmDialog:
│   └── "I acknowledge this is a controlled substance prescription"
│   └── 2FA / token input (vendor stub in MVP shows: "EPCS vendor not yet configured")
│   └── Sets epcs_token on prescription record when two-factor completed
│
└── Transmit via cl-transmit-prescription edge function
    └── On acknowledged: publish prescription_sent event
```

### Pharmacy Picker Logic

```
User opens pharmacy picker:
│
├── Is prescription a controlled substance (is_controlled = true)?
│   └── Filter: WHERE is_surescripts_enabled = true AND deleted_at IS NULL AND is_active = true
│
└── Is prescription non-controlled?
    └── Filter: WHERE deleted_at IS NULL AND is_active = true
```

***

## Pattern Library

### Pharmacy Picker (Client Hook)

```typescript theme={null}
// usePrescriptionsByChart — filter active prescriptions
const { data } = await supabase
  .from('cl_prescriptions')
  .select('*')
  .eq('organization_id', organizationId)
  .eq('chart_id', chartId)
  .is('deleted_at', null)
  .order('created_at', { ascending: false });
```

```typescript theme={null}
// usePharmacyList — filtered pharmacy picker
const { data } = await supabase
  .from('cl_pharmacies')
  .select('id, name, ncpdp_id, is_surescripts_enabled, address_line_1, address_line_2, city, state, zip_code, phone, fax')
  .eq('organization_id', organizationId)
  .is('deleted_at', null)
  .eq('is_active', true)
  // For controlled substance prescriptions, add:
  // .eq('is_surescripts_enabled', true)
  .order('name');
```

### Surescripts MVP Stub Pattern

```typescript theme={null}
// src/cores/cl/services/surescripts.ts
// Surescripts integration requires DrFirst/DoseSpot certification.
// This is a stub for MVP UI development.
export async function transmitPrescription(
  prescriptionId: string,
  _payload: SurescriptsPayload,
): Promise<SurescriptsResult> {
  // MVP stub — return mock acknowledgement
  return {
    success: true,
    messageId: `STUB-${Date.now()}-${prescriptionId.substring(0, 8)}`,
    status: 'acknowledged',
  };
}
```

### Edge Function: Publish prescription\_sent

```typescript theme={null}
// Within cl-transmit-prescription/index.ts (after Surescripts ack)
await serviceClient.from('fw_domain_events').insert({
  event_name: 'prescription_sent',
  organization_id: organizationId,
  payload: {
    prescription_id: prescriptionId,
    chart_id: prescription.chart_id,
    organization_id: organizationId,
    prescriber_id: prescription.prescriber_id,
    pharmacy_id: prescription.pharmacy_id,
    medication_name: prescription.medication_name,
    is_controlled: prescription.is_controlled,
    schedule: prescription.dea_schedule,
    sent_at: new Date().toISOString(),
  },
});
```

***

## Security and RLS

### RLS Policy Summary

| Table              | Operation | Policy                                                            | Helper                       |
| ------------------ | --------- | ----------------------------------------------------------------- | ---------------------------- |
| `cl_prescriptions` | SELECT    | `cl_has_org_access_for_epcs(organization_id)`                     | `cl_has_org_access_for_epcs` |
| `cl_prescriptions` | INSERT    | `cl_has_org_access_for_epcs(organization_id)`                     | `cl_has_org_access_for_epcs` |
| `cl_prescriptions` | UPDATE    | USING + WITH CHECK: `cl_has_org_access_for_epcs(organization_id)` | `cl_has_org_access_for_epcs` |
| `cl_prescriptions` | DELETE    | `cl_has_org_access_for_epcs(organization_id)`                     | `cl_has_org_access_for_epcs` |
| `cl_pharmacies`    | SELECT    | `cl_has_org_access_for_epcs(organization_id)`                     | `cl_has_org_access_for_epcs` |
| `cl_pharmacies`    | INSERT    | `cl_has_org_access_for_epcs(organization_id)`                     | `cl_has_org_access_for_epcs` |
| `cl_pharmacies`    | UPDATE    | USING + WITH CHECK: `cl_has_org_access_for_epcs(organization_id)` | `cl_has_org_access_for_epcs` |
| `cl_pharmacies`    | DELETE    | `cl_has_org_access_for_epcs(organization_id)`                     | `cl_has_org_access_for_epcs` |

* `FORCE ROW LEVEL SECURITY` on both tables (PHI)
* All UPDATE policies include both `USING` and `WITH CHECK` (constitution §5.2.4)
* Migration uses idempotent `DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_policies...) THEN CREATE POLICY... END IF; END $$` pattern

### Audit Requirements

* All controlled-substance prescription views/creates/updates must be logged to `pf_audit_logs` (user\_id, timestamp, action, prescription\_id)
* `epcs_token` (TEXT) set on prescription record when EPCS two-factor completed
* Edge function writes audit entry for EPCS transmissions
* PHI: no prescription data in console logs; no PHI in AI prompts or event payloads beyond stable identifiers

***

## Common Mistakes

| Mistake                                                    | Fix                                                                                                  |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Querying `cl_prescriptions` without `deleted_at IS NULL`   | Always add `.is('deleted_at', null)`                                                                 |
| Showing all pharmacies in picker for controlled substances | Filter `is_surescripts_enabled = true` when `is_controlled = true`                                   |
| Calling Surescripts transmit from React client             | Always use `cl-transmit-prescription` edge function                                                  |
| Using `serve()` from deno std in edge function             | Use `Deno.serve()`                                                                                   |
| Importing Supabase in edge function from `esm.sh`          | Use `npm:@supabase/supabase-js@2`                                                                    |
| Missing WITH CHECK on UPDATE policy                        | Both USING and WITH CHECK required (constitution §5.2.4)                                             |
| Using FORCE RLS on service-role client in edge function    | Service role bypasses RLS by design — validate org access in code                                    |
| Adding `site_id` to `cl_pharmacies`                        | Deliberate omission — pharmacy directory is org-scoped, not site-scoped (see Clarifications in spec) |
| Publishing `prescription_sent` from React client           | Event is published by the edge function using service client, not from React code                    |

***

## Pre-Flight Checklist

**Before running the Phase 1 migration:**

* [ ] `prescription_sent` in `KnownEventName` — `src/platform/events/types.ts` ✅ Done
* [ ] 5 CL-06 permission constants in `CL_PERMISSIONS` — `src/platform/permissions/constants.ts` ✅ Done
* [ ] `cl_has_org_access_for_epcs` function exists in DB (CL-06 migration)
* [ ] Migration uses idempotent `CREATE TABLE IF NOT EXISTS` pattern
* [ ] RLS policies use idempotent `DO $$ BEGIN IF NOT EXISTS...` pattern (see follow-up migration)
* [ ] `FORCE ROW LEVEL SECURITY` on both tables
* [ ] UPDATE policies include both USING and WITH CHECK
* [ ] `site_id` on `cl_prescriptions` (nullable); absent from `cl_pharmacies` (documented)
* [ ] `deleted_at` on both tables
* [ ] `custom_fields` on `cl_prescriptions`; `custom_fields` on `cl_pharmacies` (migration includes both)
* [ ] `fw_workflow_events` seed for `prescription_sent` with `ON CONFLICT DO NOTHING`
* [ ] `pf_module_permissions` seed for 5 CL-06 keys with `ON CONFLICT DO NOTHING`

***

## Related Docs

* [CROSS\_CORE\_INTEGRATIONS.md](./CROSS_CORE_INTEGRATIONS.md)
* [API\_CONTRACTS.md](./API_CONTRACTS.md)
* [EVENT\_CONTRACTS.md](./EVENT_CONTRACTS.md)
* [CL-05-medication-management-INTEGRATION.md](./medication-management-integration.md)
* [CL-04-progress-notes-session-documentation-INTEGRATION.md](./progress-notes-session-documentation-integration.md)
