# Reflect 3 Attempt 03

> MedBridge Sales Ops — CRM Reconciliation & Quote Decision Skill

- Skill: `prism-shadow/reflect-3-attempt-03-10` (Agent Skill)
- Install (CLI): `npx skillmds@latest add prism-shadow/reflect-3-attempt-03-10`
- Raw SKILL.md: https://api.skillmd.com/api/skills/prism-shadow/reflect-3-attempt-03-10/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: prism-shadow (https://skillmd.com/u/prism-shadow)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/prism-shadow/reflect-3-attempt-03-10

---

# MedBridge Sales Ops — CRM Reconciliation & Quote Decision Skill

Reusable workflow rules for producing account-ready JSON answers against the
MedBridge Sales Ops remote API. Covers the two task families: **QUOTE** tasks
(reconcile a customer RFQ/quote → quote decision package) and **RECONCILIATION**
tasks (reconcile a won opportunity → finance-ready reconciliation).

## 0. Environment & API Usage Rules

- The business source of truth is a **remote HTTP JSON API** at `API_BASE_URL`
  (provided by the runner). Never read any local `env/` directory; never start a
  local server. All business records come through `GET`/`POST` on the API.
- **Detail-by-id (`GET /api/<collection>/<id>`) is only available for:**
  `customers`, `products`, `rfqs`, `quotes`, `freight-quotes`, `opportunities`.
  For all other collections (`invoices`, `payments`, `revenue-journals`,
  `events`, `vouchers`, `policies`) use the listing endpoint
  `GET /api/<collection>` or filters `GET /api/<collection>?key=value`.
- **Filtering:** `?key=value` is case-insensitive, matches nested keys, numeric
  tolerance 2dp; multiple filters AND together.
- **Cross-collection search:** `GET /api/search?q=<text>` returns up to 100 hits
  tagged with `collection` and `id`. Useful to locate an entity by name/fragment.
- **Revenue-journals may NOT filter correctly by `customer_id`.** Always filter
  revenue-journals by `opportunity_id` (e.g.
  `/api/revenue-journals?opportunity_id=OPP-TR-XXX`).
- Trust API records over narrative wording when they conflict, but read the
  prompt carefully for which entity (quote/opportunity/RFQ) to reconcile.
- Money values are USD with 2 decimals. Dates are ISO `YYYY-MM-DD`. Use stable
  record IDs exactly as they appear in the API.
- Always pipe JSON through `python3 -m json.tool` to read it. Verify with
  `GET /health` first.

## 1. QUOTE Tasks — Workflow

Applies when the task asks for a quote decision package (EXW pricing, freight
options, route risk, recommended mode, payment terms).

### Step 1 — Identify the quote/RFQ and customer
- Fetch the quote by id (`GET /api/quotes/<id>`) or the RFQ
  (`GET /api/rfqs` then filter). Confirm `customer_id`, `quote_date`,
  `confirmed_quantity`, `product_code`, `incoterm`.
- Fetch the customer (`GET /api/customers/<id>`) for `payment_profile`,
  `segment`, `is_recurring`, `grant_terms`.
- Fetch the product (`GET /api/products/<code>`) for `price_tiers`,
  `shelf_life_months`, `article_number`, `family`, `cold_chain_required`.

### Step 2 — Select the catalog price tier by quantity
- The confirmed quantity selects the tier where `min_qty <= qty <= max_qty`
  (treat `max_qty: null` as +∞).
- **CRITICAL pitfall:** `prior_unit_price_usd` on the quote line must NOT
  override the catalog tier price. Source notes like "catalog tier overrides
  prior unit price" confirm this. The prior price is a distractor.
- Use the selected tier's `unit_price_usd` and `lead_time_days`.
- `shelf_life_months` comes from the **product record**, not the tier.
- Examples: 360 units → 300-499 tier; 1000 units → 900-1199 tier; 700 units →
  500-899 tier (NOT the 900+ tier the source note may bait you toward).

### Step 3 — Compute EXW pricing
- `exw_total_usd` = `confirmed_quantity × tier.unit_price_usd` (round to 2dp).
- `quote_basis`:
  - If the incoterm/RFQ says "EXW plus freight options" → `"EXW"` (freight
    included as separate options).
  - If the RFQ has no destination / says "EXW only" → `"EXW_ONLY"` and
    `freight_excluded: true` (no freight options at all).

### Step 4 — Gather and filter freight quotes
- `GET /api/freight-quotes?quote_id=<quote_id>`.
- **Exclude distractor freight quotes:** records with `id` starting `FR-DIS-`,
  or `destination` = "Distractor route", or clearly wrong shipment size
  ("benchmark" in risk_notes) are distractors. Do NOT include them.
