---
title: Unified intake-graph Apex service (Azure + screen flow)
description: One Apex service shared by Azure and the data-entry screen flow, replacing the 7,904-line transcription monolith. Phase 3 of the Azure roadmap.
status: Active
owner: Kyle
updated: 2026-05-09
last_verified: 2026-05-06
verification_type: eyeball
verification_ttl_days: 60
phase: ready-for-Phase-A
created: 2026-05-06
---

# Unified intake-graph Apex service — Azure + screen flow

> **What we're building:** one Apex service (`PatientOrderIntakeService`) that owns the create-graph for "new patient + order + prescription[] + dependents" — called from BOTH a new Azure REST endpoint AND a refactored data-entry screen flow. Replaces the orchestration that today lives inside the 7,904-line screen flow `New_Patient_New_Order_New_Rx_New_Case_Launch_from_data_case`.
>
> **Why:** Phase 3 of the Azure roadmap (`business-context/integrations/azure-integration.md`) PLUS a deliberate consolidation. Today's data-entry team transcribes inbound e-scripts manually via the screen flow; Azure is being extended to send structured payloads instead. We could replicate the screen-flow logic in two places, or own it once. Once is correct.
>
> **Status:** discovery complete; ready for Phase A authoring.

## §1 — Today's state (what exists now)

**Inbound e-scripts via the Azure pipeline** (Phase 0, live): LifeFile e-scripts are scraped by an Azure PAD bot and pushed to Salesforce as 3-record staging sets — `ContentVersion` (the document file) + `Rx_Image__c` (a staging record) + `Case` with `RecordType.DeveloperName='Data_Input'`. No Apex on the inbound path; Azure uses the standard REST Data API.

**Phase 1 (in progress, feature-flag off today):** Azure AI Search scores incoming patient data against the contact roster; high-confidence matches → `Case.ContactId` pre-populated. **Phase 2 (next):** same for `Case.EHR_Practitioner__c`. Both are flag flips, not new field additions.

**Data-entry team's transcription workflow** today: opens the staging Case, launches the screen flow, walks through screens to enter patient + prescriber + up to 5 prescriptions + notes + address. The flow then creates the structured records and links everything together.

**This plan = Phase 3** of the Azure roadmap (Azure writes structured records directly) + a screen-flow refactor so both intake paths share one Apex service.

## §2 — Locked architectural decisions

| Decision                                   | Locked answer                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Surface for Azure                          | **Custom `@RestResource` taking domain JSON.** Decouples Azure from Salesforce schema; one Apex transaction; structured response body.                                                                                                                                                                                                                                                                                                                                                                                                               |
| Both intake pipelines share one Apex layer | **Yes.** Single source of truth for "create patient + order + Rx graph" rules.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Service shape                              | **`PatientOrderIntakeService`** with a single canonical `intake(IntakeRequest req)` method. Two thin surfaces wrap it: `@InvocableMethod` (flow callers) + `@RestResource` (Azure).                                                                                                                                                                                                                                                                                                                                                                  |
| Project size                               | **Big project.** Multi-phase. Touches a load-bearing screen flow + introduces the org's first cohesive service class + first `@RestResource` class.                                                                                                                                                                                                                                                                                                                                                                                                  |
| Patient identity model                     | **Account → auto-Contact-create → EhrPatient** (5-step chain). Inserting an Account with the right Health Cloud picklist values triggers managed-package code that creates the paired Contact automatically. We then look up the Contact, update it, and insert the EhrPatient.                                                                                                                                                                                                                                                                      |
| Idempotency                                | **Domain-level idempotency key** on the payload root. Replays return existing IDs (200 with warnings); partial-state replays reject (require human intervention).                                                                                                                                                                                                                                                                                                                                                                                    |
| HTTP status code semantics                 | **200** success or replay-of-existing (warnings allowed in body). **400** validation rejection (missing required, invalid format, prescriber-not-in-roster, etc.). **409** idempotency conflict (replay-with-different-payload, partial-state). **500** internal failure (Apex exception). Body always carries structured `errors[]` / `warnings[]` for diagnosis. **Reasoning:** Logic Apps' `Scope_Catch_Failure` triggers on HTTP error status codes only — a pure 200-with-errors body is treated as success and silently lost. See §6 risk #14. |
| Error-code naming convention               | `SCREAMING_SNAKE_CASE` strings, broad-category prefix. Validation/domain: `INVALID_*` / `MISSING_*` / `*_NOT_IN_ROSTER` / `*_NOT_FOUND` / `*_INACTIVE`. State conflict: `IDEMPOTENCY_*` / `*_CONFLICT` / `*_PARTIAL_STATE`. Internal: `INTERNAL_ERROR` / `DOWNSTREAM_*`. Warnings (informational, not blocking) use the same shape but live in `warnings[]`. See §3 for the wrapper shape and initial vocabulary.                                                                                                                                    |

## §3 — Target architecture

```
┌─────────────────────────────────────────────────────────────────┐
│ Azure Logic Apps (wf-sf-writer extension)                        │
│   POST /services/apexrest/intake/v1/escript                      │
│   Body: domain JSON (patient, prescriber, prescriptions[], ...)  │
└──────────────────┬──────────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────────┐
│ PatientOrderIntakeRest (@RestResource)                           │
│   - Authenticates (Connected App + Integration User)              │
│   - Deserializes JSON → IntakeRequest                            │
│   - Calls PatientOrderIntakeService.intake(req)                  │
│   - Returns IntakeResponse as HTTP body                          │
└──────────────────┬──────────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────────┐
│ PatientOrderIntakeService.intake(IntakeRequest req)              │  ◄── canonical
│   public static IntakeResponse intake(IntakeRequest req)         │      core
│   - SAVEPOINT                                                     │
│   - resolvePatient(req.patient)                                  │
│   - resolvePrescriber(req.prescriber)                            │
│   - resolveOrAttachOrder(req)                                    │
│   - createPrescriptions(req.prescriptions, order)                │
│   - applyPricing(prescriptions) via B2BProductPricingService     │
│   - createPharmacyNotes(req.notes, case, order)                  │
│   - linkBackToStagingCase + linkBackToRxImage                    │
│   - on any failure → ROLLBACK + Error_Log__c + structured error  │
│   - return { success, recordIds, warnings, errors }              │
└──────────────────▲──────────────────────────────────────────────┘
                   │
┌──────────────────┴──────────────────────────────────────────────┐
│ PatientOrderIntakeInvocable (@InvocableMethod)                   │
│   - Called from refactored screen flow                           │
│   - Same IntakeRequest shape; flow Assignment nodes build it      │
└─────────────────────────────────────────────────────────────────┘
```

