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.
Owner: PM (Practice Management)
Spec: specs/pm/specs/PM-01-EN-01-patient-merge-workflow.md
Companion (UI): specs/pm/specs/PM-01-EN-03-patient-merge-ui-workflow.md
Status: 📋 Specification — implementation pending
Last Updated: 2026-05-03
1. Purpose
PM-01-EN-01 introduces the canonical mechanism by which two pm_patients rows belonging to the same organization are consolidated into a single survivor record. This document captures the cross-core integration contract for that workflow:
- The same-transaction cross-core write into
cl_patient_charts.patient_id (CL).
- The
pm_patient_merged domain event consumed by CL, PM-08, CE-29, and platform identity boundaries.
- The PF-44 audit hook.
- Idempotency, retry, and ordering expectations for downstream consumers.
2. Pattern Selection
| Concern | Pattern (per integration-patterns.md) | Rationale |
|---|
cl_patient_charts.patient_id re-point | Same-transaction write under ADR-002 exception | Referential integrity; CL FK already exists with ON DELETE RESTRICT. No event-eventual-consistency path is acceptable for a chart-to-patient pointer. |
| Cache / projection invalidation in CL, PM-08, CE-29 | Pattern 2 — Event-based (pm_patient_merged) | Loose coupling; consumers idempotently reconcile. |
| PF-44 audit | Direct call to pf_log_audit_event() inside the SECURITY DEFINER function | PF-44 audit is a synchronous, in-transaction obligation. |
| Frontend invocation | PostgREST RPC (pm_merge_patients, pm_undo_patient_merge) | No edge-function wrapper required in v1; SECURITY DEFINER + in-function permission check provide defense-in-depth. |
No other core directly imports PM code. All consumers use @/platform/clinical, @/platform/scheduling, event subscriptions, or the published RPC.
3. Cross-Core Write Inventory (ADR-002 Exception)
The SECURITY DEFINER function pm_merge_patients() performs the following CL-owned write inside the same transaction as the PM updates:
| Table | Column | Operation | Owning Core | Justification |
|---|
cl_patient_charts | patient_id | UPDATE … SET patient_id = survivor WHERE patient_id = merged | CL | ADR-002 — CL→PM FK exists; chart pointer must atomically follow the merge. |
No other CL, FA, GR, RH, HR, or FW table is written by pm_merge_patients(). Downstream side-effects (cache invalidation, claim re-grouping, lead reconciliation) are event-driven.
Drift detection: The migration MUST query information_schema.key_column_usage for every column whose foreign key target is pm_patients(id) and assert the resulting set equals the documented re-point list (PM tables) plus the documented CL exception. Any drift fails the migration. See specs/pm/specs/PM-01-EN-01-patient-merge-workflow.md § Re-pointed columns.
4. Event Contract — pm_patient_merged
- Channel:
pm_events
- Schema version: 1
- Publisher:
pm_merge_patients() (SECURITY DEFINER) on successful commit path.
- Subscribers (planned):
- CL — invalidate cached chart-by-patient lookups for
merged_patient_id.
- PM-08 (Claims) — invalidate any in-memory grouping that keys on
merged_patient_id.
- CE-29 (Lead Conversion) — re-point lead↔patient mapping where applicable.
- PF-71 (Patient Identity Boundary) — refresh identity caches.
- Idempotency key:
merge_log_id (UUID).
- Delivery semantics: at-least-once.
- Ordering: per
survivor_patient_id.
- PHI safety: payload contains UUIDs and
occurred_at only — no demographics, no merge_reason, no chart data.
- No undo event in v1. Consumers MUST treat
pm_patient_merged as best-effort and reconcile from pm_patient_merge_log when rolled_back_at IS NOT NULL. A future pm_patient_merge_undone event may be added; until then, consumers that materialise long-lived projections SHOULD poll pm_patient_merge_log on a low-frequency cadence (≤ once per patient_merge_undo_window_hours).
Full payload schema is registered in EVENT_CONTRACTS.md under PM-01-EN-01 — Patient Merge Events.
5. Audit Contract (PF-44)
Every successful pm_merge_patients() call writes exactly one pf_audit_logs row with:
entity = 'pm_patients'
action = 'merge'
actor_id = p_actor_id
metadata = { survivor_patient_id, merged_patient_id, merge_log_id, organization_id } (no PHI; no merge_reason body)
pm_undo_patient_merge() writes one additional row with action = 'merge_undo' and the same metadata shape plus rolled_back_by.
Both writes occur inside the same transaction as the data mutation.
6. Consumer Obligations
Subscribers of pm_patient_merged MUST:
- Validate
organization_id against their own tenant context before applying any side effect.
- Treat the event as idempotent — multiple deliveries for the same
merge_log_id MUST be no-ops after the first.
- Reconcile against
pm_patient_merge_log (filtering by rolled_back_at IS NULL) when in doubt.
- NOT cache
merge_reason or any derived demographic data.
- NOT block patient operations on event delivery — the source-of-truth is the PM database state at commit.
7. Failure Modes
| Failure | Behavior |
|---|
| Permission check fails (AC-2) | Function raises insufficient_privilege; no rows written; no event published; no audit row. |
| Cross-org / soft-deleted / identical IDs (AC-1) | Function raises typed exception; transaction aborts. |
| Drift in re-point column set | Migration fails (assertion). Production calls cannot proceed against an inconsistent schema. |
| Undo outside window (AC-8) | pm_undo_patient_merge() raises merge_undo_window_expired; no state change. |
| Event publish failure | Transaction is not rolled back (publish is post-commit-safe via fw_workflow_events insert in the same transaction). If fw_workflow_events insert itself fails, the entire merge transaction aborts and no audit/log rows persist. |
8. Open Items
- 42 CFR Part 2 acknowledgement step for SUD-bearing merges — deferred to a future EN; tracked in CL-11 follow-ups.
pm_patient_merge_undone event — deferred. Consumer reconciliation guidance is documented above.
- Bulk merge / auto-merge — explicitly out of scope (safety).
9. References