- **Include real freight options even if stale** — a stale/expired real option
  must appear in the output with its stale/invalid status flagged (the account
  manager needs to see it), while distractors are silently dropped.
- For each included freight option, compute
  `grand_total_usd = exw_total_usd + freight_cost_usd`.

### Step 5 — Freight validity and risk fields
- `valid_until`: the freight record's `valid_until` date.
- **Freight validity vs quote date:** a freight option is valid on the quote
  date only if `valid_until >= quote_date` AND `status == "active"`. If
  `valid_until < quote_date` the option is expired; if `status == "stale"` the
  source is stale.
- `all_freight_options_valid_on_quote_date` (train_001-style template): `true`
  only if every included real option is valid on the quote date.
- `source_is_stale` (train_004-style template): `true` when the freight
  record's `status` is `"stale"`.
- `validity_status` (train_004-style): `"VALID"` when active and
  `valid_until >= quote_date`; otherwise the quote is expired/stale — use the
  status that reflects the date check (the quote's `status` field is the
  primary signal).
- `freight_reconfirmation_required`: always `true` for quotes with freight
  options (policy POL-FREIGHT-RECONFIRM: rates need reconfirmation at final
  order, valid only through `valid_until`).

### Step 6 — Route risk enums
Map the API `route_risk` (lowercase `low`/`medium`/`high`) to the template
fields. Two template styles exist:
- train_001 style (separate fields):
  - `risk_level`: `"LOW"` / `"MEDIUM"` / `"HIGH"`
  - `risk_flag`: `"NONE"` (low) / `"MEDIUM_BORDER_RISK"` (medium) /
    high → use the matching border-risk enum.
- train_004 style (single `customs_border_risk`): use the uppercase risk
  level.

### Step 7 — Recommended transport mode
- **Rule: recommend the VALID (non-stale, non-distractor) freight option with
  the LOWEST `grand_total_usd`.**
- Exclude stale/expired/distractor options from contention, but do NOT narrow
  to low-risk only — cost is the deciding factor among valid options.
- If the cheapest valid option is medium-risk, it is still recommended (e.g.
  train_004: SEA at 81200 beats AIR at 97400 even though SEA is medium risk).
- If all real options are valid (train_001): the cheapest overall wins
  (SEA). ✗ Picking AIR "because it's low-risk/cold-chain-safe" is WRONG.

### Step 8 — Payment terms
- New NGO clients (`is_recurring: false`, `status: prospect`,
  `segment: new_ngo`) → `"PREPAY_100"` (policy POL-NEW-CLIENT-PAYMENT).
- Recurring NGO/commercial clients → use the customer's `payment_profile`
  value (e.g. `"NET_30_AFTER_PO"`) per POL-RECURRING-NGO-PAYMENT.
- `offer_validity_days`: `30` (policy POL-QUOTE-VALIDITY — catalog pricing
  valid 30 calendar days from quote date; applies to indicative quotes too).

### Step 9 — Module-level quoting (module RFQs only)
- When `request_type` is `indicative_module_quote` / `module_quote`: keep ONE
  line item per requested module. Do NOT expand into component SKUs.
- `component_composition_distractors` on the RFQ are traps — they list
  components "for medical review only" to lure you into itemized pricing.
- Each module product has its own `price_tiers`, `article_number`,
  `shelf_life_months`; use them directly.
- Also watch for distractor RFQs from the same customer with different
  quantities (e.g. an older budgetary RFQ with a different qty) — use the
  task-specified RFQ ID only.

### Step 10 — WHO documentation flag
- `who_documentation_required: true` for IEHK / `emergency_health_kit` family
  modules (WHO-standardized Interagency Emergency Health Kits). Confirmed:
  setting it `false` **decreases** the judge score.
- Product records may not carry an explicit `who_documentation_required`
  field; infer it from the product `family` / kit standard. Setting this
  `false` for WHO-standard kits is incorrect.

### Step 11 — Client warnings block (train_004-style)
- `road_quote_invalid_or_stale`: `true` when the road freight option is stale,
  expired, or high-risk.
- `freight_warning`: a short warning string describing the stale/expired road
  freight (date + risk). Keep it concise and factual.
- `policy_terms`: `{quote_basis, payment_terms, freight_reconfirmation_required}`
  mirroring the top-level values.

## 2. RECONCILIATION Tasks — Workflow

Applies when the task asks for a finance-ready engagement reconciliation of a
won opportunity (milestones, invoices, payments, revenue journals, events,
vouchers).

### Step 1 — Fetch the opportunity and customer
- `GET /api/opportunities/<id>` for `stage`, `won_amount_usd`,
  `outstanding_amount_usd`, `phases[]`, `contact`.
- `GET /api/customers/<id>` for `name`, `payment_profile`, `contacts`.
- Map `stage`: `closed_won` → `"WON"`, `open` → `"OPEN"`, `closed_lost` →
  `"LOST"`.