### Service class signature (sketch)

```
PatientOrderIntakeService     // with sharing — see §6 risks
  +static IntakeResponse intake(IntakeRequest req)
  -static PatientResolution resolvePatient(PatientBlock p)
  -static PrescriberResolution resolvePrescriber(PrescriberBlock p)
  -static OrderResolution resolveOrAttachOrder(IntakeRequest req, PatientResolution pr)
  -static List<Id> createPrescriptions(List<PrescriptionBlock> rxs, Id orderId, PatientResolution pr, PrescriberResolution prr)
  -static void applyPricing(List<Id> rxIds, Id orderId)
  -static void createPharmacyNotes(NotesBlock notes, Id newOrderCaseId, Id stagingCaseId, Id orderId)
  -static void finalizeStagingCase(Id stagingCaseId, Id newOrderCaseId, Id orderId, Id contactId, Id prescriberId)
  -static void linkRxImage(Id rxImageId, Id contactId, Id prescriberId)

PatientOrderIntakeRest        // @RestResource(urlMapping='/intake/v1/escript')
PatientOrderIntakeInvocable   // @InvocableMethod for flow callers

IntakeRequest   { idempotencyKey, sourceChannel, stagingCaseId?, stagingRxImageId?, patient, prescriber, prescriptions[], notes, addressOverride? }
IntakeResponse  { success, accountId?, contactId?, ehrPatientId?, contactPointAddressId?,
                  prescriberId?, orderId?, newOrderCaseId?,
                  actualCaseRecordType?, actualCaseOwnerType?, wasQuarantined?,  // see §4 New-order Case
                  prescriptionIds[], pharmacyNoteIds[], warnings[], errors[] }

Error    { code, message, field?, recordType?, details? }   // details? : Map<String, Object> for structured context
Warning  { code, message, field?, recordType?, details? }   // same shape; warnings don't fail the intake
```

### HTTP status code mapping

The wrapper above is compatible with HTTP status codes — the body always describes outcome, AND the status code drives transport-layer retry/dead-letter semantics. Logic Apps' `Scope_Catch_Failure` triggers on 4xx/5xx; a pure 200-with-errors body would be silently treated as success.

| Status        | When                                                                                                                                       | `success` field                                                                             |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| **200**       | Intake succeeded, OR idempotency replay returned existing graph                                                                            | `true` (with `warnings[]` allowed — e.g., `AUTO_QUARANTINE_APPLIED`, `PRICE_NOT_AVAILABLE`) |
| **400**       | Validation rejection — payload format, missing required, prescriber-not-in-roster, invalid phone, etc.                                     | `false`                                                                                     |
| **401 / 403** | Auth issue — Connected App credential, perm-set scope. Body may be empty.                                                                  | n/a                                                                                         |
| **409**       | Idempotency conflict — replay with a different payload for the same key, or partial-state replay where prior attempt left orphaned records | `false`                                                                                     |
| **500**       | Apex exception or unhandled error. Body carries `errors[].details.errorLogId` for trace.                                                   | `false`                                                                                     |

### Error / Warning code vocabulary (initial)

Convention is in §2 locked decisions. Initial codes the service must produce:

| Category       | HTTP | Code                           | When                                                       | `details` shape                                                   |
| -------------- | ---- | ------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------------------- |
| Validation     | 400  | `INVALID_PHONE_FORMAT`         | Phone field can't be normalized to 10 digits               | `{ rawValue, expectedLength }`                                    |
| Validation     | 400  | `MISSING_REQUIRED_FIELD`       | Required field absent or empty                             | `{ fieldPath }`                                                   |
| Domain reject  | 400  | `PRESCRIBER_NOT_IN_ROSTER`     | NPI lookup miss; v1 doesn't auto-create prescribers        | `{ searchedNpi, searchedId? }`                                    |
| Domain reject  | 400  | `PRODUCT_NOT_FOUND`            | Product reference doesn't resolve                          | `{ productExternalId? }`                                          |
| Domain reject  | 400  | `PATIENT_INACTIVE`             | Existing-patient reuse path matched a deactivated Contact  | `{ contactId }`                                                   |
| State conflict | 409  | `IDEMPOTENCY_REPLAY_CONFLICT`  | Same idempotency key, different payload                    | `{ idempotencyKey, existingOrderId }`                             |
| State conflict | 409  | `IDEMPOTENCY_PARTIAL_STATE`    | Prior attempt left partial graph; manual cleanup required  | `{ idempotencyKey, existingRecords }`                             |
| Internal       | 500  | `INTERNAL_ERROR`               | Unhandled Apex exception                                   | `{ errorLogId }`                                                  |
| Internal       | 500  | `DOWNSTREAM_PRICING_FAILED`    | `B2BProductPricingService` threw or returned invalid       | `{ errorLogId }`                                                  |
| Warning (200)  | n/a  | `AUTO_QUARANTINE_APPLIED`      | Case BeforeSave override fired (see §4 New-order Case)     | `{ existingQuarantineCaseIds[], existingRefillRequestCaseIds[] }` |
| Warning (200)  | n/a  | `PHONE_INVALID_FORMAT_DROPPED` | A non-required phone field was dropped (kept intake going) | `{ field, rawValue }`                                             |
| Warning (200)  | n/a  | `PRICE_NOT_AVAILABLE`          | Pricing service returned null for one or more Rxes         | `{ rxIds[] }`                                                     |

Phase C may add codes; the convention in §2 governs naming.

## §4 — The intake-graph contract (what the service writes)

### Account (Individual RT)

