Skip to main content

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

ConcernPattern (per integration-patterns.md)Rationale
cl_patient_charts.patient_id re-pointSame-transaction write under ADR-002 exceptionReferential 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-29Pattern 2 — Event-based (pm_patient_merged)Loose coupling; consumers idempotently reconcile.
PF-44 auditDirect call to pf_log_audit_event() inside the SECURITY DEFINER functionPF-44 audit is a synchronous, in-transaction obligation.
Frontend invocationPostgREST 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:
TableColumnOperationOwning CoreJustification
cl_patient_chartspatient_idUPDATE … SET patient_id = survivor WHERE patient_id = mergedCLADR-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:
  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

FailureBehavior
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 setMigration 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 failureTransaction 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