# Lead Inspection Cases — Business Specification

This document translates every Lead-related Staging Inspection case into plain
business language, derived **only** from each case's actual request payload and
assertions in `staging-inspection/config`. No behaviour is inferred beyond what
the case literally sends and checks.

Purpose: let a business reviewer confirm, case by case, that what each case
*verifies* matches the intended Lead business rules. Where a case asserts a
specific value (an owner profile, a region, a converted flag), the reviewer
should confirm that value is the genuinely expected outcome — not just what the
system happens to return today.

## Scope

20 functional cases + 4 performance cases across these runner locations:

| Role | Module | Functional cases |
| --- | --- | --- |
| admin | crm-core | 2 (smoke: create, list) |
| admin | lead-management-regression | 8 |
| admin | lead-conversion | 9 |
| sales-apac | lead-conversion | 1 |

Each case below lists: **what it sends**, **what it asserts**, and a
**Reviewer check** — the business question the assertion implicitly answers.

---

## A. admin / crm-core — smoke

### A1. `lead-create-api` — Lead create API succeeds and is readable
- **Sends:** `POST /sobjects/Lead` with the fixture `payloads.leadCreate`
  (`LastName="Staging Inspection Lead"`, `Company="Staging Inspection Co"`),
  then reads the new record back.
- **Asserts:** create returns HTTP 2xx; readback returns HTTP 200.
- **Reviewer check:** A minimal Lead (last name + company only) can be created
  and retrieved. Pure plumbing smoke — no business rule.

### A2. `lead-list` — Lead list opens
- **Sends:** browser navigation to `/object/Lead`.
- **Asserts:** page shows one of "New Lead" / "Create Lead" / "No records
  found"; must **not** show "Error Loading Object" or "Request failed with
  status 504".
- **Reviewer check:** The Lead list view renders. UI smoke — no business rule.

---

## B. admin / lead-management-regression

### B1. `phase3-flow-active-preflight-api` — required Lead flows are active
- **Sends:** SOQL on `FlowDefinition` for 8 named flows
  (`Lead_SetCountryCodeFromCountry`, `Lead_Status_Duration`,
  `IQM_Completion_Flow`, `Country_is_empty`,
  `Lead_Assignment_Rule_modification_for_NA_EMEA`, `Lead_Quick_Set_Region`,
  `Lead_Set_First_And_Last_CampaignMember`, `LeadQuicklySet`) that have a
  non-null `ActiveVersionId`.
- **Asserts:** exactly 8 are returned and each has an active version.
- **Reviewer check:** All 8 Lead automation flows are deployed **and active**
  before any behavioural case runs. Confirm this is the correct, complete list
  of flows that must be live.

### B2. `lead-before-save-field-matrix-api` — status-duration + quick defaults
- **Sends:**
  1. Create Lead (`Status="OPEN"`), read back.
  2. PATCH the Lead to `Status="Working"`, read back.
  3. Create a second Lead with `Status="OPEN"` and `Portal_ID__c="portal-<run>"`,
     read back.
- **Asserts:**
  - After create: `Status="OPEN"`, `Date_Status_Changed__c` is populated.
  - After PATCH: `Status="Working"`, `Date_Status_Changed__c` populated,
    `Duration__c` populated.
  - Second Lead: `Portal_ID__c="portal-<run>"`, `Account_Status__c="new_user"`.
- **Reviewer check:** On create the status-change date is stamped; on status
  change the duration is computed; a Lead carrying a Portal ID is defaulted to
  `Account_Status__c="new_user"`. Confirm `new_user` is the expected default
  for a portal-originated Lead.

### B3. `lead-assignment-fixture-readiness-api` — assignment preconditions
- **Sends:** three SOQL probes — the `Lead_Assignment_Rule_modification_for_NA_EMEA`
  flow is active; an **active** Account RecordType `Customer` exists; the
  configured sales fixture user exists and is **active**.
- **Asserts:** each probe returns exactly the expected active record.
- **Reviewer check:** Guard/pre-flight only — ensures the assignment case below
  runs against valid fixtures (active flow, valid Customer RecordType, active
  Sales user). No business rule of its own.

