AdvancedRx Project Plan · Updated 2026-06-11

Azure structured intake writes

One Apex service shared by Azure and the data-entry screen flow, replacing today's 7,904-line transcription monolith. Phase 3 of the Azure roadmap — the project that lets Azure write the full intake graph (patient + order + prescriptions + fills + line items + notes), instead of staging an image for a human to transcribe.

Active · ready for Phase A — gated on Portal V2 audit Portal V2 ships 2026-06-13 · post-release audit gates Phase A exit + B/D cutover Owner: Kyle Last verified 2026-06-11
4
phases of automation
P0 live · P1 in progress · P2 next · P3 = this plan
~110–130 → ~85
Rx-object flow interviews per 5-Rx intake
keep-the-flow design vs. the absorbed Fill/OrderItem builder (re-cut 2026-06-11) · budget in SOQL / DML / CPU, not interviews
~150 → ~30–40
screen-flow nodes after refactor
load-bearing transcription flow → calls Apex once
7 / A0→E
phases of build
A0 discovery ✓ · V2A Portal-V2 audit (NEW gate) · A may start in parallel

§0Reconciliation with the Azure-side plan (2026-05-27)

The Azure side (advancedrx-document-integration) merged a build-ready plan that reframes all Azure→Salesforce integration. This plan's boundary decisions (consumer, auth, transport, idempotency, error shape, sharing, file handling) are reconciled to it below. The reconciliation itself left the intake-graph contract (§4) unchanged — it is live-SOQL/FieldDefinition-verified (Phase A0, 2026-05-06). (The 2026-06-11 audit revision subsequently revised parts of §4 — see the Audit revision section below.) Where an Azure-side claim or a deep-research recommendation tensions with that verified contract, it is flagged as a conflict/proposal for joint sign-off, not silently overwritten.