### Step 2 — Fetch invoices, payments, revenue-journals
- `GET /api/invoices?opportunity_id=<id>` — one invoice per phase.
- `GET /api/payments?opportunity_id=<id>` — payment records per invoice.
- `GET /api/revenue-journals?opportunity_id=<id>` — **filter by
  `opportunity_id`, NOT `customer_id`** (customer_id filter may silently
  return nothing or wrong results).

### Step 3 — CRITICAL: milestone_id must be MS{n}
- **Use `MS1`, `MS2`, `MS3`, … as `milestone_id`** — `MS` + 1-based phase
  number, ordered ascending.
- ✗ DO NOT use the API `phase_id` (e.g. `HEL-P1`, `MER-P1`) or `invoice_id`
  (e.g. `INV-HELIOS-P1`). Even when a template's description says "stable
  phase or invoice milestone id", the expected value is `MS{n}`.
- **Why it matters:** the evaluator aligns milestone arrays and follow-up-task
  arrays by `milestone_id`. Using the wrong id format causes a **cascade
  failure** — every milestone and task field is scored as mismatched even
  when the values are correct. (Observed: score jumped 0.267 → 0.733 just by
  switching to `MS1`/`MS2`.)
- `phase_number` = 1-based index of the phase.

### Step 4 — Milestone fields
- `amount` / `invoice_total`: the invoice `amount_usd` (= phase `amount_usd`).
- `invoice_state` (train_005 template): map invoice `status` —
  `"paid"` → `"PAID"`, `"unpaid"` → `"OPEN"`.
- `payment_state` (train_005 template): `"PAID"` (fully paid),
  `"UNPAID"` (nothing paid), `"PARTIAL"` (partially paid).
- `payment_status` (train_003 template): `"PAID"` / `"PARTIAL"` / `"UNPAID"`.
- `paid_amount` / `amount_paid`: invoice `paid_amount_usd`.
- `amount_unpaid`: invoice `outstanding_amount_usd`.
- **`due_date`: `null` for PAID milestones; the invoice `due_date` for
  UNPAID milestones.** (Confirmed in train_005 at score 1.0.)

### Step 5 — Revenue-recognition status mapping (per milestone)
- Paid milestone AND a matching revenue-journal exists (match by
  `phase_id`/`invoice_id`) → `"RECOGNIZED"`
  (train_003) / `"RECOGNIZED"` (train_005).
- Paid milestone AND NO revenue-journal found →
  `"REQUIRED_MISSING"` (train_003 template) /
  `"MISSING_REVENUE_JOURNAL"` (train_005 template).