### B4. `lead-assignment-account-owner-branch-api` — unsigned Sales Account → regional SDR + AE stamp
> Linked: #2437 (umbrella regression report, **not** a routing spec — see note).
> **This case currently fails on `Owner_Profile__c`.**

- **Sends:**
  1. Find the active Sales fixture user.
  2. Create an Account whose **OwnerId is that Sales user**, `Region__c="APAC"`,
     RecordType `Customer`, and **`AccountStatus__c="Prospecting"`** (an explicit
     *non*-onboarded status, so the case unambiguously targets the mid tier).
  3. Create a Lead whose `Company` **exactly equals** that Account's Name and
     whose `Region__c="APAC"`, `Status="OPEN"`.
  4. Read the Lead back.
- **Asserts:** `Region__c="APAC"`; `OwnerId` exists; `OwnerId` is **not** the
  Sales user; `Owner_Profile__c="SDR"`; `AE__c` = the Sales user Id.
- **Reviewer check (the decisive one):** When a new Lead matches — by Company
  name + region — an existing **Sales-owned but not-yet-onboarded** Account, the
  rule under test is: **do not leave the Lead with the Account's Sales owner;
  reassign it to a regional SDR (`Owner_Profile__c="SDR"`) and record the
  original Account owner in `AE__c`.** This is the **mid tier** of the flow's
  three-tier routing (see "How the flow decides" below). Business must confirm
  this mid-tier routing is intended.
- **Note on the failure:** `Owner_Profile__c` is a formula field
  (`Owner:User.Profile.Name`) on a polymorphic owner; it currently evaluates to
  null in the platform engine, so the `="SDR"` assertion fails. That is a
  platform-engine issue, separate from whether the *business expectation* above
  is correct.
- **Note (linkage):** #2437 is the **"Phase3 Flow Regression Report — 8 Flows,
  82 TCs"** umbrella report (it also tags B1/B2/B3 in the source JSON), **not** a
  spec that defines the "matched Sales Account → SDR + AE" routing rule. The
  expected value `Owner_Profile__c="SDR"` is therefore **derived from the flow
  definition, not from a signed-off business spec** — treat it as unconfirmed
  until the assignment-tier rule is pinned to an issue/spec.

### B4b. `lead-assignment-onboarded-account-direct-sales-api` — onboarded Account → direct to Sales owner
> Linked: #2437 (umbrella regression report, not a routing spec). New case
> covering the **top tier** missing from prior coverage; expected value is
> derived from the flow definition, pending business sign-off.

- **Sends:**
  1. Find the active Sales fixture user.
  2. Create an Account owned by that Sales user, `Region__c="APAC"`, RecordType
     `Customer`, and **`AccountStatus__c="Onboarded"`** (a signed/onboarded
     status).
  3. Create a Lead whose `Company` exactly equals that Account's Name,
     `Region__c="APAC"`, `Status="OPEN"`.
  4. Read the Lead back.