| Field                              | Value                             | Notes                               |
| ---------------------------------- | --------------------------------- | ----------------------------------- |
| `RecordTypeId`                     | DeveloperName=`Individual` lookup | Do not hardcode the Id              |
| `Name`                             | `<lastName>, <firstName>`         |                                     |
| `HealthCloudGA__IndividualType__c` | `'Group'`                         | Required for HC auto-Contact-create |
| `HealthCloudGA__EnrollmentType__c` | `'NonDual'`                       | Required for HC auto-Contact-create |
| `SF_Manually_Entered__c`           | `true`                            | Same flag pattern used on Rx        |

**Critical:** inserting this Account triggers Health Cloud managed-package automation that auto-creates a paired `Contact` (Patient RT). The service then queries for the Contact, updates it (FirstName / MiddleName / LastName / Patient_Memo\_\_c), and proceeds.

**Existing-patient reuse path:** when `IntakeRequest.patient.existingContactId` is supplied (or `stagingCase.ContactId` is non-null from Phase 1 AI match), skip the Account+Contact+EhrPatient creation entirely and reuse the existing triple. Look up the existing Account via `Contact.AccountId`, the existing EhrPatient via `WHERE HealthCloudGA__Account__c = :accountId LIMIT 1`.

### Contact (Patient RT) — auto-created by Health Cloud, then updated

The service does NOT insert this Contact directly. Health Cloud creates it; the service updates with payload-supplied fields:

| Field                                                                                 | Value                                                                                                |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `FirstName`, `MiddleName`, `LastName`                                                 | from payload                                                                                         |
| `Patient_Memo__c`                                                                     | from payload (optional)                                                                              |
| Phone fields (`Phone`, `MobilePhone`, `HomePhone`, `OtherPhone`, `SecondaryPhone__c`) | from payload, **formatted to exactly 10 digits or rejected** (Contact has 5 active phone-format VRs) |

### EhrPatient (`HealthCloudGA__EhrPatient__c`)

| Field                           | Value                       | Notes                                             |
| ------------------------------- | --------------------------- | ------------------------------------------------- |
| `HealthCloudGA__Account__c`     | the new Account.Id          |                                                   |
| `Contact__c`                    | the auto-created Contact.Id | Custom back-link                                  |
| `HealthCloudGA__BirthDate__c`   | from Contact.Birthdate      |                                                   |
| `HealthCloudGA__GenderLabel__c` | from Contact.Gender         |                                                   |
| `HealthCloudGA__Name__c`        | `<lastName>, <firstName>`   |                                                   |
| `HealthCloudGA__GivenName1__c`  | First name                  | **Non-obvious naming: 1=First, 2=Middle, 3=Last** |
| `HealthCloudGA__GivenName2__c`  | Middle name                 |                                                   |
| `HealthCloudGA__GivenName3__c`  | Last name                   |                                                   |

### ContactPointAddress

| Field                                             | Value                                                           | Notes                                                                    |
| ------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `Name`                                            | `'Default'`                                                     |                                                                          |
| `Street, City, State, Country, PostalCode`        | from payload                                                    |                                                                          |
| `ParentId`                                        | the Account.Id                                                  | **Master-Detail to Account, NOT Contact**                                |
| `Contact__c`                                      | the Contact.Id                                                  | Custom lookup tracking which Contact the address belongs to              |
| `Primary__c`                                      | `true`                                                          |                                                                          |
| `Status__c`                                       | `'Active'`                                                      |                                                                          |
| `Verification_Status__c` / `Unverified_Reason__c` | from payload (when address-validation gating produced a result) | `pw_ccpro` trigger fills the rest of the verification fields post-insert |

### EhrPractitioner (`HealthCloudGA__EhrPractitioner__c`)

**Lookup-only in v1.** Service queries by `Id = req.prescriber.id` (or NPI lookup). On miss, **service rejects with HTTP 400** + `errors[]` containing `{ code: 'PRESCRIBER_NOT_IN_ROSTER', message: 'NPI <X> not found in EhrPractitioner roster', details: { searchedNpi, searchedId? } }`. Intake does not proceed. The existing prescriber roster is the source of truth; auto-create of new prescribers is out of scope for v1 — see §10.

### Order

11 explicit field assignments:

| Field                       | Value                                        | Notes                                                        |
| --------------------------- | -------------------------------------------- | ------------------------------------------------------------ |
| `AccountId`                 | resolved Account.Id                          |                                                              |
| `BillToContactId`           | resolved Contact.Id                          | Same as patient                                              |
| `ShipToContactId`           | resolved Contact.Id                          | Same as patient                                              |
| `Customer_Contact__c`       | resolved Contact.Id                          | Custom — same as patient                                     |
| `EffectiveDate`             | `Date.today()`                               |                                                              |
| `Rx_Image__c`               | `stagingCase.Rx_Image__c`                    | Custom Order lookup back to staging Rx_Image                 |
| `SF_Direct_Rx__c`           | `true`                                       | "Direct Rx" flag                                             |
| `SalesChannelId`            | lookup `WHERE SalesChannelName='AdvancedRx'` |                                                              |
| `Skip_Quarantine_Check__c`  | `true`                                       | Bypasses Order-side quarantine; matches existing screen flow |
| `Status`                    | `'Draft'`                                    |                                                              |
| `Transaction_Date__c`       | `Date.today()`                               |                                                              |
| `Intake_Idempotency_Key__c` | from payload                                 | **NEW field** to be added in Phase A                         |

**Draft-Order reuse:** if patient has an existing Draft Order, reuse instead of creating a new one. Multi-Rx-same-day: if a same-day New-Order Case already exists for the patient, attach to it.

### Prescriptions (`HealthCloudGA__EhrMedicationPrescription__c`, RT=Prescription)

Up to N (current screen flow caps at 5; Phase A re-evaluates). 17 fields per Rx:

| Field                           | Value                               | Notes                                                                                                     |
| ------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `RecordTypeId`                  | DeveloperName=`Prescription` lookup |                                                                                                           |
| `HealthCloudGA__Patient__c`     | EhrPatient.Id                       |                                                                                                           |
| `HealthCloudGA__Account__c`     | Account.Id                          |                                                                                                           |
| `Contact__c`                    | Contact.Id                          | All three patient-side relationships must match — see hazard #5                                           |
| `HealthCloudGA__Prescriber__c`  | EhrPractitioner.Id                  |                                                                                                           |
| `Prescriber_ID__c`              | EhrPractitioner.Id (as Text(255))   | Denormalized — load-bearing for downstream flows                                                          |
| `Product__c`                    | from payload                        |                                                                                                           |
| `Sig__c`                        | from payload                        |                                                                                                           |
| `Quantity__c`                   | from payload                        |                                                                                                           |
| `RefillsAuthorized__c`          | from payload                        |                                                                                                           |
| `HealthCloudGA__DateWritten__c` | from payload                        |                                                                                                           |
| `Date_filled__c`                | `Date.today()`                      | Note typo'd lowercase `d`                                                                                 |
| `Price__c`                      | `0.0` (initialized)                 | Updated by `applyPricing()` post-insert                                                                   |
| `Order__c`                      | the new Order.Id                    |                                                                                                           |
| `Rx_Image__c`                   | `stagingCase.Rx_Image__c`           |                                                                                                           |
| `SF_Manually_Entered__c`        | `true`                              | **Required.** Triggers downstream `New_Prescriptions_Manually_Entered` flow that creates Fill + OrderItem |

### Fill + OrderItem — created by downstream flow, not this service

Setting `SF_Manually_Entered__c = true` on each Master Rx fires `New_Prescriptions_Manually_Entered` AfterSave, which creates:

- A **Fill record** (RT=Fill on the same `HealthCloudGA__EhrMedicationPrescription__c` object)
- An **OrderItem** (Order Product) linked to the Order, the PricebookEntry, and the Fill via custom `Prescription_Link__c` FK
- A **PricebookEntry** if one doesn't exist for the Product

The service does NOT create these directly. It DOES, however, query Fill + OrderItem records post-insert in order to apply prices (see `applyPricing` below) — and updating those OrderItems requires the VR bypass detailed in §6 risk #2.

### Pricing — `applyPricing()` after Rxes exist

After the Rx records are inserted (and the downstream flow has created Fills + OrderItems), the service queries those records, calls `B2BProductPricingService.getCustomPriceLWC()`, and updates per-record:

| Record    | Fields written                                                                                       |
| --------- | ---------------------------------------------------------------------------------------------------- |
| Fill      | `Calculated_Price__c`, `Price__c`                                                                    |
| OrderItem | `ListPrice`, `UnitPrice=1.0`, `TotalLineAmount`, **`Validation_Bypass_Date_Time__c=Datetime.now()`** |

The bypass field is mandatory for OrderItem updates — without it the `Prevent_Manual_Edits` VR rejects every update.

**Pricing edge cases the service must handle:**

- **No matching `Custom_Product_Price__c` row** → null price returned. Surface as a `warning` in the response; let humans review. Don't fail the intake.
- **All-STANDARD-products with zero contributing-quantity** → upstream divide-by-zero exception. Pre-count contributors (`Product__r.Count_Towards_Total_Qty__c=true`) before calling; if zero, skip the pricing call and surface a warning.
- **`totalQuantity` capped at 900** by the pricing service. Preserve the cap.

### Pharmacy_Note records (RT=`Customer_Service`)

Up to 3 Notes per intake, all linked to either the new-order Case or the original staging Case:

| Note Name                | Linked Case           | Source                                                                                                      |
| ------------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------- |
| `'Active Portal Cart'`   | new-order Case        | Created when patient has an active WebCart with Rx CartItems; describes the cart contents for CS visibility |
| `'Note from Data Input'` | new-order Case        | Free-form note from payload (or DE-team input via the refactored screen flow)                               |
| `'Duplicate Order'`      | original staging Case | Created when intake detects this is a duplicate of an existing Order                                        |

All three use `RecordType.DeveloperName='Customer_Service'` (looked up; never hardcoded).

### New-order Case

| Field                              | Value                                                         |
| ---------------------------------- | ------------------------------------------------------------- |
| `RecordTypeId`                     | DeveloperName=`Customer_Service_New_Order` lookup             |
| `ContactId`                        | resolved Contact.Id                                           |
| `AccountId`                        | resolved Account.Id                                           |
| `Status`                           | `'New'`                                                       |
| `Subject`                          | `'New Order'`                                                 |
| `New_Patient__c`                   | `true` if new patient, `false` if existing                    |
| `New_Prescription_Order_Status__c` | `'Contact Patient'`                                           |
| `HealthCloudGA__DueDate__c`        | `Date.today()`                                                |
| `Order__c`                         | the new Order.Id                                              |
| `EHR_Practitioner__c`              | resolved EhrPractitioner.Id                                   |
| `OwnerId`                          | lookup `Group WHERE DeveloperName='CS_Case' AND Type='Queue'` |