- Unpaid milestone → `"NOT_REQUIRED_UNPAID"` (both templates).
- **Missing-revenue-journal detection:** compare the set of paid milestones
  against the set of revenue-journal `phase_id`s (or `invoice_id`s). Any paid
  milestone with no matching journal is missing recognition. The opportunity
  `notes` field often hints at this ("Phase 2 is paid but revenue recognition
  journal is missing").

### Step 6 — Revenue-recognition summary block (train_003 template)
- `recognition_status`:
  - `"COMPLETE_FOR_PAID_MILESTONES"` if every paid milestone is recognized.
  - `"MISSING_FOR_PAID_MILESTONES"` if any paid milestone lacks a journal.
  - `"NOT_REQUIRED"` if no milestones are paid.
- `recognized_milestones`: list of `MS{n}` ids that are recognized.
- `missing_required_milestones`: list of `MS{n}` ids that are paid but missing
  a journal.
- `recognized_amount`: sum of recognized milestone amounts.

### Step 7 — outstanding_balance computation
- `outstanding_balance` = `won_amount − total_paid_amount`
- = sum of unpaid milestone amounts
- = sum of invoice `outstanding_amount_usd`
- All three must agree; cross-check against the opportunity's
  `outstanding_amount_usd`.
- `total_paid_amount` = sum of all `paid_amount` across milestones.
- `phase_total_amount` = sum of all phase `amount_usd`; must equal
  `won_amount` → `opportunity_matches_phase_total: true`.

### Step 8 — Contact linkage
- Use the opportunity's `contact` field (a name) as the contact.
- train_003 template: `contact: {name, linked_customer_id,
  linked_opportunity_id}`.
- train_005 template: `primary_contact: {contact_name, customer_id}`.
- All follow-up tasks (`collection_task`, `invite_task`) must reference this
  contact name.

### Step 9 — Accounting action (missing revenue journal)
- If a paid milestone is missing its revenue journal:
  - `primary_accounting_action` / `accounting_action.action` =
    `"RECORD_REVENUE_MS{n}"` (with the specific milestone number).
  - `milestone_id` = `"MS{n}"`.
  - `amount` = the milestone's paid amount.
  - `debit_account` = `"DEFERRED_REVENUE"`,
    `credit_account` = `"IMPLEMENTATION_SERVICES_REVENUE"`
    (matching the existing posted journal pattern).
  - `owner_queue` = `"ACCOUNTING"`.
- If all paid milestones are already recognized:
  `"VERIFY_REVENUE_ONLY"` or `"NO_ACCOUNTING_ACTION"`.

### Step 10 — Collection action (unpaid milestones)
- Compare the current business date (given in the prompt, e.g. 2026-06-01)
  against the unpaid milestone's invoice `due_date`:
  - Current date **before** due_date → `action` =
    `"MONITOR_UNPAID_NOT_DUE"`, `owner_queue` = `"ACCOUNT_MANAGEMENT"`.
  - Current date **on/after** due_date → `action` =
    `"SEND_COLLECTION_NOTICE"`, `owner_queue` = `"COLLECTIONS"`.
- `collection_task.milestone_id` = `"MS{n}"` of the unpaid milestone.
- `collection_task.amount` = the unpaid amount.
- `collection_task.due_date` = the invoice `due_date`.
- `collection_task.contact_name` = the opportunity contact.
- If there are no unpaid milestones → `"NO_COLLECTION_ACTION"`.

### Step 11 — Event & voucher actions
- Fetch the event (`GET /api/search?q=<event_id>` or filter events) and its
  voucher (by `voucher_code`).
- `event_status`: map event `status` — `scheduled` → `"SCHEDULED"`, `active`
  → `"ACTIVE"`, `completed` → `"COMPLETED"`, `cancelled` → `"CANCELLED"`.
- `voucher_status`: map voucher `status` — `active` → `"ACTIVE"`, `draft` →
  `"DRAFT"`, `expired` → `"EXPIRED"`, `disabled` → `"DISABLED"`.
- `discount_amount`: use the voucher's `discount_percent` value as a number
  (e.g. 50 for 50%, 100 for 100%).
- `max_uses` / `voucher_max_uses`: the voucher's `max_redemptions`.
- `invite_action`:
  - Event scheduled and invite not yet sent (prompt says "before the
    invitation goes out") → `"SEND_BRIEFING_INVITE"`.
  - Event already active/invite sent → `"VERIFY_INVITE_SENT"`.
  - No event → `"NO_INVITE_ACTION"`.
- `invite_task.owner_queue`: `"ACCOUNT_MANAGEMENT"` (matching the event's
  `follow_up_owner`), or `"EVENTS"`.
- `invite_task` carries `event_id`, `voucher_code`, `contact_name`,
  `customer_id`.

## 3. Common Pitfalls Checklist

1. ✗ Using `prior_unit_price_usd` instead of the catalog tier price.
2. ✗ Picking the wrong quantity tier (off-by-one tier boundary; using a
   superseded/distractor RFQ's quantity).
3. ✗ Including distractor freight quotes (`FR-DIS-*` / "Distractor route").
4. ✗ Recommending AIR "for safety" instead of the cheapest valid option.
5. ✗ Using `phase_id` or `invoice_id` as `milestone_id` instead of `MS{n}` —
   causes cascade scoring failure.
6. ✗ Putting a date in `due_date` for a PAID milestone (should be `null`).
7. ✗ Filtering revenue-journals by `customer_id` instead of `opportunity_id`.
8. ✗ Setting `who_documentation_required: false` for IEHK/WHO-standard kits.
9. ✗ Expanding module RFQs into component line items.
10. ✗ Using `PREPAY_100` for a recurring client, or `NET_30_AFTER_PO` for a
    new client.
11. ✗ Using `offer_validity_days` other than 30 for catalog quotes.
12. ✗ Mapping invoice `status:"unpaid"` to `invoice_state:"UNPAID"` — the
    correct enum value is `"OPEN"` (train_005 template).
13. ✗ Forgetting `freight_reconfirmation_required: true` on freight-bearing
    quotes.
14. ✗ Not detecting a missing revenue journal for a paid milestone (always
    cross-check paid milestones against the revenue-journal set).
15. ✗ Leaving out the accounting action when a revenue journal is missing
    (must emit `RECORD_REVENUE_MS{n}`).

## 4. Output Discipline

- Return **only valid JSON** matching the task's `answer_template.json` —
  same field names, controlled enum values, correct nesting, no extra fields,
  no markdown fences, no prose.
- Use the exact enum strings declared in the template.
- Money as numbers with 2 decimals; dates as ISO `YYYY-MM-DD`; booleans as
  `true`/`false`; nulls as `null` (not omitted).
- Order milestones ascending by `milestone_id` (`MS1`, `MS2`, …).
- Order freight options consistently (AIR, SEA, ROAD) unless the template
  implies otherwise.

