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

# Patient Merge Workflow — Integration Notes

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

**Owner:** PM (Practice Management)
**Spec:** [`specs/pm/specs/PM-01-EN-01-patient-merge-workflow.md`](../../../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`](../../../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`](./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:

1. Validate `organization_id` against their own tenant context before applying any side effect.
2. Treat the event as **idempotent** — multiple deliveries for the same `merge_log_id` MUST be no-ops after the first.
3. Reconcile against `pm_patient_merge_log` (filtering by `rolled_back_at IS NULL`) when in doubt.
4. NOT cache `merge_reason` or any derived demographic data.
5. 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

* Spec: [`specs/pm/specs/PM-01-EN-01-patient-merge-workflow.md`](../../../specs/pm/specs/PM-01-EN-01-patient-merge-workflow.md)
* Plan: [`specs/pm/plans/PM-01-EN-01-patient-merge-workflow-PLAN.md`](../../../specs/pm/plans/PM-01-EN-01-patient-merge-workflow-PLAN.md)
* Tasks: [`specs/pm/tasks/PM-01-EN-01-TASKS.md`](../../../specs/pm/tasks/PM-01-EN-01-TASKS.md)
* Context: [`specs/pm/specs/PM-01-EN-01-CONTEXT.md`](../../../specs/pm/specs/PM-01-EN-01-CONTEXT.md)
* Companion (UI): [`specs/pm/specs/PM-01-EN-03-patient-merge-ui-workflow.md`](../../../specs/pm/specs/PM-01-EN-03-patient-merge-ui-workflow.md)
* ADR-002: [`docs/architecture/decisions/ADR-002-cl-pm-cross-core-foreign-keys.md`](../decisions/ADR-002-cl-pm-cross-core-foreign-keys.md)
* Event contracts: [`EVENT_CONTRACTS.md`](./EVENT_CONTRACTS.md) — PM-01-EN-01 Patient Merge Events
* Cross-core overview: [`CROSS_CORE_INTEGRATIONS.md`](./CROSS_CORE_INTEGRATIONS.md)
