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 auditPortal V2 ships 2026-06-13 · post-release audit gates Phase A exit
+ B/D cutoverOwner: KyleLast 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:
The service now creates Fill + OrderItem + PricebookEntry
directly
(absorbing New_Prescriptions_Manually_Entered), with
prices computed BEFORE insert via a new bulk pricing method.
Idempotency mechanics rewritten as a DML-outcome
matrix on a generic Integration_Idempotency_Ledger__c;
Order.Intake_Idempotency_Key__c dropped (the field
never existed in the org).
Draft-Order auto-attach retired on the unattended path
+ channel-scoped Draft existence check — paid portal orders park at
Status='Draft' for pharmacist counseling and reships
create Drafts, so a channel-blind query cross-captures portal
orders.
Wire-contract additions — per-Rx prescriber,
sigId (Sig__c is a Lookup, not text),
orderDisposition, patientMemo,
fillIds[]/orderItemIds[], patient-dedupe
backstop, fail-fast FLS. Six new error codes:
PATIENT_POSSIBLE_DUPLICATE, SIG_NOT_FOUND,
DRAFT_ORDER_EXISTS, ACCOUNT_LOCK_TIMEOUT,
FLS_MISCONFIGURED, DUPLICATE_RX_SUSPECTED.
§4 factual corrections — patient-name format
is First [Middle ]Last (not Last, First);
the Case-attach criterion is Order__c-keyed (not
same-day); the auto-quarantine override reassigns OwnerId only (not
RT); pricing's no-price edge throws NPE for STANDARD products (not
null).
NEW GATE: the Portal V2 release (2026-06-13) must
be audited post-release before Phase A code-freezes — see the Portal
V2 section below.
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)
Diff Portal_New_Order_Create_Rx_Cart_Items,
B2B_Cart_To_Order,
Order_Portal_Update_Async,
B2B_Update_Order_Fields_Order_On_Order_Summary_Creation, and the checkout/payment Apex (B2BPaymentMethodsController
etc.) against the audited pre-V2 versions.
Re-verify the org-wide writer inventory for
Rx.SF_Manually_Entered__c (flows + Apex + LWC). Any new
writer blocks the dormancy/retirement plan until reconciled.
Trace the new fill-new-Rx-on-portal path's record graph (does it
create NV-RT master Rxes? Fills? which Order does it attach to?) and
re-cut the portal-consumption contract accordingly.
Confirm or refute the Draft-parking removal + card-hold model
(auth/capture mechanics, where capture fires, pre-PV1 order status)
and re-cut the Draft-Order channel-scoping analysis + risk #20.
Re-verify getOrderListNew /
Visible_on_Portal__c behavior for intake-created Draft
Orders under V2.
Refresh storefront-data-audit.md +
portal-refill-funnel.md (both past TTL + partially
superseded by V2; they carry supersession banners until then).
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:
External systemAzureSalesforceSF record / objectHuman 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
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
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
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
V2A · Portal V2 post-release audit —
NEW 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 decision —
inherited 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.
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'
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 + Published —
wf-sf-dispatch won't auto-retry; manual replay only
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_Production →
ADF 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 → IntakeRequest →
Integration_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).
ADF Integration ECA, OAuth 2.0 Client Credentials
(run-as azure-integration); perm set extended
field-complete for the intake graph (required
by stripInaccessible)
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
ContentVersion — VersionData (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)
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
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)
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
SalesChannelId = lookup
WHERE SalesChannelName='AdvancedRx'
Intake_Idempotency_Key__c —
RESOLVED 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.
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__c — note 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__c —
NOT 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)
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__c — InFlight →
Completed / Failed (flips inside the
same transaction)
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__c —
Fill → 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[]
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
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 separateMagic_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 · Upstream01
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 scrape02
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 · Orchestration03
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.
Trust handoff · Azure → Salesforce
Step 4 · ECA · Client Credentials04
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 identity05
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 · Profile06
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 set07
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 surface08
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/escript →
PatientOrderIntakeRest, 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 sameADF 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.
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.
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
UpstreamLifeFile
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
Azurewf-start-lifefile-automationPAD 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.
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)
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
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
SalesforceStaging CaseHumanDE 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
SalesforceAccountContact (HC auto)EhrPatientCPAOrderMaster Rx ×NFill + OrderItemPharmacy_Note ×3new-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
SalesforceCase_Insert_Update_Same_Recordnew-order Casestaging 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
HumanPharmacistSalesforcePv1_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_preocess —
preocess. 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 ID —
CS_Case queue. Worse: stored as a flow constant
and pasted inline (the missed-dedup anti-pattern in
flow.md)
1 hardcoded Pricebook2 ID —
01s4W000005JCZUQA4 (Standard Price Book). Survives
same-parent sandbox refresh; breaks on cross-org migration.
The new service usesRecordType.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.
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 decisionMarissa neededSandbox-verifiableRecommended (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".
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.
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.
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).
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.
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.
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 separateData_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).
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 + DOwner: 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: KyleReference: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.
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 planOwner: MarissaReference: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 cutoverOwner: MarissaReference: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.
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 cutoverOwner: 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.
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.
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.
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.