- **Asserts:** `Region__c="APAC"`; `OwnerId` **equals** the Sales user Id (the
  Lead stays with the Account's Sales owner — no SDR reassignment).
- **Reviewer check:** When a Lead matches a Sales-owned Account that **is**
  onboarded (`AccountStatus__c ∈ {Onboarded, Growth}`), the Lead goes
  **directly to that Sales owner**, bypassing the SDR round-robin. Business must
  confirm onboarded/Growth is the correct "signed customer" marker for the top
  tier.

### B5. `lead-management-api-regression` — describe hygiene, name, country→region
> Linked: #758, #1571, #2226.

- **Sends:** Lead `describe`; query a `Countries_Region__c` seed row; query the
  matching `Country_Dictionary__c` full name; query `Lead WHERE Region__c = null`;
  create a Lead (`FirstName="QAR9"`, `Country=<seed full name>`); read it back.
- **Asserts:** describe payload contains no raw HTML entities
  (`&amp; &lt; &gt; &quot; &#`); seed objects return usable rows; null-region
  query executes; created Lead `Name` includes both `"QAR9"` and
  `"StagingInspection<run>"`; `CountryCode` populated; `Region__c` populated.
- **Reviewer check:** Lead `Name` is synthesised from first+last name; setting a
  country drives `CountryCode` and `Region__c`; picklist metadata is not
  HTML-escaped. Confirm country→region derivation is expected on create.

### B6. `lead-lookup-search-api` — name search returns the Lead
> Linked: #763, #2027.

- **Sends:** create a Lead, then SOSL `FIND {StagingInspection<run>} IN NAME
  FIELDS RETURNING Lead(...)`.
- **Asserts:** at least one search record returned.
- **Reviewer check:** A freshly created Lead is findable by name search. Search
  smoke — no business rule.

### B7. `lead-layout-describe-api` — layout describe responds
> Linked: #167.

- **Sends:** `GET /sobjects/Lead/describe/layouts`.
- **Asserts:** response body exists.
- **Reviewer check:** Layout-editor describe endpoint responds. API smoke — no
  business rule.

---

## C. admin / lead-conversion

### C1. `lead-convert-rollback-owner-ocr-api` — failed convert rolls back; owner + primary OCR
> Linked: #757.

- **Sends:**
  1. Create a Lead; attempt `convertLead` with `accountId="INVALID_ACCT_ID_ROLLBACK"`,
     `doNotCreateOpportunity=true`.
  2. Query for any orphan Contact by the Lead's email.
  3. Create a valid Account; create a second Lead; read its OwnerId.
  4. `convertLead` into the valid Account (`convertedStatus="Customer"`,
     creates an Opportunity).
  5. Read the converted Contact; query the Opportunity's contact roles.
- **Asserts:** invalid convert returns `isSuccess=false`; **no** orphan Contact
  exists (`totalSize=0`); valid convert returns `isSuccess=true` with a
  contactId and opportunityId; converted Contact's `OwnerId` equals the Lead's
  pre-convert owner; the Opportunity has exactly one primary contact role and it
  is the converted Contact.
- **Reviewer check:** A failed conversion leaves no partial Contact behind
  (atomic rollback); a successful conversion (a) makes the Contact inherit the
  Lead's owner and (b) creates exactly one **primary** OpportunityContactRole
  for that Contact. Confirm owner inheritance + single-primary-OCR are expected.

### C2. `lead-convert-converted-state-api` — converted fields set; converted Lead locked
> Linked: #757.

- **Sends:** create Account + Lead; `convertLead`; read the Lead; then PATCH the
  converted Lead `Status`.
- **Asserts:** `IsConverted=true`; `ConvertedAccountId`, `ConvertedContactId`,
  `ConvertedOpportunityId` all populated. (The trailing PATCH exercises edit
  behaviour on a converted Lead.)
- **Reviewer check:** Conversion stamps all three converted-reference fields and
  flips `IsConverted`. Confirm a converted Lead should expose these IDs.

### C3. `lead-convert-before-save-field-set-api` — ConvertedOpportunityId visible to before-save flow
> Linked: #2207, #2214.

- **Sends:** create Account + Lead; `convertLead` (creates Opportunity named
  "Staging Inspection Convert Field Set Opportunity <run>"); query the converted
  Lead's `ConvertedOpportunityId`, `ConvertedOpportunity.Name`,
  `Opportunity_Name_New__c`, `Opportunity_Number__c`.
- **Asserts:** `ConvertedOpportunityId` equals the new Opportunity; its Name
  matches; `Opportunity_Name_New__c` and `Opportunity_Number__c` are populated.
- **Reviewer check:** During conversion the Opportunity link is available early
  enough that the before-save field-set flow can copy Opportunity name/number
  onto the Lead. Confirm these derived fields are expected to be populated.

### C4. `convertleads-profile-permission-api` — Sales & SDR have ConvertLeads
> Linked: #761.

- **Sends:** SOQL on `Profile` for the Sales profile and the SDR profile,
  selecting `PermissionsConvertLeads`.
- **Asserts:** each query returns exactly one profile.
- **Reviewer check:** Both the Sales and SDR profiles exist (presence check).
  Confirm these are the two profiles expected to convert Leads. (Note: only
  presence is asserted, not the permission flag value.)

### C5. `lead-convert-campaignmember-relink-api` — CampaignMember relinks; CampaignId propagates
> Linked: #2664.

- **Sends:** create Campaign; create Lead; create a CampaignMember linking the
  Lead; create Account; `convertLead`; then query the CampaignMember, the
  Opportunity, the Contact, and the Campaign rollup.
- **Asserts:** the CampaignMember now has `LeadId=null` and
  `ContactId=<converted Contact>` (relinked from Lead to Contact), keeping its
  CampaignId; the Opportunity's `CampaignId` equals that Campaign; the Contact's
  `First_Campaign__c` and `Last_Campaign__c` both equal that Campaign; the
  Campaign exposes `NumberOfConvertedLeads`.
- **Reviewer check:** On conversion, a Lead's CampaignMember is re-pointed to the
  new Contact (not duplicated/dropped), the source Campaign is stamped onto the
  Opportunity, and first/last-campaign attribution carries to the Contact.
  Confirm this campaign-attribution carry-over is the intended behaviour.

### C6. `lead-convert-campaignmember-lifecycle-exclusion-api` — exclude lifecycle campaign from attribution
> Linked: #2683 (perf scope only — see note). Business rule source for the
> *correctness* assertion is not yet pinned to an issue.

- **Sends:** create a **normal** Campaign and a **lifecycle** Campaign; create a
  Lead; add the Lead to both as CampaignMembers; query the Lead's
  first/last-campaign attribution before convert; create Account; `convertLead`;
  query the converted Contact's attribution.
- **Asserts:** before convert, Lead `First_Campaign__c` and `Last_Campaign__c`
  both equal the **normal** Campaign (the lifecycle Campaign is excluded); after
  convert, the Contact's first/last campaign both still equal the **normal**
  Campaign.
- **Reviewer check:** Lifecycle-type campaign membership must **not** count
  toward first/last-campaign attribution. The case asserts the **resulting
  value** only — it is mechanism-neutral: after conversion the Contact's
  first/last campaign still excludes the lifecycle campaign, **whether that
  value is produced by a recompute path or inherited from the source Lead**.
  Confirm which campaign type(s) are meant to be excluded from attribution.
- **Note (linkage):** #2683 is a **performance** issue. Its acceptance criteria
  explicitly *remove* the convert-time first/last-CampaignMember recompute and
  instead inherit the value from the source Lead via convert field mapping. So
  this correctness assertion must **not** be read as "the convert-time recompute
  excludes lifecycle" — after #2683 lands there is no convert-time recompute.
  C6 should be linked to the lifecycle-exclusion *correctness* source, not only
  to the perf issue #2683.


### C7. `lead-convert-no-campaign-source-api` — no CampaignMember → empty attribution
> Linked: #757, #2664.

- **Sends:** create Lead (no CampaignMember); create Account; `convertLead`;
  query Contact attribution and Opportunity CampaignId.
- **Asserts:** Contact `First_Campaign__c=null`, `Last_Campaign__c=null`;
  Opportunity `CampaignId=null`.
- **Reviewer check:** With no campaign membership, conversion leaves campaign
  attribution and Opportunity CampaignId empty (no spurious stamping). Confirm
  empty is correct when there is no source campaign.

### C8. `lead-convert-create-new-contact-ocr-api` — Create-New-Contact path + primary OCR
> Linked: #2659, #2653.

- **Sends:** create Lead (`FirstName="CreateContact"`); create Account;
  `convertLead` into that Account (`convertedStatus="Customer"`, creates
  Opportunity); read the new Contact; query the OpportunityContactRole; verify
  the Lead.
- **Asserts:** convert succeeds with contactId + opportunityId; Contact
  `FirstName="CreateContact"`, `LastName` contains "StagingInspection",
  `AccountId` equals the created Account; exactly one OCR with `IsPrimary=true`;
  Lead `IsConverted=true` with all three converted IDs matching.
- **Reviewer check:** The "create a new Contact" conversion path produces a
  Contact under the chosen Account and exactly one primary OCR. Confirm the
  new-contact path is expected to yield a single primary OCR.

### C9. `lead-convert-create-contact-scope-api` (inline) — Sales-owned Lead converts via standard action
> Linked: #2653, #2659.

- **Sends:** find the Sales fixture user; find an Account with
  `ApprovalStatus__c="Approved"`; create a Lead **owned by the Sales user**;
  `convertLead`; query the converted Lead and
  the OpportunityContactRole.
- **Asserts:** convert succeeds with a contactId; Lead `IsConverted=true` and
  `ConvertedContactId` matches; exactly one OCR with `IsPrimary=true` for the
  converted Contact.
- **Reviewer check:** A Sales-owned Lead can be converted through the standard
  convert action (internal scope) and yields a single primary OCR. Confirm the
  approved-Account precondition is intended for this path.

---

## D. sales-apac / lead-conversion

### D1. `lead-convert-create-contact-apac-sales-api` — APAC Sales converts a fixed Lead
> Linked: #2659.

- **Sends:** as the **APAC Sales** user, `convertLead` for a hard-coded Lead
  `00QRC00001A4e2D2AR` into a hard-coded Account `0015g000002hX1yAAE`
  (`convertedStatus="Customer"`, creates Opportunity); query the OCR.
- **Asserts:** convert succeeds with contactId + opportunityId; exactly one OCR
  with `IsPrimary=true`.
- **Reviewer check:** An APAC Sales user (not admin) can run the create-contact
  conversion path end-to-end. Confirm this regression should keep targeting the
  fixed Lead/Account IDs (these are environment-specific and may need refresh).

---

## E. Performance cases (latency, not behaviour)

These assert latency budgets only; they do not verify business rules.

| Case | Role / Module | What it measures |
| --- | --- | --- |
| `lead-convert-campaignmember-latency-api` | sales / performance | End-to-end convert latency with a CampaignMember present. |
| `lead-convert-no-campaign-latency-api` | sales / performance | End-to-end convert latency with no CampaignMember. |
| `lead-count-latency-api` | sales / performance | Lead count query latency. |
| `lead-list-latency-api` | sales / performance | Lead list query latency. |

- **Reviewer check:** Confirm the latency targets (in `runtime-latency-standard.md`)
  match business expectations for convert and list responsiveness.

---

## How the flow decides (assignment routing)

The `Lead_Assignment_Rule_modification_for_NA_EMEA` flow matches a new Lead
against existing Accounts through **two** record lookups, both keyed on
`Account.Name = Lead.Company` (excluding Amazon/Google), same `Region__c`, and
an **active Sales-profile owner** (`Owner_IsActive__c = true`,
`Owner_Profile__c = "Sales"`). They differ by one condition:

| Tier | Lookup | Extra condition | Routing outcome |
| --- | --- | --- | --- |
| **Top** | `get_Onboarded_account` | `AccountStatus__c ∈ {Onboarded, Growth}` | Lead → the Account's **Sales owner** directly |
| **Mid** | `get_account` | (no AccountStatus condition) | Lead → **regional SDR** round-robin; original Sales owner stamped into `AE__c`, `Owner_Profile__c="SDR"` |
| **Base** | none matched | no matching Account | Lead → **regional default** owner by `Region__c` (NA/EMEA/LATAM/APAC/APAC-IN/Japan; Japan also → MKT Pool) |

So the discriminator between "direct to Sales" and "route to SDR" is
**`AccountStatus__c`**: onboarded/growth customers keep the Sales owner;
everything else with a Sales-owned matching Account goes to an SDR.

- **B4b** exercises the **Top** tier (`AccountStatus__c="Onboarded"` → Sales owner).
- **B4** exercises the **Mid** tier (`AccountStatus__c="Prospecting"` → SDR + AE).
- The **Base** tier (no matching Account) is not yet covered by a dedicated case.

## Open question for business review

Two things need business sign-off:

1. **Mid-tier routing (B4):** a Lead matching a Sales-owned Account that is
   **not** onboarded/growth routes to an **SDR**, not the Sales owner, with the
   Sales owner preserved as `AE__c`. Confirm this is intended.
2. **Top-tier marker (B4b):** `AccountStatus__c ∈ {Onboarded, Growth}` is the
   "signed customer" marker that sends the Lead **directly to Sales**. Confirm
   this is the correct/complete set of statuses (e.g. should `Onboarded CP`
   also count?).

Separately, B4 will stay red until the platform formula engine evaluates the
polymorphic `Owner:User.Profile.Name` formula (currently null) — a platform
issue, independent of the business decisions above.