**Heads-up — auto-quarantine override:** the canonical Case BeforeSave flow may override RT and OwnerId for some patients (see §6 risk #3). **Mitigation:** after the Case insert, the service post-queries the Case and reports the actual landed values in `IntakeResponse`:

- `actualCaseRecordType` (Text — the DeveloperName that landed; may be `Customer_Service_New_Order` or `Quarantined_Order`)
- `actualCaseOwnerType` ('Queue' | 'User' — what actually owns the Case post-insert)
- `wasQuarantined` (Boolean — convenience flag; true when the override fired)

When `wasQuarantined=true`, the service also adds a `warnings[]` entry with `code='AUTO_QUARANTINE_APPLIED'` and `details: { existingQuarantineCaseIds[], existingRefillRequestCaseIds[] }` for analytics + DE-team UX surfacing. Consumers (Azure-side `wf-sf-writer`, Phase B refactored screen flow) branch on these fields rather than re-querying the Case themselves.

### Staging-Case finalization + Rx_Image back-link

Original staging Case (the Azure-created `Data_Input`-RT Case) is updated:

- `ContactId` = resolved Contact.Id
- `EHR_Practitioner__c` = resolved EhrPractitioner.Id
- `ParentId` = new-order Case.Id (or existing same-day New-Order Case.Id)
- `Status` = `'Closed'`
- `Order__c` = the new Order.Id
- `OwnerId` — **NOT updated.** Diverges from the screen-flow path, which today reassigns OwnerId to the running user. Service has no "running user"; the staging Case stays owned by the integration user (its original CreatedBy). Avoids tripping the `Restrict_Case_Ownership_Changes` VR. See §6 risk #15.

Staging `Rx_Image__c` is updated:

- `Contact__c` = Contact.Id
- `EHR_Practitioner__c` = EhrPractitioner.Id

## §5 — Phase decomposition

### Phase A0 — Discovery (complete)

**What it was:** before authoring the service, validate every assumption against the live screen flow + sandbox schema. Walked the screen flow's Assignment nodes for every service-bound SObject variable; ran `FieldDefinition` + `ValidationRule` SOQL on every target object; read the downstream `New_Prescriptions_Manually_Entered` flow that creates OrderItems + Fills.

**Why first:** the screen flow's documented behavior didn't match its actual behavior in several material ways. Catching that on paper saves Phase A authoring from being half-rewrite.

**Output:** [`phase-a0-discovery.md`](phase-a0-discovery.md). The contract in §4 above and the risks in §6 below incorporate everything the discovery surfaced. Tech team can read the discovery report for audit trail; the contract here is the operational view.

### Phase A — Author the service (foundation)

**Scope:** `PatientOrderIntakeService` core + wrapper classes + helper resolvers. Tests only — no callers wired yet.

**Deliverables:**

1. `PatientOrderIntakeService.cls` (`with sharing`).
2. Wrapper classes (`IntakeRequest`, `IntakeResponse`, `PatientBlock`, `PrescriberBlock`, `PrescriptionBlock`, `NotesBlock`, `Error`, `Warning`).
3. Helper class `PhoneFormatter` (strip non-digits, validate length 10, reject otherwise).
4. New custom field `Order.Intake_Idempotency_Key__c` (Text(64), External ID, Unique, Case-Insensitive).
5. `IntakeTestFactory` test class producing representative `IntakeRequest` shapes + seed methods for SalesChannel `'AdvancedRx'`, Standard Pricebook, test Product+Custom_Product_Price\_\_c rows, test EhrPractitioner.
6. Per-method unit tests + end-to-end happy-path tests + rejection-path tests + idempotency-replay tests + governor-limit test (5-Rx new-patient new-prescriber).

**Exit gates:**

- All tests assert outcomes (no coverage games per `.claude/rules/apex.md`).
- Code Analyzer clean.
- `with sharing` decision documented inline in class header.
- No hardcoded RT IDs / Group IDs / User IDs / Pricebook2 IDs anywhere in the service.
- **Sandbox-verified that Health Cloud auto-Contact-create works under `with sharing`** when invoked as the Azure Integration User. **If this test fails, Phase A does not ship.** Team picks between widening the integration user's perm set OR flipping the relevant Account-insert step to `without sharing` (changes §6 risk #1 disposition before Phase A re-attempts the gate).

**Dependencies:** none. Can start immediately.

### Phase B — Refactor the screen flow to call the service

**Scope:** `New_Patient_New_Order_New_Rx_New_Case_Launch_from_data_case` keeps its user-facing screens; orchestration moves to Apex via `PatientOrderIntakeInvocable`. Same DE-team UX; cleaner backend.

**Deliverables:**

1. `PatientOrderIntakeInvocable.cls` (`@InvocableMethod` wrapper).
2. Refactored flow (~150 nodes → ~30-40 nodes, mostly screens + the assignment nodes that build the IntakeRequest variable + one Action call).
3. The 5 `c:CommitTransaction` ceremony nodes removed (Apex handles atomicity).
4. Equivalence tests: refactored flow produces the same record graph as the original on representative fixtures.
5. Refactor `New_Prescriptions_and_New_Order_Case` (alternative hand-entry path) to call the same invocable.

**Exit gates:** equivalence verified end-to-end against ClaudeTest; DE-team smoke test pass; original flow XML archived (status=Obsolete) but on-disk for one release cycle as rollback.

**Dependencies:** Phase A complete.

### Phase C — Build the Azure REST endpoint

**Scope:** `PatientOrderIntakeRest` `@RestResource` mapping Azure JSON → `IntakeRequest`.

**Deliverables:**

1. `PatientOrderIntakeRest.cls` at URL `/services/apexrest/intake/v1/escript`.
2. JSON contract documented in this plan + class header.
3. `Azure_Integration` permission set expanded with Apex Class Access on the REST class + the underlying service + Object/Field permissions on every object the service writes.
4. HTTP status code mapping: validation errors → 400; idempotency replay → 200 with warnings; internal failures → 500 with `Error_Log__c.Id` in body.
5. Apex tests using `RestRequest` / `RestResponse` against the endpoint.
6. Postman / curl smoke test from outside Salesforce.

**Exit gates:** standalone external HTTP test succeeds; Azure-side smoke test (Logic App POSTs a hand-built payload) returns expected response.

**Dependencies:** Phase A complete. Independent of Phase B (can run concurrently).

### Phase D — Per-channel cutover

**Scope:** Azure-side switches over to the new endpoint, gated by a CMDT feature flag, starting narrow.

**Deliverables:**

1. `Intake_Pipeline_Routing__mdt` (new) — per-channel boolean: `eScript`, `fax`, `manualUpload`, etc. True = route to structured-write endpoint; false = fall back to today's image-staging path.
2. `wf-sf-writer` (Azure side) extension — when source channel's flag is true AND payload contains structured fields, POST to the new endpoint instead of writing the 3-record staging set.
3. Initial cutover: e-scripts only (faxes stay on existing path until the new path proves stable).
4. Confidence-threshold gating: even within e-script, only payloads above the configured Azure-AI confidence threshold use the structured path. Below threshold → image-staging fallback. Threshold tunes post-launch.

**Exit gates:** parallel-run period in production with a small slice of e-script intake (initial slice TBD, start narrow, widen as comfort grows); error rate (Apex rejections + records that bounce at PV1) below the bar set during the parallel-run window.

**Dependencies:** Phase C complete. Phase B not strictly required, but recommended so DE team isn't running two divergent paths during the parallel-run window.

### Phase E — Cleanup + consolidation

**Scope:** retire what the new service replaces.

**Deliverables:**

1. Deactivate the obsolete `New_Prescriptions_and_New_Order_Case` flow once Phase B refactor is proven.
2. Confirm `NewLeaf_ObjectStructure.cls` is on the NewLeaf cleanup list (separate concern; out of scope here).
3. Move the original (pre-refactor) screen flow XML to archive after one release cycle.
4. Audit any remaining hardcoded RT IDs / Group IDs / Pricebook IDs in this domain. Replace with DeveloperName lookups.
5. Update `business-context/integrations/azure-integration.md` to reflect Phase 3 live.
6. Add `PatientOrderIntakeService` to `CLAUDE-PROPOSED.md` § "Apex pattern exemplars" as the canonical multi-record-graph + savepoint-rollback exemplar.

**Dependencies:** Phase D in production for one release cycle.

## §6 — Risks + constraints

1. **Sharing keyword decision (load-bearing).** The screen flow runs `SystemModeWithoutSharing`. The new service should default to `with sharing` per `.claude/rules/apex.md`. **But:** Health Cloud's managed-package code that auto-creates the Contact on Account insert may rely on system-mode behavior. Phase A includes an explicit sandbox test of "Account insert from `with sharing` Apex running as the integration user → Contact auto-created within the same transaction?". If the auto-create fails under user mode, three options: widen the integration user's profile permissions, flip the relevant Account-insert step to `without sharing`, or abandon the auto-create path and create the Contact explicitly (changes the contract).

2. **OrderItem `Prevent_Manual_Edits` VR — must be bypassed on every update.** The service's `applyPricing()` updates OrderItems with computed prices. Each update MUST set `Validation_Bypass_Date_Time__c = Datetime.now()`. Without this, every OrderItem update fails with a VR error. This is the most consequential single technical fact for Phase A authoring — document it in the service contract and enforce it in helper methods.

3. **Auto-quarantine override on new-order Case.** When the patient has any open Case with RT=`Quarantined_Order` OR RT=`Refill_Request_Case`, the canonical `Case_Insert_Update_Same_Record` BeforeSave flow silently overrides the new Case's RT (→ `Quarantined_Order`) and OwnerId (→ Advanced Rx Admin user). Gated by `ISNEW=true`, so only Inserts are affected (the staging-Case Update is safe). **By design, not a bug. Mitigation:** the service post-queries the Case after insert and reports the actual landed RT + OwnerType in `IntakeResponse.actualCaseRecordType` / `actualCaseOwnerType` / `wasQuarantined`, plus a `warnings[]` entry with `code='AUTO_QUARANTINE_APPLIED'`. See §4 New-order Case for contract detail.

4. **Contact phone-format VRs (5 active).** `Primary_Phone_10_Characters`, `Mobile_Phone_10_Character_Limit`, `Home_Phone_10_Characters`, `Other_Phone_10_Character_Limit`, `Secondary_Phone_10_Characters`. All require exactly 10 digits. Service must format/validate before insert/update. Phase A delivers a `PhoneFormatter` helper.

5. **Multi-relationship integrity on Rx.** Rx has 3 parallel patient-side relationships: `HealthCloudGA__Patient__c` (EhrPatient), `HealthCloudGA__Account__c` (Account), `Contact__c` (Contact). All three must be populated from the same patient-resolution result. If they drift, the data is silently inconsistent — none of the Health-Cloud-managed integrity helps because these are AdvancedRx-added custom Lookups.

6. **Phase 3 perm-set blast radius.** The new endpoint expands what `azure-integration@advancedrx.net` can write — full CRUD on Account + Contact + ContactPointAddress + EhrPractitioner + Order + OrderItem + Rx + Pharmacy_Note\_\_c + Case-update + Rx_Image-update. Open architectural decision: accept consolidation on the existing Connected App + perm set (continues Phases 0/1/2 trajectory), or split a separate Connected App + perm set for Phase 3 writes only (isolates blast radius at rotation-coordination cost). **Decide before Phase D.**

7. **Health Cloud upgrade fragility.** Service writes to managed `HealthCloudGA__*` objects directly. Future package upgrades may rename managed fields and silently break the service. Pin a Health Cloud version in the test plan; rerun service tests after every Health Cloud managed-package upgrade.

8. **Governor-limit math.** 5 Rxes × 4 unconditional Rx-triggered flows = 20 flow interviews per service call. Plus the canonical Same/Related pair on Account, Contact, EhrPatient, Order, Case, OrderItem, ContactPointAddress, Rx_Image. CPU + SOQL + DML totals add up. Phase A's 5-Rx end-to-end test must verify governor-limit headroom; if too tight, options are batching (split DMLs across smaller chunks within one transaction) or async (move pricing into a Queueable post-commit).

9. **DE-team workflow regression risk.** The screen flow is load-bearing for daily DE-team operations. Phase B preserves the UX surface but rewires orchestration. Subtle behavior changes (commit timing relative to user input, error message text, ordering of side-effect flows) could surface as regressions. Mitigated by Phase B's equivalence tests + DE-team smoke test before exit.

10. **`B2BProductPricingService` divide-by-zero latent bug.** If all Rxes are STANDARD-category but none have `Product__r.Count_Towards_Total_Qty__c=true`, the existing pricing path throws `MathException` ("Divide by zero") at `B2BProductPricingService.cls:85`. Service must defend (pre-count contributors before calling). The latent bug exists in the screen-flow path too; cleanup is tracked separately at [advancedrx-salesforce#57](https://github.com/AdvancedRxPharmacy/advancedrx-salesforce/issues/57). See also §10.

11. **`B2BProductPricingService` 6-slot constraint.** The pricing service is hardcoded to 6 inputs. Service intake supports up to 5 today (matches the screen flow's cap). If we ever raise the cap, either (a) batch the pricing call 6-at-a-time, or (b) extend the pricing service with a list-shaped helper. Not blocking.

12. **Hardcoded IDs to retrofit (anti-pattern catalog).** The screen flow + downstream `New_Prescriptions_Manually_Entered` flow together contain:
    - **5 hardcoded RT IDs** — Account-Individual, Case-CustomerServiceNewOrder, Rx-Prescription, Rx-Fill, Pharmacy_Note-CustomerService.
    - **1 hardcoded Group ID** — `CS_Case` queue, stored in a `<constants>` block in the screen flow.
    - **1 hardcoded Pricebook2 ID** — `01s4W000005JCZUQA4` (Standard Price Book), in `New_Prescriptions_Manually_Entered.PricebookEntry` create. Survives same-parent sandbox refresh; breaks on cross-org migration.

    The new service uses `RecordType.DeveloperName` / `Group.DeveloperName` / `Pricebook2 WHERE Name='Standard Price Book'` lookups exclusively. Phase A and Phase B remove all of these from any new code; Phase E may opportunistically clean up the still-existing hardcodes in untouched paths.

13. **ClaudeTest sandbox data void.** Per `CLAUDE.md` §1, ClaudeTest carries metadata + reference data but no transactional records. Tests must seed everything (Account, Contact, EhrPatient, Product, Pricebook, SalesChannel `'AdvancedRx'`, EhrPractitioner). `IntakeTestFactory` + a committed `scripts/seed-intake-test-data.apex` handle this in Phase A.

14. **Logic Apps `Scope_Catch_Failure` triggers on HTTP error status codes only.** This is the structural reason behind the §2 HTTP-status-code locked decision and why we can't simplify the service to "always return 200 with errors[] in the body." Logic Apps treats any 200 response as success regardless of body content, so a 200-with-errors response would cause `wf-sf-writer` to write `Status='Success'` to SQL — silently losing every validation failure. **Future readers evaluating "can we simplify to 200-with-errors?": no, not without modifying `wf-sf-writer.Scope_Catch_Failure` to inspect the response body.** That modification is more invasive than owning HTTP status codes service-side. Reference: `business-context/integrations/azure-integration.md` "Failure handling" section.

15. **Staging-Case OwnerId reassignment risk.** The screen flow today reassigns the staging-Case OwnerId to the running user as part of closing it. The service-side path (per §4 Staging-Case finalization) **deliberately leaves OwnerId unchanged** — original owner is the integration user, reassignment isn't load-bearing for a closed Case, and it avoids tripping the `Restrict_Case_Ownership_Changes` VR (the one active VR on Case). If a later requirement surfaces that the staging-Case owner SHOULD change, verify the VR doesn't block before adding the update.

16. **`Prescription_Id__c` vs `Patient_Fill_Id__c` are different fields.** Two separate hash fields, on two different records, populated at two different times by `HashUtils` with different recordIds:
    - **`Prescription_Id__c`** — on the Master Rx record (RT=Prescription). Populated by `New_Prescriptions_Manually_Entered.Generate_Prescription_ID` post-Master-Rx-insert.
    - **`Patient_Fill_Id__c`** — on the Fill record (RT=Fill, same SObject, different RT). Populated separately; ~10 Apex classes reference it for cross-system correlation.

    Service must not conflate them. The downstream flow handles both; service just sets `SF_Manually_Entered__c=true` and lets the flow run. But any code reading these fields must query the right record + right field.

17. **`NL_Prescription__c` is a Fill→Master Rx FK despite the NewLeaf-suggesting name.** On the Fill record, the `NL_Prescription__c` Lookup points back to the Master Rx. Name suggests legacy NewLeaf cluster (which is being deleted) but the field is in active use as the parent-pointer. Don't try to drop based on naming. Service doesn't write this directly (downstream flow does), but engineers reading `EhrMedicationPrescription.md` should know.

## §7 — Migration + cutover strategy

**Two cutovers run simultaneously after Phase A:**

1. **DE team's screen flow** (Phase B) goes from "flow does everything" to "flow calls service." UI is identical; only orchestration changes. Flow stays active throughout — no users are migrated to a different UI.
2. **Azure intake** (Phase D) goes from "image-staging only, DE transcribes" to "structured write for high-confidence cases." DE workload shrinks for auto-handled cases; staging-Case + screen-flow path remains as fallback for cases the new path doesn't handle (low Azure-AI confidence, parser failure, validation rejection).

### Why parallel-run, not big-bang

Three independent things can go wrong: (a) service bugs (bad field mapping, missed RT, wrong sharing); (b) Azure-payload bugs (wrong field name, missing required field); (c) process surprise (DE team finds a workflow assumption the new path violates). Big-bang cutover hides all three. Parallel-run isolates them: feature flag tells Azure to use the new path for a small slice; everything else flows the old way. Errors land as `Error_Log__c` rows + structured rejections; volume stays small enough to triage each one.

### Feature-flag matrix (Phase D)

```
                       | Use new endpoint? | Confidence gate
e-script (high conf.)  | true              | confidence >= X
e-script (low conf.)   | false             | always staging
e-script (no Phase 1)  | false             | always staging
fax (parsed)           | false (initially) | always staging
manual upload          | false (always)    | always staging
```

Each row is a `Intake_Pipeline_Routing__mdt` row. Rollback = flip a row to `false`; no code deploy needed.

### Rollback plan

| Failure                           | Recovery                                                                                                                                                                       |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Service-level bug                 | Flip the relevant CMDT row to `false`. Azure falls back to staging-only writes. No deploy.                                                                                     |
| Catastrophic service failure      | `wf-sf-writer`'s existing failure handling (Scope_Catch_Failure → DLQ → SQL status update) handles the rejection gracefully; Azure operates on stale-but-correct staging data. |
| Phase B regression in screen flow | Revert `<status>` flip on the original flow XML. Flow framework supports flow-version reactivation.                                                                            |
| Phase E executed prematurely      | Don't. Phase E does NOT run until Phase D has been stable for ≥1 release cycle. The fallback paths must remain available during cutover.                                       |

## §8 — Test strategy

### Test factory + seed data

`IntakeTestFactory.cls` produces:

- `IntakeRequest` shapes for representative cases: 1-Rx new-patient, 1-Rx existing-patient, 5-Rx new-patient new-prescriber, multi-Rx-same-day (parented to existing New-Order Case), idempotency replay, address-validation-fail, missing-required-field, partial-state-replay.
- Seed methods for `Product2` + `Custom_Product_Price__c` rows so pricing exercises real logic.
- Seed methods for `SalesChannel` (`SalesChannelName='AdvancedRx'`) + `Pricebook2` (Standard Price Book) defaults.
- Helper assertions for "the record graph this IntakeRequest produces matches the expected linkage."

A committed `scripts/seed-intake-test-data.apex` produces the minimum reference data in ClaudeTest sandbox before tests can run.

### Test categories

| Category                          | Phase | Coverage                                                                                   |
| --------------------------------- | ----- | ------------------------------------------------------------------------------------------ |
| Per-helper-method unit tests      | A     | Each resolver / builder / linker in isolation                                              |
| Service end-to-end happy-path     | A     | All representative shapes go through `intake()` and produce expected graphs                |
| Service end-to-end rejection-path | A     | Each error class produces correct response + `Error_Log__c` row + clean rollback           |
| Idempotency tests                 | A     | Duplicate-key replay returns existing IDs; partial-state replay rejects                    |
| Bulk / governor tests             | A     | 5-Rx case stays under SOQL/CPU limits with all downstream automation firing                |
| Sharing-mode test                 | A     | Auto-Contact-create works under `with sharing` running as the integration user             |
| Invocable wrapper tests           | B     | `@InvocableMethod` shape correctly translates flow inputs                                  |
| REST wrapper tests                | C     | `@RestResource` correctly translates JSON → IntakeRequest and IntakeResponse → HTTP body   |
| Equivalence tests                 | B     | Refactored flow produces same graph as original on representative fixtures                 |
| External HTTP smoke               | C     | Postman / curl from outside Salesforce                                                     |
| Production parallel-run audit     | D     | Daily query: structured-write Cases vs. staging-only Cases — fields populated equivalently |

### Coverage discipline

Per `.claude/rules/apex.md`: every test method asserts at least one outcome. No `increaseCoverage()` no-ops. The 75% deploy coverage gate is treated as a floor; behavior coverage is what we actually measure.

## §9 — Open decisions (require human input)

| #   | Question                                                                                                                                                                                                | Blocks                                                  | Owner          |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -------------- |
| 1   | Phase 3 perm-set blast radius (§6 risk #6) — accept consolidation on the existing Connected App + perm set, or split a separate Connected App for Phase 3 write workloads?                              | Phase D                                                 | Kyle           |
| 2   | Fax intake — does Azure parse faxes into the same JSON contract as e-scripts, or do faxes use a different contract / channel?                                                                           | Phase D fax row                                         | Kyle + Marissa |
| 3   | Confidence-threshold tuning — at what Azure-AI confidence floor does the structured-write path stop being worth it (records bounce at PV1 too often, parser ambiguity outweighs DE-team labor savings)? | Tunable post-launch via the CMDT, not a Phase D blocker | Kyle           |

## §10 — Out of scope (this version)

Explicitly NOT changing in this project:

- Patient-clinical capture (allergies, sinus meds, special-counseling-requirements, clinical conditions). Stays on the V3 CSA call-flow chain + Contact-page WebLink path per `business-context/workflows/patient-clinical-intake-and-counseling.md`.
- Pharmacist verification (PV1) flow. Unchanged.
- B2B portal new-order paths (`Portal_New_Order_Create_Rx_Cart_Items`, `B2B_Cart_To_Order`). Different domain model (CartItem-driven), different lifecycle.
- Existing-Order image upload (`Image_Uploaded_to_Existing_Order`). Existing-Order use case, not new-Order.
- Refill / Fill lifecycle automation (`Fill_Decrementing`, `Automation_on_New_Refills`). Could converge later.
- DE-team UI itself. Phase B preserves all screens.
- LifeFile UI scrape on the Azure side. Upstream behavior.
- `wf-sf-writer` itself, except the routing extension that decides which payloads go to the new endpoint.
- "Prescriber not in roster" handling. Service rejects with `PRESCRIBER_NOT_IN_ROSTER` (HTTP 400) in v1; auto-create or prescriber-onboarding flow is a future phase.
- **`B2BProductPricingService` divide-by-zero cleanup** (`B2BProductPricingService.cls:85`). Latent bug — `MathException` when caller passes all-STANDARD-category Rxes with zero contributing quantity. Service-side defense lives in `applyPricing()` (§6 risk #10). The underlying defect cleanup is tracked separately at [advancedrx-salesforce#57](https://github.com/AdvancedRxPharmacy/advancedrx-salesforce/issues/57); not a blocker for any phase of this plan.

## §11 — Cross-references

- [`phase-a0-discovery.md`](phase-a0-discovery.md) — discovery audit trail; full extraction source for §4 contract + §6 risks.
- `business-context/integrations/azure-integration.md` — phased roadmap (this plan = Phase 3); inbound auth chain; failure-handling shape; cross-system field-name drift hazard.
- `business-context/objects/Case.md` — Case RT inventory; auto-quarantine pattern; 12-flow trigger collision context.
- `business-context/objects/Order.md` — Order field surface; OrderItem creation path; quarantine bypass field.
- `business-context/objects/OrderItem.md` — `Validation_Bypass_Date_Time__c` requirement; full create/update contract.
- `business-context/objects/EhrMedicationPrescription.md` — Master Rx + Fill RT; multi-relationship hazard; post-create denormalization.
- `business-context/objects/EhrPatient.md` — the third leg of the patient triangle; GivenName1/2/3 mapping.
- `business-context/objects/Pharmacy_Note__c.md` — 2-RT structure; 3 transcription-path Notes.
- `business-context/objects/Rx_Image.md` — staging record; service-side back-link target.
- `business-context/object-purpose-map.md` — Account → Contact → EhrPatient triangle; standard-object usage.
- `force-app/main/default/classes/B2BProductPricingService.cls` — pricing logic the service calls.
- `force-app/main/default/classes/HashUtils.cls` — `Patient_Fill_Id__c` / `Prescription_Id__c` generator.
- `.claude/rules/apex.md` — sharing keyword, FLS, no caller-supplied SOQL, no hardcoded IDs, async error handling, test assertions.
- `.claude/rules/flow.md` — `RecordType.DeveloperName`, fault connectors, no hardcoded IDs.
- `CLAUDE.md` §9 — Health Cloud upgrade fragility, label drift, hardcoded-ID anti-pattern catalog.