Azure-side decisions adopted: consumer is a .NET 8 isolated Azure Function (not a Logic App); one Azure_Integration identity for all Azure writes, authenticating via the existing ADF Integration ECA with OAuth 2.0 Client Credentials (consumer key + secret in Key Vault via Managed Identity; run-as azure-integration; api scope) — the same system ECA already used for ADF reads, separate from the Magic_Link_Bridge impersonation ECA (no JWT/cert for system writes); a two-pattern boundary (simple fax Case + File via standard REST = "brick #1"; the patient/order/Rx graph via this plan's Apex @RestResource); and faxes become a human-transcription Fax Case now, with a future OCR pipeline emitting the same domain JSON.
Deep-research recommendations folded in: idempotency via a uniqueness-indexed ledger object (insert-or-throw = distributed lock; replay returns the stored body); RFC 9457 problem+json error shape with a retryable flag; explicit HTTP status codes 200/400/422/409/429/500/503 on RestContext.response; inherited sharing + Security.stripInaccessible(CREATABLE) on the PHI path; and file bytes uploaded to ContentVersion first (only the Id rides in the JSON — never base64 into the Apex body).
Flagged for Kyle + Azure joint sign-off (proposals, not overwrites): (1) idempotency ledger supersedes Order.Intake_Idempotency_Key__c-as-lock (an Order created mid-transaction can't be the lock) — RESOLVED 2026-06-11: the generic Integration_Idempotency_Ledger__c is adopted and the Order field is dropped (it never existed in the org); (2) HTTP 400→422 for domain/validation rejections (400 reserved for malformed body) — the Azure Function's retry-classification table must match; (3) RFC 9457 problem+json changes the exact wire shape the Function parses; (4) stripInaccessible(CREATABLE) silently drops any contract field the integration user lacks FLS on — verified 2026-05-27 the Azure_Integration permset today grants zero write FLS on Order/OrderItem/Rx/Account/CPA/ Pharmacy_Note (only EhrPractitioner Read), so the Phase C perm-set expansion must be field-complete or the graph breaks silently.
Auth lane — RESOLVED (corrected per Azure PR #191): all Azure writes reuse the existing prod ADF Integration ECA (OAuth 2.0 Client Credentials), kept separate from the Magic_Link_Bridge impersonation ECA (JWT/cert) — two ECAs split by trust profile, not per-workload sprawl. No new app, no JWT/cert for writes. See the Open questions section (Q1). The magic-link docs stay untouched precisely because Magic_Link_Bridge is a separate impersonation ECA, not the system write lane.

ΔAudit revision (2026-06-11)

Two multi-agent, adversarially-verified audits (plan-vs-org, 2026-06-10; plan-vs-B2B-portal, 2026-06-11) were folded into the plan. Material changes, mirrored from plan.md's banner:

Decisions locked by the audit — and what stays open

Decided
Idempotency ledger — generic Integration_Idempotency_Ledger__c (unique-indexed, channel-prefixed keys), inserted BEFORE any graph DML; full DML-outcome matrix incl. Failed-key takeover + UNABLE_TO_LOCK_ROW → retryable 409. Order.Intake_Idempotency_Key__c dropped. Resolves §0 flag #1 / open question #4.
Decided
Absorb the Fill/OrderItem builder — the service creates Fill + OrderItem + PricebookEntry itself (createFillsAndOrderItems, separately callable, list-shaped, field-for-field parity with the flow's contract). New_Prescriptions_Manually_Entered goes Active-but-dormant (the service never sets SF_Manually_Entered__c) and retires at Phase E.
Decided
Draft-Order policy — channel-scoped existence check (Status='Draft' AND SF_Direct_Rx__c=true AND SalesChannel.SalesChannelName='AdvancedRx'); the unattended REST path NEVER auto-attaches (CREATE_NEW-only in v1; existing Draft → 422 DRAFT_ORDER_EXISTS → image-staging for the human 3-way call); the flow path carries the DE user's choice via orderDisposition (ATTACH_EXISTING / MARK_DUPLICATE).
Still open
Four §9 items remain genuinely open — the 400→422 status split and the RFC 9457 problem+json wire shape (both need Azure-side sign-off), the duplicate-Rx detection rule shape, and Sig-mapping ownership. Shown as open in the Open questions section (Q3b / Q3c / Q3f / Q3g).

V2APortal V2 dependency — post-release audit gate

Portal V2 ships 2026-06-13 (Sarah's deploy). Two reported changes — unverifiable until the release lands in ClaudeTest — directly touch claims this plan treats as verified. A NEW build phase, Phase V2A, audits the release before anything cuts over.

Hard gate: Phase A authoring may START in parallel (the service core is mostly V2-independent), but Phase A does not exit, and no Phase B/D cutover proceeds, until the Phase V2A audit lands and §4/§6 of the plan are re-cut where V2 changed facts.
Reported change 1
Existing patients can fill NEW prescriptions on the portal. If that path creates master Rx records — and especially if it sets Rx.SF_Manually_Entered__c=true — it invalidates the "zero portal writers" claim underpinning the absorb/dormancy design (double-creation risk) and changes the Fill/OrderItem portal-consumption contract.
Reported change 2
Paid orders reportedly no longer park at Status='Draft' pending pharmacist counseling — replaced by a credit-card hold (auth-only) so pharmacists can edit orders pre-capture. This changes the Draft cross-capture analysis, the Visible_on_Portal__c/Paid__c visibility reasoning, and the charged-but-no-order failure surface — auth-then-capture has different failure modes than charge-then-convert. The channel-scoped Draft check stays regardless: reships still create Drafts.

Audit checklist (run against ClaudeTest once V2 is deployed in)

Why the portal matters at all: the §10 boundary was corrected 2026-06-11 — "different domain model" is only true in the producing direction. The portal new-order funnel CONSUMES this service's graph: it takes the intake-created Draft Order as input, reads the NV Rxes + Fills, builds CartItems off Fill.Id / Patient_Fill_Id__c, and re-points Fill.Order__c after checkout. Field-for-field parity of the absorbed builder is therefore a Phase A exit gate.

§1The big picture

Same pipeline, four states. Toggle the phase to watch what Azure writes today (Phase 0), what's about to flip on (Phases 1 + 2), and what this plan adds (Phase 3). Every other phase-aware section on the page reacts to the same toggle.

Show pipeline at:
Upstream Azure Salesforce Human LifeFile (web UI) no API — passive UI PAD bot 8 AM daily · UI scrape → JSON Logic Apps · wf-sf-writer 5 managed API connections AI Search · matching patient (P1) · prescriber (P2) Data Factory · ADF Contact roster sync Azure SQL PrescriptionDocuments + roster ContentVersion PDF / PNG bytes Rx_Image__c External_Id_c__c · Image__c Staging Case RT=Data_Input ContactId blank · transcribed by hand + Contact pre-linked when match high + Contact + Prescriber pre-linked closed · ParentId → new-order Case PatientOrderIntakeRest @RestResource · /intake/v1/escript Account → Contact → EhrPatient + ContactPointAddress HC auto-creates Contact Order + Rx records + Notes + Fill + OrderItem + PBE (flow P0–2 · service P3) + new-order Case Data-entry transcription ~all e-scripts · screen flow most e-scripts · pre-link reduces lookup most e-scripts · less lookup · less linking fallback path only · low-confidence + parser-failed Pharmacist · PV1 checkpoint regardless of source parsed JSON match scores sync .NET 8 Fn · structured POST transcribes low-confidence only verifies
External system Azure Salesforce SF record / object Human action Dashed + dimmed = not active in this phase
Phase 0 · live today
What's lit up: LifeFile → PAD → Logic Apps writes ContentVersion + Rx_Image__c + Case (RT=Data_Input). Staging Case lands with ContactId=null. Everything past the staging Case is human transcription. The DE team carries every e-script.
Phase 1 · in progress
What's new: ADF syncs Contact roster into Azure SQL; AI Search scores incoming patients; high-confidence matches arrive with Case.ContactId pre-populated. DE workload shrinks for matched patients (review, don't lookup). Flag: Escript.Evaluator.RequirePatientMatching false → true. SF-side perm-set expansion lands with the Azure flag flip — neither alone is functional.
Phase 2 · next
What's new: same AI-match approach, applied to prescribers. Case.EHR_Practitioner__c already exists and wf-sf-writer already writes to it — currently resolves to null because the flag is off. Phase 2 is a feature-flag flip on the Azure side, not a new SF field addition.
Phase 3 · this plan
What's new: Azure stops posting an image and starts posting a domain JSON to a new Apex @RestResource (PatientOrderIntakeRest). The endpoint owns the full intake graph creation: Account → HC-auto-Contact → EhrPatient → ContactPointAddress → Order → Rx[] → Fill + OrderItem + PricebookEntry (prices computed BEFORE create — absorbed 2026-06-11) → Pharmacy_Notes → new-order Case. The data-entry screen flow gets refactored to call the same service via an @InvocableMethod. One Apex layer, two surfaces. Azure still uploads the image first — stagingRxImageId is REQUIRED on the eScript channel. DE team becomes the fallback path for low-confidence + parser-failed payloads.

§2Phases in depth

Each phase is additive — earlier phases stay live as later phases land. The panel matching your current phase toggle gets the strong border + shadow.

Phase 0

Live today

Active in production

Today's state. Azure scrapes LifeFile and creates the 3-record staging set per e-script. The data-entry team transcribes everything else by hand.

What's running

  • Daily 8 AM wf-start-lifefile-automation kicks the PAD bot
  • PAD bot scrapes LifeFile UI and parses to JSON
  • wf-sf-writer creates ContentVersion + Rx_Image__c + Case (RT=Data_Input) + 2 ContentDocumentLinks
  • Idempotent on External_Id_c__c (Rx_Image) and External_ID__c (Case)
  • DE team transcribes via the 7,904-line screen flow

Feature flags

  • RequirePatientMatching = false
  • RequirePrescriberMatching = false
  • Every Case lands with ContactId=null and EHR_Practitioner__c=null

What's de-risked

  • Image-based ingestion has been running stable; idempotency proven
  • Staging schema is mature
  • Auth lane (the existing ADF Integration ECA — OAuth Client Credentials, run-as azure-integration — already proven by ADF + perm set) operational

Living-with-it hazards

  • Every e-script becomes a DE work item — no automation past staging
  • 12-flow Case collision fires on every Azure-created Case
  • DE workload scales linearly with intake volume
Phase 1

Patient matching

In progress

ADF syncs the SF Contact roster to Azure SQL; AI Search scores incoming patients against the roster; high-confidence matches arrive with Case.ContactId pre-populated. Pure flag flip on the Azure side — paired with a SF perm-set expansion.

What's new

  • ADF outbound Contact sync to Azure SQL
  • AI Search scoring (phone, DOB, name + 1,100+ nicknames)
  • wf-sf-writer calls AI Search per e-script
  • High-confidence matches → Case.ContactId pre-populated on insert
  • Low-confidence → unchanged (Case lands unlinked)

Feature flag flip

  • RequirePatientMatching false → true
  • Confidence threshold tunable Azure-side without an SF deploy

What's de-risked

  • DE keeps human review (advisory match, not authoritative)
  • Threshold safety net via Azure-side tuning
  • Backward compatible: low-confidence stays on the existing path

Cutover gates & hazards

  • Interlock: SF perm-set expansion (Contact Read + View All + FLS) MUST land WITH the flag flip
  • DE workflow change: some Cases pre-linked, some not
  • No SF-side audit trail today — confidence stays in Azure SQL (recommend adding Match_Confidence_Score__c)
Phase 2

Prescriber matching

Next · pure flag flip

Same AI-match approach applied to prescribers. The Case.EHR_Practitioner__c field already exists, wf-sf-writer already writes to it, and it currently resolves to null because the flag is off. Phase 2 is purely a flag flip — not a SF schema change.

What's new

  • AI Search scoring extended to prescriber identifiers
  • High-confidence prescriber matches → Case.EHR_Practitioner__c pre-populated

Feature flag flip

  • RequirePrescriberMatching false → true

What's de-risked

  • SF write surface already in place (verified 2026-04-21 via FieldDefinition)
  • No new SF field additions
  • No new perm-set expansion required for this phase

Cutover gates & hazards

  • Same DE-workflow-change concern as Phase 1
  • Same audit-trail gap (confidence stays in Azure SQL)
Phase 3

Structured intake writes — this plan

Phase A ready · gated on V2A audit

Azure stops POSTing an image and starts POSTing structured JSON to a new Apex @RestResource. The endpoint creates the entire intake graph in one transaction. The data-entry screen flow gets refactored to call the same Apex service via an @InvocableMethod wrapper — one source of truth for the create-graph, two surfaces.

What ships

  • PatientOrderIntakeRest at /services/apexrest/intake/v1/escript
  • PatientOrderIntakeService.intake(IntakeRequest) — savepoint-bounded transaction
  • PatientOrderIntakeInvocable wrapper for the refactored screen flow
  • Integration_Idempotency_Ledger__c — generic, unique-indexed, channel-prefixed keys; inserted BEFORE any graph DML; full DML-outcome matrix (Failed-key takeover; UNABLE_TO_LOCK_ROW → retryable 409). Order.Intake_Idempotency_Key__c dropped decided 2026-06-11
  • Fill + OrderItem + PricebookEntry builder (createFillsAndOrderItems) — absorbed from New_Prescriptions_Manually_Entered; records created with final prices (no post-create update, no VR bypass on insert) decided 2026-06-11
  • Bulk pricing method on B2BProductPricingService — prices from (productId, quantity) pairs BEFORE the Fill/OrderItem insert; defends the divide-by-zero AND the STANDARD-null NPE; existing 6-slot callers untouched
  • Patient-dedupe backstop (422 PATIENT_POSSIBLE_DUPLICATE) + fail-fast FLS (500 FLS_MISCONFIGURED)
  • HTTP status codes 200 / 400 / 422 / 409 / 429 / 500 / 503 on RestContext.response; RFC 9457 problem+json body with retryable proposal · §0 #2/#3

Phase decomposition

  • A0 · Discovery — ✓ complete (8+ contract corrections, 38 VR audit, OrderItem + Fill contracts)
  • V2A · Portal V2 post-release auditNEW 2026-06-11; V2 ships 2026-06-13; gates Phase A exit + B/D cutover (see the Portal V2 section)
  • A · Author the service — may start in parallel with V2A; tests only, no callers wired; does not exit until the V2A audit lands
  • B · Refactor screen flow — ~150 nodes → ~30–40, calls invocable (the New_Prescriptions_and_New_Order_Case refactor was dropped 2026-06-11 — that flow is Draft/inactive)
  • C · Build REST endpoint — independent of B, can run concurrent
  • D · Per-channel cutover — CMDT-gated, e-scripts first, faxes later; hard-gated on V2A
  • E · Cleanup — only after D stable for one release cycle; retires the dormant New_Prescriptions_Manually_Entered (gated on the V2A writer re-check)

What's de-risked

  • A0 validated 8+ contract corrections against live schema and screen flow
  • OrderItem VR-bypass, auto-quarantine override (OwnerId-only), 3-FK integrity all documented; price-before-create removes the VR-bypass ceremony from the intake hot path
  • Idempotency design + structured error vocabulary defined
  • Sandbox test gate before A ships (HC auto-Contact-create under with sharing)
  • Per-channel CMDT routing means rollback is a row flip — no deploy

Risk register highlights

  • Sharing keyword decisioninherited sharing on the REST class + with sharing service + stripInaccessible(CREATABLE); no without sharing on PHI (retired the old escape hatch). HC auto-Contact-create still Phase-A-test-gated; if it fails, widen perms (not flip sharing)
  • OrderItem Prevent_Manual_Edits VR — every UPDATE needs Validation_Bypass_Date_Time__c; creates are bypass-free under price-before-create
  • Auto-quarantine override on new-order Case insert — reassigns OwnerId only, never the RT (corrected 2026-06-11); open intake-created Cases at volume also quarantine patients' next PORTAL orders — track during Phase D widening
  • 3 patient FKs on Rx must agree — no managed integrity
  • Perm-set blast radius + FLS-completeness — auth decided (reuse the ADF Integration ECA, Client Credentials). stripInaccessible(CREATABLE) needs the Phase C perm set field-complete (today: zero write FLS on the graph objects)
  • HC upgrade fragility — pin version in test plan; rerun service tests after each upgrade
  • .NET 8 Azure Function classifies on HTTP status (was Logic Apps Scope_Catch_Failure) — a 200-with-errors body is silently lost; conclusion unchanged by the consumer swap. Uncatchable LimitException arrives as a platform-shaped 500 (not problem+json)
  • Account-lock contention with portal checkout — overlap → 10s lock timeout, with the failure landing portal-side post-charge; do all resolution/pricing BEFORE the first Account-touching DML; map UNABLE_TO_LOCK_ROW → retryable 409 ACCOUNT_LOCK_TIMEOUT
  • Portal hard-deletes intake-created Fills — partial-state logic must treat missing graph records as legitimate, not corrupt replay; intake-created PBEs carry the computed price, never $0 (the refill wizard reads Standard PBE)
  • Governor math re-cut 2026-06-11 — keep-design ~110–130 Rx-object interviews vs. ~85 absorbed; budget in SOQL (100) / DML (150) / CPU (10s), not interviews; pre-decided fallback: pricing moves to a post-commit Queueable (200 + PRICING_PENDING warning)

§3Pipeline anatomy

Every moving part in the pipeline, in roughly upstream-to-downstream order. Each card names what the component does, where its credentials live, and how the integration breaks when the component does. Color stripe matches the §1 diagram (slate=external · blue=Azure · indigo=Salesforce · amber=auth surface · dashed blue=Phase 3 future).

LifeFile

Upstream · external

The web app where prescribers' e-scripts arrive. No API, no webhooks, no push. AdvancedRx's only integration with LifeFile is browser automation against the UI as a logged-in human user.

Auth
lifefile-username + lifefile-password, loaded into the PAD bot from Azure Key Vault
Fails when
UI selectors change upstream → PAD scrape breaks (no API contract to lean on)

Reference: azure-integration.md "Upstream source"

PAD bot

Power Automate Desktop

Daily 8 AM browser-automation bot. Logs into LifeFile, captures each new e-script's image as PNG, and parses UI text into structured JSON for downstream Logic Apps to consume.

Auth
Azure Key Vault → LifeFile creds at run time
Fails when
LifeFile UI changes; bot host machine offline; cred expiry not refreshed

Reference: azure-integration.md "Upstream source"

Azure Logic Apps

Orchestration · 5 SF connectors

Three workflows compose the pipeline: wf-start-lifefile-automation (8 AM cron → triggers PAD) → wf-sf-dispatch (per-document fan-out via Service Bus) → wf-sf-writer (the Salesforce writer with Scope_Catch_Failure). API version pinned in the SF connection config to insulate against vendor drift.

Auth
Managed Identity (Azure side → Key Vault / Service Bus) reads the ADF Integration ECA consumer secret → SF-bound writes use OAuth 2.0 Client Credentials on that ECA (run-as azure-integration, api scope; §0/§5). No JWT/cert (that's the separate Magic_Link_Bridge ECA).
Fails when
SF auth expires / secret rotated without refresh / login-IP blocked / scope reverted; SF returns 4xx → Scope_Catch_Failure → DLQ + SQL SalesforceStatus='Failed'

Reference: azure-integration.md "What Azure creates" + "Failure handling"

Azure SQL · PrescriptionDocuments

State + audit

One row per inbound e-script. Tracks DispatchStatus, SalesforceStatus, AI confidence (PatientMatchScore + PatientMatchReasons + MatchedPatientId), and any partially-written SF record IDs. Stored proc usp_UpdateSalesforceStatus uses COALESCE so partial writes never overwrite real IDs on retry.

Auth
Logic Apps managed connection
Fails when
Row stuck at Failed + Publishedwf-sf-dispatch won't auto-retry; manual replay only

Reference: azure-integration.md "Failure handling"

Azure AI Search

Phase 1 + 2 matching engine

Scores incoming patient identifiers (Phase 1) and prescriber identifiers (Phase 2) against the Contact roster ADF synced into Azure SQL. Signals: phone, DOB, name similarity (phonetic + 1,100+ nickname pairs). Threshold tunable Azure-side without an SF deploy.

Auth
Internal Azure managed connection
Fails when
Roster stale (ADF lag); threshold tuned permissive → false-positive matches reach PV1; threshold tuned strict → DE workload doesn't drop

Reference: azure-integration.md "Outbound · Data Factory Contact sync"

Azure Data Factory · ADF

Outbound Contact sync

Pulls SF Contact/prescriber roster → Azure SQL on a scheduled cadence. Feeds AI Search. Outbound READ — authenticates through the ADF Integration ECA (OAuth 2.0 Client Credentials, run-as azure-integration, api scope), the same system ECA the new Azure writes reuse. One ECA, one auth primitive for ADF reads + all writes — no writes-vs-reads split. (The classic Azure Logic Apps + Data Factory Connected Apps still exist in the org but are legacy — see the flag in the auth section.)

Auth
Linked service LS_Salesforce_ProductionADF Integration ECA, OAuth 2.0 Client Credentials, scope api
Fails when
ECA consumer secret rotated without ADF refresh; perm set narrowed below ADF's SELECT field list

Reference: azure-integration.md "Outbound" + "Why one Connected App"

ECA · ADF Integration (system data lane)

SF auth surface · system

The SF-side auth surface for all Azure system data — the outbound ADF reads and all Azure writes (fax Case + File now, structured intake later). External Client App + OAuth 2.0 Client Credentials (run-as azure-integration, api scope; do not revert to full — deprecated Winter '26). Already proven by ADF, so the fax writes reuse it as-is. Kept separate from the Magic_Link_Bridge ECA (patient impersonation, JWT/cert): two ECAs split by trust profile. (Slated to be renamed broad — e.g. Azure Integration — since it now covers all system writes, not just ADF reads.)

Creds
Consumer key + secret in Azure Key Vault, read at runtime via the Function's Managed Identity (no signing cert — Client Credentials). KV expiry-alert wiring needs verification before prod cutover. ECA enforces login-IP restrictions (allow-list the Function's NAT egress).
Sandbox
Sandbox replica created via metadata deploy 2026-05-27 (runbook: docs/runbooks/adf-integration-eca-setup.md) and proven end-to-end via the closed-loop fax smoke test 2026-05-28. Prod ECA verified 2026-05-28 — config matches the runbook 1:1. After every sandbox refresh the integration user gets frozen and must be unfrozen.

Reference: plan.md §0 + §5 · Azure plan PR #191

User · azure-integration@advancedrx.net

Run-As identity

Single Run-As user covers both inbound + outbound. Salesforce Integration User License (no UI login). The CreatedById on every Azure-written record is this user — the strongest writer-origin signal when debugging "where did this record come from?"

Auth
Passwordless · OAuth 2.0 Client Credentials via the ADF Integration ECA — same primitive for both ADF reads and Azure writes
Fails when
Deactivation; license drop; profile reassignment; user-Id-based VR bypasses authored against this Id

Reference: azure-integration.md "Auth chain"

Profile + Perm Set · least-privilege scope

Capability layer

Profile Minimum Access - Azure Integration is the least-privilege base (CLAUDE.md §3.14 exemplar — the one AdvancedRx-controlled integration that follows the convention cleanly). Perm set Azure_Integration layers capabilities. Each phase expands what a compromised credential can reach — Phase 1 added Contact Read; Phase 3 will add full intake-graph CRUD.

Maintenance
Layer new caps via the perm set; do not bulk-edit the profile
Auth (decided)
Reuse the ADF Integration ECA (OAuth Client Credentials), separate from Magic_Link_Bridge. The remaining work is config + the Phase C perm set being field-complete (stripInaccessible).

Reference: plan.md §6 risk #6 + azure-integration.md "Why one user"

PatientOrderIntakeRest

Phase 3 · planned

The new Apex @RestResource (inherited sharing) the .NET 8 Azure Function will POST to in Phase 3. URL: /services/apexrest/intake/v1/escript. Maps Azure JSON → IntakeRequestIntegration_Idempotency_Ledger__c lock (DML-outcome matrix) → PatientOrderIntakeService.intake(), which also creates Fill + OrderItem + PricebookEntry directly with prices computed BEFORE insert (absorbed 2026-06-11) → returns the result with the status code set on RestContext.response. The Function classifies retry/dead-letter off the status code (200-with-errors won't work — see §9 hazards).

Status
200 success / completed-replay · 400 malformed · 422 domain-invalid · 409 idempotency in-flight/conflict · 429 governor · 500 internal · 503 downstream proposal · §0 #2/#3
Auth
ADF Integration ECA, OAuth 2.0 Client Credentials (run-as azure-integration); perm set extended field-complete for the intake graph (required by stripInaccessible)

Reference: plan.md §3 (target architecture) + §0 reconciliation

§4What gets written where

Fourteen cards. Each names an SObject (or group) the pipeline touches, who creates it, what fields are populated, and the per-record hazard worth remembering. Phase 3 cards dim until you flip the toggle to P3. Hover a dimmed card to read it without flipping.

Created by Azure today P0+ · every phase

ContentVersion + ContentDocumentLink ×2

standard SF

Created by: wf-sf-writer · Trigger: every inbound e-script

  • ContentVersionVersionData (the PDF / PNG bytes), PathOnClient, Title
  • ContentDocumentLink #1 — links the file to Rx_Image__c
  • ContentDocumentLink #2 — links the same file to Case
Note: both links point at the same ContentVersion — that's how SOQL "Cases linked to a specific image" triangulates through ContentDocumentLink.

Rx_Image__c

staging record

Created by: wf-sf-writer · Idempotent on: External ID (upsert)

  • External_Id_c__c — Rx External ID; API name is typo'd (_c__c suffix). Idempotent upsert key.
  • Image__c — HTML rich-text (32k limit) with embedded <img> reference
  • Phase 3 finalize: service writes Contact__c + EHR_Practitioner__c after the intake graph lands
Don't autocorrect the External_Id_c__c API name — the typo is preserved deliberately. Renaming breaks every reference.

Staging Case

RT=Data_Input

Created by: wf-sf-writer · Idempotent on: External_ID__c (clean API name)

  • Subject = "LastName, FirstName (DOB: MM/DD/YY)" · same-day repeats prefix "Multiple RX"
  • External_ID__c — Rx External ID, case-insensitive unique
  • RecordType.DeveloperName = Data_Input
  • P0: ContactId = null · EHR_Practitioner__c = null
  • P1: + ContactId pre-populated when patient match high
  • P2: + ContactId + EHR_Practitioner__c both pre-populated when match high
  • P3 finalize: ParentId → new-order Case · Status='Closed' · Order__c set · OwnerId untouched
12-flow Case collision fires on every insert. Worst object-trigger collision in the org. Real per-update tax is 2 flows (the canonical Same/Related pair); the other 10 are gated.

Phase 3 — created by the Apex service P3 · PatientOrderIntakeService

Account

RT=Individual

Created by: resolvePatient() · Skipped: existing-patient reuse path

  • RecordType.DeveloperName = IndustriesIndividual (label is "Individual" — they differ; .get('Individual') returns null. Never hardcode the ID)
  • Name = "<firstName> [<middleName> ]<lastName>"corrected 2026-06-11: earlier drafts said Last, First, which would name every new patient backwards vs. the flow-created base
  • HealthCloudGA__IndividualType__c = 'Group'
  • HealthCloudGA__EnrollmentType__c = 'NonDual'
  • SF_Manually_Entered__c = true
Inserting this triggers HC managed-package code that creates a paired Contact automatically. The HC picklist values are load-bearing for that auto-create — don't drift them. Dedupe backstop (added 2026-06-11): when no existingContactId is supplied, resolvePatient() runs an exact First+Last+DOB (and/or normalized-phone) match — on hit, reject 422 PATIENT_POSSIBLE_DUPLICATE (route to image-staging for human adjudication), never blind-create a duplicate triple. DUPLICATES_DETECTED DML faults map to the same 422.

Contact

RT=Patient · auto-created

Created by: Health Cloud package (server-side, on Account insert) · Updated by: service post-create

  • Service writes after the auto-create:
  • FirstName, MiddleName, LastName, Patient_Memo__c
  • HealthCloudGA__Gender__c, Email, Birthdate, SF_Manually_Entered__c = true
  • Phone fields (Phone, MobilePhone) — formatted to exactly 10 digits or rejected
  • Existing-patient reuse path still updates Patient_Memo__c when the payload supplies it — never other demographics (rev 2026-06-11)
7 active phone-format VRs. The patient path writes Phone + MobilePhone (the Primary_/Secondary_Phone rules target prescriber fields). No SecondaryPhone__c field exists. Service uses a PhoneFormatter helper. Don't insert Contact yourself — Health Cloud does it.

HealthCloudGA__EhrPatient__c

third leg of patient triangle

Created by: service · Anchors: Account + Contact + EhrPatient

  • HealthCloudGA__Account__c = Account.Id
  • Contact__c = the auto-created Contact.Id (back-link)
  • HealthCloudGA__BirthDate__c, HealthCloudGA__GenderLabel__c
  • HealthCloudGA__GivenName1__c = First name
  • HealthCloudGA__GivenName2__c = Middle name
  • HealthCloudGA__GivenName3__c = Last name
Non-obvious mapping: GivenName1 = First, 2 = Middle, 3 = Last. Not sequential by position — by role.

ContactPointAddress

standard SF

Created by: service · Verified by: ProvenWorks (post-insert)

  • Name = 'Default' · Primary__c = true · Status__c = 'Active'
  • Street, City, State, Country, PostalCode — from payload
  • ParentId = Account.Id (Master-Detail to Account, NOT Contact)
  • Contact__c = Contact.Id (custom Lookup)
  • Verification_Status__c / Unverified_Reason__c — when address-validation gating produced a result
Master-Detail is to Account, not Contact — common assumption error. The Contact__c custom Lookup tracks which Contact the address belongs to, but the parent record is the Account.

Order

Status=Draft

Created by: service · Draft check: channel-scoped; the unattended REST path NEVER auto-attaches (rev 2026-06-11)

  • AccountId, BillToContactId, ShipToContactId, Customer_Contact__c — all → resolved Contact
  • EffectiveDate, Transaction_Date__c = today
  • Rx_Image__c — lookup to staging image
  • SF_Direct_Rx__c = true · Skip_Quarantine_Check__c = true
  • SalesChannelId = lookup WHERE SalesChannelName='AdvancedRx'
  • Intake_Idempotency_Key__cRESOLVED 2026-06-11: dropped. The field never existed in the org; Integration_Idempotency_Ledger__c is both the lock and the queryable trace (Order__c pointer on the ledger row). Do not create it decided
Draft-Order policy (rewritten 2026-06-11): the Draft existence check is channel-scoped — Status='Draft' AND SF_Direct_Rx__c=true AND SalesChannel.SalesChannelName='AdvancedRx' — because paid portal orders park at Draft for pharmacist counseling and reships create Drafts (a channel-blind query cross-captures them). The unattended REST path never auto-attaches: existing Draft → 422 DRAFT_ORDER_EXISTS → image-staging for the human 3-way call. The flow path passes the DE user's choice via orderDisposition. Case attach is Order__c-keyed (a consequence of Order reuse), NOT a same-day rule.

HealthCloudGA__EhrMedicationPrescription__c · Master Rx

RT=Needs_Verification · up to 5

Created by: service · 17 fields per Rx · prescriber resolved per prescription (rev 2026-06-11)

  • RecordType.DeveloperName = Needs_Verification (NOT Prescription) — pre-PV1 RT; the downstream special-counseling flow gates its entry criteria on it
  • Three patient FKs (must match): HealthCloudGA__Patient__c · HealthCloudGA__Account__c · Contact__c
  • Prescriber: HealthCloudGA__Prescriber__c + denormalized Prescriber_ID__c (Text 255) — resolved PER Rx (each slot can carry its own prescriber; defaults to the request-level block); any NPI miss → PRESCRIBER_NOT_IN_ROSTER naming the failing Rx index
  • Order: Order__c
  • Image: Rx_Image__c
  • Drug data: Product__c, Quantity__c, RefillsAuthorized__c, HealthCloudGA__DateWritten__c
  • Sig__c = sigId from payload — Lookup(Sig), NOT free text (corrected 2026-06-11); on miss the service rejects 422 SIG_NOT_FOUND, never a silent null-Sig insert
  • Date_filled__cnote typo'd lowercase 'd'
  • Price__c = from computePrices() — set at create (rev 2026-06-11; pricing runs before insert, no post-insert update pass)
  • SF_Manually_Entered__cNOT set by the service (removed from the contract 2026-06-11): the service creates Fill + OrderItem itself; setting the flag while New_Prescriptions_Manually_Entered is still Active would double-create
3-FK integrity hazard: the three patient-side relationships MUST point at the same patient resolution result. No managed integrity helps you here — drift is silently inconsistent.

New-order Case

Owner may auto-quarantine

Created by: service · OwnerId: CS_Case queue (or admin if quarantined)

  • RecordType.DeveloperName = Customer_Service_New_Order — the RT is never rewritten by the quarantine override (corrected 2026-06-11; the flow XML disproves the old RT-rewrite claim)
  • ContactId, AccountId, Order__c, EHR_Practitioner__c
  • Status='New' · Subject='New Order' · HealthCloudGA__DueDate__c=today
  • New_Patient__c · New_Prescription_Order_Status__c='Contact Patient'
  • OwnerId = lookup Group WHERE DeveloperName='CS_Case' AND Type='Queue'
Auto-quarantine override (corrected 2026-06-11): if the patient has any open Case with RT=Quarantined_Order or Refill_Request_Case, the canonical Case_Insert_Update_Same_Record BeforeSave silently reassigns OwnerId only — it never rewrites the RT. Gated on ISNEW=true. Service post-queries the Case and returns actualCaseOwnerType + wasQuarantined (owner-based detection, not RT) plus an AUTO_QUARANTINE_APPLIED warning.

Pharmacy_Note__c ×3

RT=Customer_Service

Created by: service · up to 3 per intake

  • 'Active Portal Cart' on new-order Case — when patient has an active WebCart with Rx CartItems
  • 'Note from Data Input' on new-order Case — free-form note from payload (or DE-team input via refactored screen flow)
  • 'Duplicate Order' on staging Case — when intake detects this is a duplicate of an existing Order
All three use RecordType.DeveloperName='Customer_Service' looked up at runtime — never hardcoded. The Pharmacy_Note__c object has only 2 RTs (Customer_Service + Pharmacist); these CS-bound notes correctly route to the CS RT.

Integration_Idempotency_Ledger__c

NEW object · the distributed lock

Created by: the REST endpoint, BEFORE any graph DML · Decided: 2026-06-11 (resolves §0 flag #1 / open question #4)

  • Idempotency_Key__c — Text, External ID, Unique, case-insensitive; channel-prefixed (escript:<key>) — the uniqueness index IS the lock; Azure derives the key deterministically from the e-script
  • Channel__c — picklist (eScript, fax, portal_checkout, …) — generic by design; wiring other consumers is out of scope
  • Status__cInFlightCompleted / Failed (flips inside the same transaction)
  • Request_Hash__c — detects same-key, different-payload (409 IDEMPOTENCY_REPLAY_CONFLICT)
  • Response_Body__c — stored IntakeResponse JSON; a completed-key replay returns it verbatim (200)
  • Order__c + record-id pointers — trace + partial-state detection
DML-outcome matrix, not an "if InFlight" branch: a committed row can never be observed as InFlight — a concurrent duplicate experiences a row-lock wait. Insert success = lock held; DUPLICATE_VALUE → query the committed row (true replay / 409 conflict / Failed-key takeover); UNABLE_TO_LOCK_ROW → retryable 409 IDEMPOTENCY_IN_FLIGHT. Ordering: ledger insert → savepoint → graph. LimitException is uncatchable — the ledger vanishes with the transaction and the replay re-runs clean. Missing graph records ≠ corrupt replay (the portal hard-deletes Fills).

Phase 3 — Fill + OrderItem + PricebookEntry, created BY THE SERVICE absorbed 2026-06-11 · createFillsAndOrderItems()

Fill (HealthCloudGA__EhrMedicationPrescription__c RT=Fill)

service-created (absorbed)

Created by: the service's createFillsAndOrderItems() builder — field-for-field parity with the flow's Create_Fill contract (a Phase A exit gate)

  • Fields cached from Master Rx: patient FKs · prescriber · product · sig · quantity · strength · dosage form · days supply
  • NL_Prescription__cFill → Master Rx parent FK
  • Price__c / Calculated_Price__c — from computePrices(), set AT create
  • Patient_Fill_Id__c — stamped by the canonical Rx AfterSave pair (HashUtils); the builder re-queries Fills post-insert to populate IntakeResponse.fillIds[]
  • Date_filled__c (typo'd) · Order__c · Prescription_Number__c · RefillsRemaining__c initialization
NL_Prescription__c name is misleading. Sounds like legacy NewLeaf, but it's the active Fill→Master FK in live use. Don't drop based on naming. Portal-load-bearing: the portal new-order funnel builds CartItems off Fill.Id / Patient_Fill_Id__c and re-points Fill.Order__c after checkout — and the portal hard-deletes intake-created Fills on cart-item removal.

OrderItem + PricebookEntry

service-created · final prices at insert

Created by: the service builder, with prices from computePrices() — a new bulk method on B2BProductPricingService, run BEFORE the insert

  • OrderId · Product2Id · PricebookEntryId · Quantity
  • Created with final prices: ListPrice, UnitPrice=1.0, TotalLineAmount — no post-create update pass, no transient $0 line-item window
  • Prescription_Link__c = Fill record (custom FK)
  • PricebookEntry find-or-create — duplicate-safe across bulk inserts (dedupe same-product rows; catch DUPLICATE_VALUE); created at the computed price, never $0; Standard Price Book by Name lookup, NOT hardcoded ID
Validation_Bypass_Date_Time__c on UPDATES only. Price-before-create removes the Prevent_Manual_Edits VR-bypass ceremony from the intake hot path — no bypass needed at insert. Any LATER OrderItem update must still set Validation_Bypass_Date_Time__c = Datetime.now() or the VR rejects it.
Dormancy + retirement: the service does NOT set SF_Manually_Entered__c, so New_Prescriptions_Manually_Entered stays Active-but-dormant through the parallel run — its Create-only entry criteria mean zero interviews once the screen flow stops being the writer (Phase B) — and retires at Phase E after one stable release cycle, gated on the Phase V2A re-check that Portal V2's fill-new-Rx-on-portal feature introduced no new writer of the flag.

§5Auth chain

Both Azure workloads — inbound writes and outbound ADF Contact reads — run as one Azure_Integration identity, one profile, one perm set, and one auth primitive: OAuth 2.0 Client Credentials via the existing ADF Integration ECA (consumer key + secret in Key Vault read via Managed Identity; api scope). The fax/system writes reuse the same ECA ADF already uses — no writes-vs-reads split. Corrected per Azure PR #191: this supersedes the earlier "ECA + JWT-Bearer / magic-link cert" framing — JWT/cert belongs to the separate Magic_Link_Bridge impersonation ECA, not the system write lane. The chain below is the inbound write path; the outbound ADF delta (same ECA) is noted at the bottom.

Step 1 · Upstream 01

LifeFile (web UI)

No auth surface for AdvancedRx — passive UI we scrape, not an integration partner.

Carries
The e-script document itself — viewed in a browser, no API surface.
Creds
None on this surface. Auth happens at the next step (PAD logs in as a human user).
Rotation
n/a — not a system AdvancedRx operates.
Compromise
If LifeFile creds leak: attacker can read prescriptions but can't write to SF (different auth domain).
Step 2 · UI scrape 02

PAD bot

Logs into LifeFile each morning as a human user; captures images, parses JSON.

Carries
LifeFile session cookie for the duration of the scrape.
Creds
lifefile-username + lifefile-password in Azure Key Vault, fetched at run time.
Rotation
Rotate the LifeFile credentials in Key Vault; PAD picks up the new values on the next 8 AM run.
Compromise
Equivalent to LifeFile creds compromise — read-only e-scripts, no SF write capability.
Step 3 · Orchestration 03

Azure Logic Apps

The orchestrator. Holds nothing sensitive locally — auth happens at the next hop.

Carries
Parsed JSON + image bytes for each e-script.
Creds
Managed Identity on the Azure side (no stored creds for Azure resources; also reads the ADF Integration ECA consumer secret from Key Vault). SF-bound writes use OAuth 2.0 Client Credentials on that ECA.
Rotation
Managed Identity is auto-rotated by Azure. ECA consumer-secret rotation (Key Vault) handled at step 4.
Compromise
Managed Identity scoped to the Logic App's resource group; not directly exfiltrable. SF-bound auth gated by step 4.
Step 4 · ECA · Client Credentials 04

External Client App (ECA) · ADF Integration

The trust contract for the Azure → SF system data lane (ADF reads + all writes). The existing prod ECA Marissa set up for ADF — reused as-is. Separate from the Magic_Link_Bridge impersonation ECA (JWT/cert).

Carries
A short-lived access token issued for the Run-As user via the Client Credentials grant.
Flow
OAuth 2.0 Client Credentials: the Function presents the ECA consumer key + secret to the SF token endpoint; SF issues a token as azure-integration. Scope = api (NOT full — deprecated Winter '26). No JWT/cert.
Creds
Consumer key + secret in Azure Key Vault, read at runtime via Managed Identity. KV expiry-alert wiring needs verification before prod cutover. ECA enforces login-IP restrictions (allow-list the Function's NAT egress).
Rotation
Rotate the ECA consumer secret in Key Vault (refresh both the fax/write Function config AND ADF's linked service — same secret). Runbook: .claude/runbooks/task-13-secret-rotation.md.
Compromise
Anyone with the consumer secret can mint tokens as the Run-As user → step 5 capability set.
Step 5 · Run-As identity 05

azure-integration@advancedrx.net

One user account fronts every Azure workload — the strongest writer-origin signal.

Carries
The user identity tokens are issued for. CreatedById on every Azure-written record.
License
Salesforce Integration User License (no UI login).
Rotation
n/a (passwordless). Deactivation = full integration outage; treat as a load-bearing user.
Compromise
If the ECA consumer secret leaks (step 4), an attacker can act as this user — capability scoped at steps 6 + 7.
Step 6 · Profile 06

Minimum Access - Azure Integration

The least-privilege base. Locked-down by default; capabilities layered on top via the perm set.

Carries
Least-privilege base. Cloned from standard Minimum Access; permissions added back deliberately.
Reference
The CLAUDE.md §3.14 exemplar — the one AdvancedRx-controlled integration that follows least-privilege convention cleanly.
Maintenance
Do NOT bulk-edit the profile to grant new capabilities — layer them onto the perm set instead (step 7).
Compromise
Profile-level perms are intentionally narrow; the real capability surface lives in the perm set.
Step 7 · Permission set 07

Azure_Integration

Where capability actually lives. Each phase expands the scope of what gets reached.

Today
ContentVersion + Rx_Image__c + Case CRUD (inbound writes).
Phase 1
+ Contact Read + View All (ADF outbound). Lands together with the Azure-side RequirePatientMatching flag flip — neither is functional alone.
Phase 3
+ full intake-graph CRUD (Account, Contact, EhrPatient, ContactPointAddress, Order, OrderItem, PricebookEntry, Rx + Fill, Pharmacy_Note__c, the idempotency ledger, Case + Rx_Image updates). Must be field-complete — stripInaccessible(CREATABLE) drops any ungranted field (today: zero write FLS on the graph objects). Auth decided: the existing ADF Integration ECA (Client Credentials), §10 Q1. Fail-fast upgrade (2026-06-11): when the strip removes a mandatory contract field, the service fails with non-retryable 500 FLS_MISCONFIGURED — prod permset drift becomes loud, not silent.
Compromise
Each phase widens what a compromised credential reaches. Document scope at each rollout.
Step 8 · API surface 08

Salesforce REST API

What the integration user can actually do. Phase 3 adds the new @RestResource; the Azure-side consumer + auth lane change (see §0).

Today
Standard REST: insert ContentVersion + Rx_Image__c + Case + ContentDocumentLink. Idempotent upserts on External ID fields (External_Id_c__c on Rx_Image, External_ID__c on Case).
Phase 3
+ POST /services/apexrest/intake/v1/escriptPatientOrderIntakeRest, called by the .NET 8 Azure Function over the ADF Integration ECA (OAuth 2.0 Client Credentials, run-as azure-integration) — see §0.
Errors
4xx + 5xx drive retry/dead-letter classification — today's staging path via Logic Apps Scope_Catch_Failure, the Phase 3 endpoint via the .NET 8 Function. 200-with-errors looks like success to either — see §9 hazards.
Outbound delta (ADF Contact sync) — no auth delta: the outbound ADF reads use the same ADF Integration ECA, the same Client Credentials flow, the same Run-As user / profile / perm set as the writes. ADF read vs. system write is the only difference; one credential covers both, so a single ECA-secret rotation refreshes both. Flag for Kyle: the org still contains the legacy classic Connected Apps Azure Logic Apps (0H4aZ00000013OrSAI) and Data Factory (0H44W000000M5hgSAC) — per Kyle the real auth is the ADF Integration ECA, NOT these. azure-integration.md still describes an "Azure Logic Apps Connected App" auth surface and is stale on this point — recommend a separate doc-cleanup PR (out of scope here).

§6Feature flags

Three flags govern the rollout — two on the Azure side, one new on the Salesforce side that lands in Phase D. Phases 1 and 2 are flag flips, not feature builds; the SF write surface for both is already in place.

Flag Lives in Today (P0) After flip What flipping it does
Escript.Evaluator.RequirePatientMatching
Phase 1 trigger
Azure (wf-sf-writer evaluator config) false — every Case lands with ContactId=null, DE matches by hand true — high-confidence matches arrive with ContactId pre-populated Azure AI Search starts scoring incoming patients against the ADF-synced Contact roster. Threshold tunable Azure-side without an SF deploy.
Escript.Evaluator.RequirePrescriberMatching
Phase 2 trigger
Azure (wf-sf-writer evaluator config) false — outcome = "Ignored" → EHR_Practitioner__c resolves null even though wf-sf-writer writes the field true — high-confidence matches set Case.EHR_Practitioner__c Same AI-match approach, applied to prescribers. Field already exists; wf-sf-writer already writes it. Phase 2 is a flag flip, not a SF field addition.
Intake_Pipeline_Routing__mdt
Phase D channel cutover
Salesforce (CMDT) — added in Phase D n/a — doesn't exist yet per-channel boolean — e.g. eScript=true, fax=false Per-channel + per-confidence-band routing. true = Azure POSTs structured JSON to PatientOrderIntakeRest; false = falls back to today's image-staging path. Rollback = flip a row, no deploy.
Phase 1 SF + Azure interlock: the RequirePatientMatching flip on the Azure side AND the Azure_Integration perm-set expansion (Contact Read + View All + FLS on the synced fields) on the SF side must land together. Either alone leaves the pipeline broken intermediate — Azure asking for Contact data the integration user can't read, or SF granting access nothing's reading. The azure-integration@advancedrx.net user is already on the perm set for inbound writes; Phase 1 widens its scope, no new perm-set assignment needed.

§7The transcription handoff

14 flows reference Rx_Image__c in some way. The load-bearing one is the 7,904-line screen flow the data-entry team launches off the staging Case to produce structured records — Phase B refactors that flow to call PatientOrderIntakeService via an @InvocableMethod wrapper, preserving all the screens but moving orchestration into Apex. Filter to scope the table.

Flow Type Status Role in the cluster
New_Patient_New_Order_New_Rx_New_Case_Launch_from_data_case Screen Active The canonical transcription path. DE team launches from the staging Case; produces Account → HC-auto-Contact → EhrPatient → Order → Rx records → Pharmacy_Notes → new-order Case. ~7,904 lines / ~150 nodes today; Phase B refactors to ~30–40 nodes calling PatientOrderIntakeInvocable.
New_Prescriptions_and_New_Order_Case Screen Draft · inactive Alternative hand-entry path — Draft, doesn't run today, and builds an incompatible graph (attach-to-existing-Order, no patient/staging/Case creation, max 3 Rxes). Its Phase B refactor was dropped 2026-06-11; reviving it would need a separate attach-to-existing-Order service mode.
E_Script_Case_Creation Record-triggered Active Creates a paired Case for a newly-staged Rx_Image. May complement or supersede part of Azure's case-creation behavior — verify before extending.
Image_Uploaded_to_Existing_Order Record-triggered Active Manual-upload path — when an image is uploaded against an existing Order (rather than via Azure ingest), this flow links the image into the right Order context.
Rx_Image_Annotation_Button Screen Active Pharmacist / DE-team rich-text annotation of the image. Mutates Image__c; preserve the inline <img> reference.
Pharmacist_PV1_Rx_Image_Annotation_Button Screen Active PV1-time pharmacist annotation. Same surface as above; pharmacist-action context.
Fax_Clarification_Flow Screen Active Fax-path intake (distinct from e-script) — reads Rx_Image__c for fax-clarification context.
New_Order_Flow Screen / Autolaunched Active Order-side automation that reads Rx_Image__c for context.
Order_Insert_Update_Same_Record Record-triggered (Order) Active Order canonical BeforeSave — reads Rx_Image__c for source-image lookup. Doesn't mutate Rx_Image directly.
Order_Insert_Update_Related_Records Record-triggered (Order) Active Order canonical AfterSave — also reads Rx_Image__c for downstream linkage.
Automation_on_New_Refills Record-triggered (Rx) Active Refill automation — reads Rx_Image for source-image audit trail.
Advanced_Rx_Prescription_Insert_Update_Same_Record Record-triggered (Rx) Active Rx canonical BeforeSave — reads Rx_Image for cross-record validation.
FL_Loop_in_an_order_Final_Verification_view Screen Active Final-verification screen flow — surfaces Rx_Image for pharmacist review at PV checkpoint.
Data_New_Formula_or_Sig Screen Active Misc data-entry helper that reads Rx_Image when the user is creating a new Formula or Sig in context.
Update_Prescription_Image_via_Rx_Image Record-triggered Inactive Predecessor of Image_Uploaded_to_Existing_Order. Inactive but on disk — don't extend.
Update_Prescription_image_via_Rx_Image_preocess Process Builder (typo'd preocess) Inactive Process-Builder sibling of the row above. Inactive. Migration candidate; don't extend.
No flows match the current filter.

No canonical Same/Related pair exists on Rx_Image__c. Both record-triggered flows are functionally-named, not Rx_Image_Insert_Update_*. New record-level automation should consider introducing the canonical pair shape per .claude/rules/flow.md.

§8One e-script's lifecycle

Walk one inbound e-script through the pipeline, step by step. The stepper is phase-aware — switch the toggle (top right) to see how each step changes across phases. Steps marked "P3" are introduced in Phase 3 and dim out in earlier phases.

Step 1 of 9

An e-script arrives at LifeFile

Upstream LifeFile

A doctor sends a prescription to their pharmacy via LifeFile. The document arrives in the doctor's LifeFile UI for the pharmacy to retrieve.

AdvancedRx has no integration with LifeFile — no API, no webhooks, no email or fax bridge. The only way to discover documents is to log in as a human user and read them off the screen.

Upstream UI dependency: if LifeFile changes the e-script page layout, the scrape breaks. There's no API contract to fall back on.
Step 2 of 9

The PAD bot scrapes LifeFile

Azure wf-start-lifefile-automation PAD bot

At 8 AM daily, Logic App wf-start-lifefile-automation kicks off the Power Automate Desktop bot.

The bot pulls lifefile-username + lifefile-password from Azure Key Vault, logs in as a human user, navigates the UI to the new e-scripts queue, and for each document: captures the image as PNG, parses the surrounding UI text into structured JSON.

The bot hands the parsed document set off to wf-sf-dispatch for fan-out to the writer.

Step 3 of 9

Logic Apps writes the 5-record staging set

Azure wf-sf-dispatch wf-sf-writer Salesforce ContentVersion Rx_Image__c ContentDocumentLink ×2 Case

wf-sf-dispatch queues one Service Bus message per document. wf-sf-writer consumes each and writes the staging set to Salesforce in one logical sequence:

  • Insert ContentVersion — the file bytes (PDF or PNG)
  • Insert Rx_Image__c — staging record with rich-text Image__c + idempotent External_Id_c__c
  • Insert ContentDocumentLink #1 — links the file to the staging record
  • Insert Case with Subject = "LastName, FirstName (DOB: MM/DD/YY)" · same-day repeats prefix "Multiple RX"
  • Insert ContentDocumentLink #2 — links the same file to the Case (so it shows on the Case Files panel)

Idempotent on the External ID — if Azure replays the same document later, both Rx_Image__c and Case upsert instead of duplicating.

12-flow Case collision: the moment the Case inserts, all 12 active Case-triggered flows evaluate. Real per-update cost is the 2 canonical flows (Same / Related); the other 10 are gated.
Step 4 of 9

Azure AI matches incoming patient (and prescriber)

P1 in progress · P2 next
Azure AI Search Azure SQL · Contact roster Data Factory
Phase 0: this step doesn't run. Escript.Evaluator.RequirePatientMatching = false. Every Case lands with ContactId = null.
Phase 1+ (patient match): RequirePatientMatching = true. wf-sf-writer calls Azure AI Search before the Case insert. AI Search scores the incoming patient identifiers against the ADF-synced Contact roster — signals: phone, DOB, name similarity (phonetic + 1,100+ nickname pairs). If confidence ≥ threshold, wf-sf-writer includes ContactId in the Case insert from step 3.
Phase 2+ (prescriber match): RequirePrescriberMatching = true. Same approach applied to prescribers. Case.EHR_Practitioner__c pre-populated when match high. The field already exists and wf-sf-writer already writes to it — Phase 2 is purely a flag flip.
The match is advisory, not authoritative. Pharmacy practice still requires DE / pharmacist review. Goal: reduce lookup time, not remove human review.
Step 5 of 9

The Azure Function POSTs structured JSON to the Apex endpoint

P3 only
Azure .NET 8 Azure Function Salesforce PatientOrderIntakeRest
Pre-Phase-3: this step doesn't run. wf-sf-writer only writes the staging set; the data-entry team produces structured records by hand from step 7 onward.
Phase 3: for high-confidence + parseable payloads (gated by Intake_Pipeline_Routing__mdt per channel), the .NET 8 Azure Function POSTs to /services/apexrest/intake/v1/escript over the ADF Integration ECA (OAuth Client Credentials). Body: the parsed domain JSON — idempotency key, patient block, prescriber block, prescriptions array, notes, address, optional staging Case + Rx_Image IDs. (Faxes do NOT come here yet — they're a simple-REST Fax Case until a fax-OCR pipeline emits the same JSON; see §0.)

The PatientOrderIntakeRest endpoint takes the idempotency-ledger lock, deserializes into IntakeRequest, calls PatientOrderIntakeService.intake(), and sets the status code on RestContext.response with an RFC 9457 problem+json body. The status code drives the Azure Function's retry / dead-letter logic — 200 success/replay · 400 malformed · 422 domain-invalid · 409 idempotency · 429 governor · 500 internal · 503 downstream proposal · §0 #2/#3.

200-with-errors won't work. The .NET 8 Azure Function (like the Logic App before it) classifies retry vs. dead-letter off the HTTP status only. A pure-200 body with an errors[] list would be silently treated as success — and every validation failure would be lost.
Step 6 of 9

The Case lands in the work queue

Salesforce Staging Case Human DE team
Primary path (P0–P2): the Case appears in the data-entry queue with subject "LastName, FirstName (DOB)". DE picks it up, opens it, launches the screen flow. Every inbound e-script flows through here.
Fallback path only (P3): only low-confidence matches and parser-failures land here. The bulk of intake goes through the structured POST in step 5 and never reaches the queue.

Either way, the staging Case + image are the audit trail. Even when the Apex service does the work, the staging Case is finalized later (step 8) so the source-image lineage remains intact.

Step 7 of 9

The structured graph appears

Salesforce Account Contact (HC auto) EhrPatient CPA Order Master Rx ×N Fill + OrderItem Pharmacy_Note ×3 new-order Case
P0–P2 (screen flow path): the 7,904-line New_Patient_New_Order_New_Rx_New_Case_Launch_from_data_case walks DE through screens — patient demographics, prescriber, up to 5 prescriptions, address, notes — then creates everything in roughly the order shown above. ~150 nodes.
P3 (Apex service path): all of it runs in one Apex transaction with a SAVEPOINT (taken AFTER the ledger insert). PatientOrderIntakeService.intake() does: resolvePatient() (incl. dedupe backstop) → resolvePrescribers() (per-Rx) → resolveOrAttachOrder() (channel-scoped Draft check) → createPrescriptions()computePrices()createFillsAndOrderItems()createPharmacyNotes()finalizeStagingCase() + Rx_Image. On any failure: ROLLBACK + Error_Log__c + structured error in body.

Either way, the spine is: insert Account → Health Cloud managed package auto-creates the paired Contact → service / flow updates the Contact → insert EhrPatient + ContactPointAddress → insert Order → insert up to 5 Master Rx records → Fill + OrderItem + PricebookEntry → up to 3 Pharmacy_Notes → new-order Case. Who creates Fill/OrderItem differs (rev 2026-06-11): the screen-flow path sets SF_Manually_Entered__c=true and the downstream New_Prescriptions_Manually_Entered flow creates them, with prices applied as a later update (VR bypass via Validation_Bypass_Date_Time__c). The P3 service does NOT set the flag — it computes prices first and creates Fill + OrderItem + PricebookEntry directly with final prices (no update pass, no bypass on insert, no transient $0 window); the downstream flow goes dormant.

Three patient FKs on Rx must agree. HealthCloudGA__Patient__c, HealthCloudGA__Account__c, Contact__c all point at the same human — no managed integrity helps here. Drift = silent inconsistency.
Step 8 of 9

Auto-quarantine check + staging Case finalize

Salesforce Case_Insert_Update_Same_Record new-order Case staging Case

On the new-order Case insert, the canonical Case BeforeSave flow runs an auto-quarantine check: does this patient have any open Case with RT=Quarantined_Order or Refill_Request_Case? If yes, it silently reassigns OwnerId only — it never rewrites the Case RT (corrected 2026-06-11 against the flow XML). Gated on ISNEW=true, so only Inserts are affected.

This is by design — patient with quarantine concerns gets routed to admin, not CS. The service post-queries the Case and returns the actual landed values (actualCaseOwnerType, wasQuarantined — owner-based detection, not RT) plus an AUTO_QUARANTINE_APPLIED warning so consumers don't have to re-query.

Then the staging Case (the original Azure-created Data_Input Case) is finalized: ContactId + EHR_Practitioner__c set, ParentId → new-order Case, Status='Closed', Order__c set. OwnerId deliberately left unchanged — avoids tripping Restrict_Case_Ownership_Changes, the one active VR on Case.

Two quarantine systems exist. Order.Skip_Quarantine_Check__c=true bypasses an Order-side mechanism. It does NOT bypass this Case-side auto-quarantine.
Step 9 of 9

PV1 pharmacist verifies

Human Pharmacist Salesforce Pv1_Screen_Flow

A pharmacist verifies the prescription before any fill ships. PV1 (Pharmacist Verification 1) is the regulatory checkpoint — the actual gate between "data exists in Salesforce" and "patient receives a fill."

PV1 is the same checkpoint regardless of who or what produced the records. Whether DE transcribed by hand, Phase 1+ AI pre-linked an existing patient, or Phase 3's Apex service auto-wrote the entire graph, every prescription passes through this verification before it ships.

Out of scope for this plan — but the path leads here. The whole upstream automation effort exists to make PV1 faster, not to replace it.
Step 1 of 9

§9Hazards & gotchas

The subtle stuff: typo'd API names that mustn't be autocorrected, hardcoded IDs to retrofit, silent-failure surfaces, multi-relationship integrity, Health Cloud upgrade fragility, portal interplay (new 2026-06-11), and a worth-keeping teaching moment about inference vs. ground truth.

These API names are typo'd in the live org. The typos are load-bearing — deployments referencing the "corrected" spelling will fail with INVALID_FIELD. Don't autocorrect.

  • External_Id_c__c on Rx_Image__c_c__c suffix. UI label is External_Id__c (clean). Phase 3 idempotent-upsert path depends on this.
  • Date_filled__c on Master Rx — lowercase d. Service writes this on every Rx insert.
  • Update_Prescription_image_via_Rx_Image_preocesspreocess. It's an inactive Process Builder.
  • Overrided__c on Refill — should be Overrode or Overridden.
  • Special_counseling_requirents_* on Compound — requirents.
  • B2BAuthroize* Apex class family — Authroize.
  • External_Id_c__c consequence: SOQL searches for External_Id__c on Rx_Image__c miss every Azure-staged record.

The screen flow + downstream New_Prescriptions_Manually_Entered flow together hardcode 5 RT IDs, 1 Group ID, and 1 Pricebook ID. ~175 locations across the org carry the same anti-pattern. Phase A and B remove these from new code; Phase E may opportunistically clean up untouched paths.

  • 5 hardcoded RT IDs (DeveloperNames verified live 2026-05-25) — Account IndustriesIndividual · Case Customer_Service_New_Order · Rx-master Needs_Verification (old draft mislabeled "Prescription") · Fill Refill (label "Fill") · Pharmacy_Note Customer_Service
  • 1 hardcoded Group IDCS_Case queue. Worse: stored as a flow constant and pasted inline (the missed-dedup anti-pattern in flow.md)
  • 1 hardcoded Pricebook2 ID01s4W000005JCZUQA4 (Standard Price Book). Survives same-parent sandbox refresh; breaks on cross-org migration.
  • The new service uses RecordType.DeveloperName / Group.DeveloperName / Pricebook2 WHERE Name='Standard Price Book' exclusively.
  • Severity nuance (per .claude/rules/apex.md): User IDs > Profile IDs > RT IDs. User IDs break immediately on deactivation; RT IDs survive same-parent refresh.

Five places in the pipeline can fail without surfacing a visible error. Knowing where they are is half the battle when debugging.

  • Flow fault connectors org-wide: 18% conformance per flow.md §3.12. Flows fired by Azure-written Cases may fail silently.
  • Consumer classifies on HTTP status only: the Phase 3 .NET 8 Azure Function (and the current staging path's Logic Apps Scope_Catch_Failure) treats a 200-with-errors body as success — every validation failure would be lost. (This is why the Phase 3 endpoint must set real HTTP status codes — 400/422/409/429/500/503.)
  • No SF-side audit of Azure failures: the Error_Log__c convention does not extend to Azure-side failures. Debugging needs Azure run history + SQL + Service Bus DLQ.
  • External_Id_c__c typo: SOQL on External_Id__c for Rx_Image__c returns zero Azure-staged records.
  • Azure connector field filtering: some managed Salesforce connectors in Azure do client-side field filtering and silently drop unknowns before the API call. Verify error surfacing in sandbox before relying on it.
  • 75% Apex coverage gate: measures executed lines, not assertion density. increaseCoverage() no-ops + zero-assertion tests inflate reported coverage. Treat as a floor, not a quality signal.

Master Rx carries three parallel patient-side relationships that all must point at the same human. The Health Cloud package's auto-pairing only protects one side of the triangle — the other two are AdvancedRx-added custom Lookups with no managed integrity guarantee.

  • HealthCloudGA__Patient__cHealthCloudGA__EhrPatient__c (managed)
  • HealthCloudGA__Account__cAccount (custom, AdvancedRx-added)
  • Contact__cContact (custom, AdvancedRx-added)

If a future code path sets HealthCloudGA__Patient__c to one patient's EhrPatient but Contact__c to a different patient's Contact, the data is silently inconsistent — none of the Health-Cloud-managed integrity helps. The service must resolve the Account+Contact+EhrPatient triple as a single unit and populate all three FKs from that single resolution result. (Phase A0 §1 documents the screen flow's correct multi-FK population.)

The service writes to managed HealthCloudGA__* objects (Account-side via auto-Contact-create, plus EhrPatient, Master Rx, Fill). Future package upgrades may rename managed fields and silently break the service.

  • Pin Health Cloud version in the test plan. Rerun service tests after every HC managed-package upgrade.
  • 7 VRs on managed HealthCloudGA__* objects (Rx, Practitioner) carry custom AdvancedRx logic — upgrade-fragile too. Any package upgrade may invalidate the bypass logic.
  • The auto-Contact-create dependency is load-bearing. Inserting an Account (Individual RT, Group + NonDual picklist values) triggers HC-managed-package code that creates the paired Contact server-side. The whole 5-step patient-creation chain depends on this. If HC behavior changes, the service breaks silently.
  • Phase A test gate: verify auto-Contact-create works under with sharing Apex running as the integration user before Phase A ships. If it fails, the two surviving options are widening the integration user's perm set (preferred — also required for stripInaccessible field-completeness) or creating the Contact explicitly instead of relying on the auto-create (changes the contract). The old "flip to without sharing" escape hatch is retired — never without sharing on a PHI path.

The 2026-06-11 portal audit corrected the plan's boundary: the portal new-order funnel consumes this service's graph — it takes the intake-created Draft Order as input and operates on the Rx / Fill / OrderItem records the service creates. Five interplay risks, all re-checked by the Phase V2A audit after Portal V2 ships 2026-06-13.

  • Account-lock contention with portal checkout — portal checkout charges the card first, then converts cart→order while holding the patient Account's parent-row lock; the intake transaction holds the same lock for its whole graph build. Overlap → 10s lock timeout, with the failure landing portal-side post-charge (the documented charged-but-no-order hazard). Mitigations: all resolution/validation/pricing BEFORE the first Account-touching DML; map UNABLE_TO_LOCK_ROW → retryable 409 ACCOUNT_LOCK_TIMEOUT so Azure backs off and replays under the ledger.
  • Portal hard-deletes intake-created Fills — cart-item removal / cart deletion delete Fill records this service created. Partial-state / idempotency logic must treat missing graph records as legitimate post-intake mutation, not corrupt replay.
  • $0 Standard-pricebook PBE coupling — intake-created PBEs don't leak into the storefront catalog (the store reads the separate 'Advanced RX Store Price Book' — verified live 2026-06-11), but the refill wizard reads Standard-PBE prices — so the builder creates PBEs at the computed price, never $0.
  • Quarantine-volume interplay — every open Account Case (not On Hold) quarantines the patient's next PORTAL order; unattended intake at volume creates open New-Order Cases. Track quarantine-rate as a Phase D widening metric.
  • Portal V2 supersedes parts of this analysis — the audit ran against pre-V2 metadata; V2 (fill-new-Rx-on-portal; reported card-hold instead of Draft-parking) touches the Draft-Order policy, the absorb/dormancy safety claim, and the lock/Fill risks above. Phase V2A is the closure mechanism.

The Azure pipeline story has been corrected twice during this project's research. Both corrections illustrate the same hazard: structural inference produces internally consistent but externally wrong attributions with unsettling ease.

2026-04-17
Inference: the 5 *_to_Salesforce_*__mdt mapping CMDTs target the LifeFile pipeline. Wrong
Reasoning shape: CMDT naming pattern + LifeFile is real → must be the target. But LifeFile has zero SF-side integration.
2026-04-19
Replacement inference: the same 5 CMDTs feed the Azure inbound flow. Also wrong
Reasoning shape: Azure is real + CMDTs must feed something → must be the target. Azure uses none of these CMDTs; grep found references only in dead NewLeaf batch jobs.
2026-04-21
Confirmed: the 5 CMDTs are NewLeaf-era legacy artifacts — die with the NewLeaf cleanup. Independent of Azure entirely. Current
Lesson worth keeping: internal-shape inference (object models, naming conventions, trigger frameworks, junction patterns, audit-field shapes) is reliable. External-system identity and data-flow topology from metadata alone are not. "Not A, therefore B" isn't confirmation of B — it's a narrower inference with the same shape, and the same failure mode. When a CMDT, code class, or field name suggests "this connects to System X," treat it as a hypothesis until human-confirmed.

§10Open questions

What still needs a decision before each phase ships. Border colors: Kyle decision Marissa needed Sandbox-verifiable Recommended (not blocking).

Q1 · Auth lane / ECA choice (RESOLVED)

Decided · Azure PR #191

Resolved (Azure PR #191; sandbox ECA stood up 2026-05-27; closed-loop fax smoke test cleared 2026-05-28; prod ECA read-only audit cleared 2026-05-28): all Azure writes reuse the existing prod ADF Integration ECA via OAuth 2.0 Client Credentials (run-as azure-integration, api scope; consumer secret in Key Vault) — the same system ECA ADF already uses, kept separate from the Magic_Link_Bridge impersonation ECA (JWT/cert). Two ECAs split by trust profile; no new app, no JWT/cert for writes. The sandbox replica was created via metadata deploy on 2026-05-27 — canonical recipe: docs/runbooks/adf-integration-eca-setup.md — and proven end-to-end via the closed-loop fax smoke test on 2026-05-28. Prod ECA verified 2026-05-28 (read-only UI audit): config matches the runbook spec 1:1 (Run As azure-integration@advancedrx.net, IP Relaxation Enforce, Client Credentials only, scopes Api + RefreshToken, secret + PKCE, refresh 365 Days) — no prod ECA changes needed. 🔴 Prod-side cutover blocker: add Case.Origin (Edit) FLS to the prod Azure_Integration permset before the direct-case lane runs in prod — same change deployed to sandbox 2026-05-27; without it the Function's Origin='Fax' write is silently dropped (prod permset currently has FLS edit on External_ID__c / Subject / Priority / BusinessHoursId but not Origin). Two small UI confirmations still pending in prod: (a) Case.Origin picklist has Fax Active; (b) NAT 135.119.140.149 allow-list location (org-level Trusted IP Ranges vs. run-as user's profile/permset Login IP Ranges — ADF already authenticates against this ECA, so it's allow-listed somewhere). Phase C perm set must still be field-complete (stripInaccessible(CREATABLE) drops anything ungranted; today: zero write FLS on the graph objects). Operational note: after every sandbox refresh, the integration user gets frozen (UserLogin.IsFrozen=true) and must be unfrozen — see docs/plans/multi-sandbox-kit.md "Post-refresh checklist".

Blocks: Phase C (sandbox config + perm set) Owner: Kyle / Marissa Reference: plan.md §0 + §9 #1 + §6 #6

Q2 · Fax intake JSON contract (resolved → confirm)

Kyle + Marissa

Resolved 2026-05-27: inbound faxes become a human-transcription Fax Case via standard REST NOW (the simple-write "brick #1" pattern — out of this @RestResource's scope; see azure-fax-to-case-discovery.md). A future fax-OCR pipeline turns the fax image into the same domain JSON this Apex service consumes, at which point the fax row of Intake_Pipeline_Routing__mdt flips to true. Confirm the OCR→JSON contract equivalence when that phase is scoped.

Blocks: Phase D fax row (future) Owner: Kyle + Marissa Reference: plan.md §0 + §9 #2

Q3 · Confidence-threshold tuning

Kyle (post-launch)

At what Azure-AI confidence floor does the structured-write path stop being worth it (records bouncing at PV1 too often, parser ambiguity outweighing DE-team labor savings)? Tunable Azure-side via the AI Search threshold + the per-channel CMDT — not a Phase D blocker; tune from real-world match quality after launch.

Blocks: nothing (post-launch tuning) Owner: Kyle Reference: plan.md §9 #3

Q3a · Idempotency lock — ledger object vs. Order field (RESOLVED)

Decided · 2026-06-11 audit

Resolved 2026-06-11 (audit fold-in): the generic Integration_Idempotency_Ledger__c is adopted; Order.Intake_Idempotency_Key__c is dropped (live FieldDefinition check: the field never existed — do not create it). Mechanics rewritten as the §4 DML-outcome matrix: ledger insert → savepoint → graph; rollback-to-savepoint preserves the ledger row (updated to Failed); Failed-key replays with a matching hash take over the key; UNABLE_TO_LOCK_ROW → retryable 409. Uncatchable LimitException documented for the Azure classifier (ledger vanishes; platform-shaped 500; clean replay).

Blocks: nothing — resolved Owner: Kyle Reference: plan.md §4 ledger + §9 #4

Q3b · Response wire shape — RFC 9457 problem+json (new)

Kyle + Azure side

Reconciliation proposal 2026-05-27: align the response body to RFC 9457 application/problem+json with extensions errorCode / field / retryable / idempotencyKey (the retryable flag drives the Azure Function's retry-vs-dead-letter decision). Changes the exact wire shape the Function parses — needs joint sign-off on the contract.

Blocks: Phase C Owner: Kyle + Azure side Reference: plan.md §0 #3 + §9 #5

Q3c · HTTP status split — 400 → 422 (new)

Kyle + Azure side

Reconciliation proposal 2026-05-27: move domain/validation rejections from 400 → 422 (400 reserved for a malformed/undeserializable body), and add 429 (governor) + 503 (downstream). The §2 locked decision said 400 for all validation — the Azure Function's retry-classification table must match the split exactly before it locks.

Blocks: Phase C Owner: Kyle + Azure side Reference: plan.md §0 #2 + §9 #6

Q3d · Fax-Case lifecycle (future OCR) (new)

Kyle + Marissa

When the future fax-OCR path lands: one Case lifecycle vs. separate linked lanes? Does an OCR'd fax enrich the same Fax Case the simple-REST "brick #1" already created (one Case, upsert on the same External_ID__c), or does the structured @RestResource mint a separate Data_Input-style staging Case linked back to the Fax Case (two records, two RTs/queues)? The e-script path uses Data_Input; the live fax path uses Fax. This decides RT/queue/automation and the idempotency-key reuse on the OCR flip. Surfaced by the cross-repo review (Repo A).

Blocks: future fax-OCR phase (not Phase A–E) Owner: Kyle + Marissa Reference: plan.md §9 #7 + azure-fax-to-case-discovery.md

Q3e · Portal V2 post-release audit (new 2026-06-11 · HARD GATE)

Gates Phase A exit + B/D

Portal V2 ships 2026-06-13 (fill-new-Rx-on-portal; reported card-hold replacing Draft-parking). Run the Phase V2A checklist against ClaudeTest once the release is in; re-cut the Draft-Order policy, the absorb/dormancy safety claim, and risks #19/#20/#22 where V2 changed facts. Hard gate for Phase A exit and Phase B/D cutover — see the Portal V2 section above.

Blocks: Phase A exit, Phases B + D Owner: Kyle (+ Sarah's deploy) Reference: plan.md §5 Phase V2A + §9 #8

Q3f · Duplicate-Rx detection rule shape (new 2026-06-11)

Kyle decision

Same patient + same Product__c + recent window vs. an existing non-cancelled Rx: surface as 200 + DUPLICATE_RX_SUSPECTED warning (preserving today's 'Duplicate Order' note artifact), or reject 422 to staging for the human 3-way? Today this classification is a HUMAN decision the unattended path removes.

Blocks: Phase A (resolveOrAttachOrder + vocabulary) Owner: Kyle Reference: plan.md §9 #9

Q3g · Sig mapping ownership (new 2026-06-11)

Kyle + Azure side

Sig__c is a Lookup(Sig), not free text — e-scripts carry free-text sigs, so someone must map text → Sig record. Azure-side mapping against an exported Sig catalog with a stable external Id (recommended — mirrors the patient/prescriber-match architecture) vs. service-side exact-text match? Either way the service rejects 422 SIG_NOT_FOUND on a miss — never a silent null-Sig insert.

Blocks: Phase A wire contract + Azure-side work Owner: Kyle + Azure side Reference: plan.md §9 #10 + §4 Prescriptions

Q4 · Manual_Order__c semantics on Rx_Image__c

Marissa needed

Azure never sets this checkbox (verified via wf-sf-writer). The DE-team-sets-it-true-for-manual-uploads interpretation is supported by code + workflow history, but Marissa's explicit confirmation is still pending. Affects nothing in this plan directly — flagged here so future code touching the field knows the semantics aren't fully confirmed.

Blocks: nothing in this plan Owner: Marissa Reference: azure-integration.md open question #2

Q5 · Phase 1 DE workflow redesign

Marissa decision

When RequirePatientMatching flips, DE's workflow changes — some Cases arrive pre-linked, some don't. Kyle's leading idea: encode confidence + match status in the Case subject so DE can quickly assess whether to trust the auto-link. DE keeps human review either way; the goal is reduced lookup time, not removed review. Marissa decides the Case-subject format convention.

Blocks: Phase 1 prod cutover Owner: Marissa Reference: azure-integration.md open question #3

Q6 · SF-side match audit trail

Recommended

Today the AI confidence score lives in Azure SQL only (dbo.PrescriptionDocuments.PatientMatchScore + PatientMatchReasons). A pharmacist debugging "why did this Case link to the wrong patient?" needs Azure SQL access. Recommended before Phase 1 prod cutover: add Case.Match_Confidence_Score__c + Case.Match_Reasons__c; wire wf-sf-writer + usp_UpdateSalesforceStatus to populate. One-line Azure change; SF additions carry the 12-flow + 21-RT blast radius — coordinate the deploy.

Blocks: nothing (deferral OK) Owner: Kyle (decision) + Marissa (UX) Reference: azure-integration.md open question #4

Q7 · ECA consumer secret · Key Vault coverage

Sandbox-verifiable

One credential (the ADF Integration ECA consumer secret covers both ADF reads and all writes): confirm it's tracked in Azure Key Vault with expiry-alert wiring (analogous to MICROSOFT_PROVIDER_AUTHENTICATION_SECRET). Extend task-13-secret-rotation.md so one rotation refreshes both the write/fax Function config AND ADF's linked service (same secret). Note prod and sandbox have separate ECA secrets. Close before prod cutover.

Blocks: Phase 1 prod cutover Owner: Kyle (verifiable in Azure Portal) Reference: azure-integration.md open question #5

Q8 · Health Cloud auto-Contact-create under with sharing

Sandbox-verifiable

Phase A authoring depends on Health Cloud's managed-package code creating the paired Contact when the integration user inserts an Account (Individual RT, Group + NonDual). The screen flow runs SystemModeWithoutSharing. Whether the auto-create works the same when invoked from with sharing Apex is unverified. Phase A gate — if the test fails, the two surviving options are (a) widening the integration user's perm set (preferred — also required for stripInaccessible field-completeness), or (b) creating the Contact explicitly instead of relying on the auto-create (changes the contract). The old "flip to without sharing" escape hatch is retired — never without sharing on a PHI path.

Blocks: Phase A ship Owner: Kyle (sandbox test) Reference: plan.md §6 risk #1 + Phase A0 §5.B

§11Glossary

Acronyms and the API names that recur. Includes the External_Id_c__c typo — preserved deliberately, not autocorrected.

Acronyms
ADF
Azure Data Factory — pulls SF Contact roster into Azure SQL on a schedule.
AI Search
Azure AI Search (formerly Azure Cognitive Search) — scores incoming patients/prescribers against the synced roster.
CMDT
Custom Metadata Type — Salesforce schema for non-secret config that survives sandbox refresh.
CPA
ContactPointAddress — standard SF object; Master-Detail to Account, custom Lookup to Contact.
CS
Customer Service — the team that does patient outreach. CS_Case is their queue's DeveloperName.
DE
Data Entry — the team that transcribes inbound e-scripts via the screen flow.
DLQ
Dead Letter Queue — Azure Service Bus messages that fail are dead-lettered with a descriptive reason.
DLRS
Declarative Lookup Rollup Summaries — managed package that generates rollup triggers (dlrs_*).
FK
Foreign Key — Salesforce Lookup or Master-Detail relationship.
FLS
Field-Level Security — per-field read/edit permission, layered on the perm set.
HC
Health Cloud — Salesforce managed package providing HealthCloudGA__* objects.
LWC
Lightning Web Component.
NC
Named Credential — SF surface for secure outbound callout configuration.
NPI
National Provider Identifier — 10-digit prescriber ID (3 active VRs enforce format).
OWD
Organization-Wide Default — SF base sharing model (Public Read/Write, Read Only, or Private).
PAD
Power Automate Desktop — Microsoft's desktop UI-automation tool. Used to scrape LifeFile.
PB
Process Builder — legacy SF automation tool, superseded by Flow.
PHI
Protected Health Information — under USP 795 + HIPAA scope at AdvancedRx.
PV1
Pharmacist Verification 1 — regulatory checkpoint before any fill ships. Same checkpoint regardless of who produced the records.
RT
Record Type — drives layouts, picklist values, and automation routing.
SCV
Service Cloud Voice — Salesforce voice-call lifecycle product (30 active seats, Amazon Connect routing).
SOQL
Salesforce Object Query Language — SF's read-only query language.
USP 795
United States Pharmacopeia chapter 795 — governs nonsterile compounding. AdvancedRx is licensed in 48 continental states.
VR
Validation Rule — record-level field validation; fails the DML if the formula returns true.
API names that recur
azure-integration@advancedrx.net
The Run-As user for both inbound Azure writes and outbound ADF reads. CreatedById on every Azure-written record.
ADF Integration (External Client App)
The SF-side auth surface for all Azure system data — outbound ADF reads and all writes (fax Case + File, structured intake). OAuth 2.0 Client Credentials, run-as azure-integration, api scope; consumer key + secret in Key Vault (read via Managed Identity). No JWT/cert. Already proven by ADF; the fax writes reuse it. Slated to be renamed broad (e.g. Azure Integration). Sandbox replica created 2026-05-27 and proven via the closed-loop fax smoke test 2026-05-28; prod ECA verified 2026-05-28.
Magic_Link_Bridge (External Client App)
The separate impersonation ECA (JWT-Bearer + Key-Vault cert) for patient magic-link login. Not used for system data writes — those use ADF Integration (above). Two ECAs split by trust profile.
Azure Logic Apps / Data Factory (legacy Connected Apps)
Classic Connected Apps still present in the org (0H4aZ00000013OrSAI / 0H44W000000M5hgSAC) but NOT the system auth — per Kyle, ADF + writes use the ADF Integration ECA. azure-integration.md still describes an "Azure Logic Apps Connected App" auth surface and is stale on this point (flagged for a separate doc-cleanup PR).
Azure_Integration
The perm set layered on the integration user. Each phase expands its scope.
Case_Insert_Update_Same_Record
Canonical Case BeforeSave flow. The auto-quarantine override lives here — it reassigns OwnerId only, never the RT (corrected 2026-06-11), when the patient has an open quarantined or refill-request Case; gated on ISNEW=true.
Escript.Evaluator.RequirePatientMatching
Phase 1 feature flag (Azure-side). False today; flipping to true triggers AI patient-matching and pre-populates Case.ContactId on high-confidence matches.
Escript.Evaluator.RequirePrescriberMatching
Phase 2 feature flag (Azure-side). False today; flipping triggers prescriber matching, pre-populates Case.EHR_Practitioner__c.
External_Id_c__c
Typo'd API name on Rx_Image__c. The _c__c suffix is preserved deliberately — autocorrecting to External_Id__c breaks every reference.
External_ID__c
Clean API name on Case. Different object than above; both carry the same Rx External ID value.
Intake_Pipeline_Routing__mdt
New CMDT in Phase D. Per-channel + per-confidence-band routing — true = use Phase 3 endpoint, false = staging path.
Integration_Idempotency_Ledger__c
NEW generic ledger object (decided 2026-06-11). Unique-indexed, channel-prefixed idempotency keys; inserted before any graph DML — the uniqueness index is the distributed lock; stores the response body for completed-key replays. Supersedes Order.Intake_Idempotency_Key__c (dropped — never existed).
LS_Salesforce_Production
The ADF linked service that authenticates to Salesforce.
Minimum Access - Azure Integration
Least-privilege custom profile (CLAUDE.md §3.14 exemplar) — least-perms base; capabilities layered via the perm set.
New_Patient_New_Order_New_Rx_New_Case_Launch_from_data_case
The 7,904-line canonical screen flow. Produces the structured patient + order + Rx graph from a staging Case. Phase B refactor target.
New_Prescriptions_Manually_Entered
Downstream flow that fires when SF_Manually_Entered__c=true on Master Rx insert, creating Fill + OrderItem + PricebookEntry. Absorbed 2026-06-11: the service creates those records itself and never sets the flag — the flow goes Active-but-dormant and retires at Phase E (gated on the V2A writer re-check).
Portal V2
The B2B patient-portal release shipping 2026-06-13 — fill-new-Rx-on-portal + a reported credit-card hold replacing post-payment Draft-parking. Audited post-release by Phase V2A, which gates Phase A exit and Phase B/D cutover.
PatientOrderIntakeRest
New Apex @RestResource at /services/apexrest/intake/v1/escript (Phase 3). Maps Azure JSON → service.intake() → HTTP response.
PatientOrderIntakeService
New Apex service class (Phase 3). Single canonical intake(IntakeRequest) method shared by the REST surface and the refactored screen flow. Creates the full graph including Fill + OrderItem + PricebookEntry (absorbed 2026-06-11), with prices computed before insert.
Validation_Bypass_Date_Time__c
OrderItem field that bypasses the Prevent_Manual_Edits VR — required on every OrderItem UPDATE. Creates are bypass-free under the price-before-create design (rev 2026-06-11).
wf-sf-dispatch
Logic App that fans out per-document writes via Service Bus.
wf-sf-writer
Logic App that writes to Salesforce. Carries Scope_Catch_Failure for HTTP-error-driven DLQ.
wf-start-lifefile-automation
Daily 8 AM Logic App that triggers the PAD bot.