MedBridge Sales Ops — Account-Ready JSON
You are filling a fixed JSON answer template from a read-only business API. The
template is the contract: copy its key names, nesting, enum vocabulary, and
example values exactly, and only change the data. The single source of truth is
the MedBridge Sales Ops API. Never invent numbers, dates, or ids — derive every
value from a fetched record and reconcile cross-references before you trust them.
0. Always do this first
- Read
environment_access.md (or whatever the task calls it) for the base URL.
It is typically http://127.0.0.1:8094, but the runner may inject it as
API_BASE_URL / BASE_URL / a ${PORT}. Confirm with GET /health.
- Read the task prompt AND
input/payloads/answer_template.json. The template
tells you the task family and the exact output shape. Identify which family
(see below) before fetching anything.
- Fetch by id with detail endpoints (
/api/quotes/<id>, /api/opportunities/<id>,
etc.) whenever the prompt gives an id — that is the unambiguous record.
- Output rule (every family): return ONLY the JSON, no markdown fences, no prose.
Keep money at 2-decimal cent precision, dates as ISO
YYYY-MM-DD, and preserve
the template's field order and casing.
The loose-filter pitfall (read before any list query)
List endpoints accept ?field=value, but the filter matches the value ANYWHERE
in the (nested) record, so it returns extra unrelated rows, including planted
distractors. For example GET /api/freight-quotes?quote_id=Q-... can return
5 rows when only 3 belong to the quote.
Defensive rule: fetch the full collection and filter precisely yourself. Match
on the exact field you mean (e.g. record["quote_id"] == quote_id) and then drop
distractors using the cross-checks in each SOP. Distractor records typically have
ids containing DIS, status of stale/mismatch, a valid_until already
expired, or shipment/quantity dimensions that do not match the governing record.
GET /api/search?q=<text> is fine for finding an id, never for selecting the
authoritative row.
Identifying the task family
| Signal in prompt / template |
Family |
SOP |
| Quote id, "freight options", EXW + grand totals, route risk, recommended mode, payment/freight flags |
Freight decision package |
§A |
| RFQ id, "module level", "indicative", "EXW only / freight excluded", line items + grand_total |
Module-only EXW quote |
§B |
| Opportunity id, milestones/phases, invoices, payments, revenue recognition, outstanding balance, event/voucher, follow-up tasks |
Engagement / receivables reconciliation |
§C |
The freight-decision and reconciliation templates vary in field names across
tasks (e.g. risk_level vs customs_border_risk; milestones[] vs
engagement_reconciliation.milestones[]). Always bind to the template you were
given. The logic below is stable; the field names are whatever the template uses.
Shared catalog-tier pricing rule (used by §A and §B)
Products carry price_tiers[], each {min_qty, max_qty, unit_price_usd, lead_time_days, shelf_life_months?}. Select the tier whose
min_qty <= confirmed_quantity <= max_qty, treating max_qty == null as
unbounded (open top tier). The catalog tier overrides any
prior_unit_price_usd on the quote line — a revision re-prices at the current
tier for the confirmed quantity. lead_time_days comes from the selected tier;
shelf_life_months is a product-level field. exw_total = unit_price * quantity
(then line_total / grand_total accordingly). Never use the prior price.
§A — Freight decision package (quote + EXW + freight)
Goal: revised EXW pricing for the confirmed quantity, the three current freight
options (one AIR, one SEA, one ROAD) with EXW-plus-freight grand totals, route/
border risk, recommended mode, freight validity/reconfirmation, and payment terms.
GET /api/quotes/<id> → customer_id, quote_date, primary product code,
confirmed_quantity. Then fetch the customer and the product.
- Price via the shared catalog-tier rule.
exw_total = unit_price * qty.
- Select the 3 freight rows. Fetch the full
/api/freight-quotes
collection. Keep rows where quote_id == <this quote>, then keep exactly the
legitimate AIR/SEA/ROAD set and drop distractors:
- Exclude ids containing
DIS and any status of stale/mismatch as the
authoritative option for a mode, unless the template explicitly wants you
to report a stale option as one of the three (see step 5 — sometimes the
genuine ROAD option is itself stale and must be shown with a STALE/source_is_stale
flag). Distinguish: a genuine option matches the quote's shipment dimensions
(same shipment_cbm/shipment_weight_kg family) and follows the quote's
product token in its id (e.g. FR-WC-* for the WC quote); a distractor has
mismatched dimensions or an unrelated id.
- There should be exactly one row per mode after filtering. If two rows claim
the same mode, the genuine one matches shipment size and is not a
DIS/stale
benchmark.
- Per-option fields (map to whatever the template names them):
freight_cost = cost_usd; grand_total = exw_total + freight_cost.
transit_days: copy the template's format — some templates want the text
"4-6 days" (use transit_days_text), others want "4-6" (strip " days")
or min-max. Match the template's example exactly.
valid_until = valid_until.
- Risk:
route_risk is lowercase in the API. Map to the template's enum,
usually UPPERCASE: low→LOW, medium→MEDIUM, high→HIGH. If the template
has a separate risk_flag, use NONE for low risk and a border flag such as
MEDIUM_BORDER_RISK / HIGH_BORDER_RISK when risk_notes mention border/
customs risk at that level (mirror the example value in the template).
- Validity status (if the template has one):
VALID when
valid_until >= quote_date and status == active; STALE/EXPIRED (and
source_is_stale = true) when status == stale or valid_until < quote_date.
- Recommended mode — feasibility/validity first, then cost, then risk:
- Consider only options that are VALID on the quote date (not stale, not
expired) AND carry acceptable risk (avoid HIGH border/customs risk).
- Among the remaining options, recommend the lowest grand_total.
- A cheaper option that is stale/expired or HIGH-risk does NOT win — exclude it
first, then compare cost. (E.g. a stale, high-risk ROAD that happens to be a
few dollars cheaper must not be recommended; the cheapest valid low-risk
mode wins instead.)
- Cold-chain note: if the product has
cold_chain_required: true, a mode whose
freight row lacks cold_chain_support is not a safe recommendation.
- Flags / policy:
freight_reconfirmation_required = true always (policy POL-FREIGHT-RECONFIRM:
freight rates must be reconfirmed at final order).
all_freight_options_valid_on_quote_date: true only if all three reported
options have valid_until >= quote_date.
road_quote_invalid_or_stale (if present): true when the ROAD option is
expired/stale.
quote_basis: for these quotes it is EXW-plus-freight, e.g.
EXW_PLUS_FREIGHT_OPTIONS (match the template's spelling/casing).
- Payment terms and customer policy: see §D.
freight_warning (free-text, if present): state that rates need
reconfirmation at final order, and name any expired/high-risk option with its
expiry date and risk so it is not used without a fresh quote. Keep it factual.
§B — Module-only EXW quote (RFQ)
Goal: indicative EXW quote at module/SKU line level, freight excluded.
GET /api/rfqs/<id> → customer_id, quote_date, and requested_modules[]
(product_code + quantity). Fetch the customer and each requested product.
- Quote at the requested line level only. RFQs include
component_composition_distractors / "for medical review only" notes. Do NOT
split modules into component SKUs and do NOT add component lines (policy
POL-MODULE-GRANULARITY). One output line per requested module, in the requested
order, using the requested quantity.
- Per line:
unit_price from the catalog tier for that module's quantity,
article_number and shelf_life_months from the product, lead_time_days
from the selected tier, line_total = unit_price * quantity.
grand_total = sum(line_total). freight_excluded = true, quote_basis = EXW_ONLY (no destination ⇒ EXW only, policy POL-INDICATIVE-EXW).
- Controls:
offer_validity_days = 30 (POL-QUOTE-VALIDITY, catalog pricing valid
30 days). who_documentation_required = true for these humanitarian health
kits unless the template/prompt says otherwise. Payment terms per §D (a new NGO
⇒ PREPAY_100).
§C — Engagement / receivables reconciliation
Goal: confirm the won opportunity total agrees with its milestone phases;
summarize invoice/payment/outstanding/revenue-recognition state per milestone;
include the related event + voucher; and route the required follow-up tasks.
GET /api/opportunities/<id> → stage, won_amount_usd, contact,
outstanding_amount_usd, and phases[] (each has phase_id, amount_usd,
invoice_id, completion_date). Map stage: closed_won → WON, open/
proposal/negotiation → OPEN, closed_lost → LOST.
- Match check:
phase_total = sum(phase.amount_usd).
opportunity_matches_(milestones|phase_total) = (won_amount == phase_total).
- Per milestone (one per phase, ordered ascending; if the template labels them
MS1/MS2/MS3, assign by phase order). Cross-reference precisely (loose-filter
pitfall): from the full /api/invoices, /api/payments, /api/revenue-journals
collections, pick the rows whose phase_id/invoice_id/opportunity_id
match THIS phase. Per milestone:
amount / invoice_total = phase.amount_usd (== matching invoice amount_usd).
invoice_state from the invoice status (paid→PAID, unpaid/overdue/ draft→OPEN, etc. — map to the template's enum).
paid_amount = sum(matching posted payments) (or invoice paid_amount_usd).
payment_state: PAID if paid == amount, PARTIAL if 0 < paid < amount, UNPAID
if paid == 0.
due_date: this is the load-bearing rule — null out the due date for any
PAID milestone; only an unpaid/outstanding milestone keeps its invoice
due_date. (Templates default paid due_date to null.)
recognition_status (revenue recognition; policy POL-REVREC):
RECOGNIZED — milestone is paid AND a revenue journal exists for it.
MISSING_REVENUE_JOURNAL / REQUIRED_MISSING — milestone is paid (and
complete) but NO revenue journal exists. This is a finding that drives an
accounting action.
NOT_REQUIRED_UNPAID — milestone is unpaid; recognition not yet required.
- Account totals:
total_paid = sum(paid across milestones);
outstanding_balance = sum(amount - paid) (should equal the opportunity's
outstanding_amount_usd; if it disagrees, recompute from milestones and trust
the reconciled figure). Carry the primary contact from the opportunity for
all follow-up routing.
- Revenue-recognition rollup (if the template has a block):
COMPLETE_FOR_PAID_MILESTONES — every paid milestone has a journal.
MISSING_FOR_PAID_MILESTONES — at least one paid milestone lacks a journal.
recognized_milestones = paid+journaled; missing_required_milestones =
paid but unjournaled; recognized_amount = sum of journaled amounts.
- Follow-up / action routing (use the template's exact enums):
- Accounting action: if a paid milestone is missing its journal →
RECORD_REVENUE_<MSx> for that milestone, amount = milestone amount,
debit_account = DEFERRED_REVENUE, credit_account = IMPLEMENTATION_SERVICES_REVENUE, owner_queue = ACCOUNTING. If all paid
milestones are already recognized → VERIFY_REVENUE_ONLY. If nothing is
paid/recognizable → NO_ACCOUNTING_ACTION (accounts/owner NONE).
- Collection action for the earliest unpaid milestone, compared to the
business/as-of date (from the prompt; reconciliation tasks use the stated
current date, e.g.
2026-06-01):
- unpaid and due_date in the future →
MONITOR_UNPAID_NOT_DUE
(COLLECT_UNPAID_MILESTONE as the next action where the template uses that
enum), owner ACCOUNT_MANAGEMENT, due_date = invoice due_date.
- unpaid and due_date today/past (overdue) →
SEND_COLLECTION_NOTICE,
owner COLLECTIONS.
- no unpaid milestone →
NO_COLLECTION_ACTION.
The collection task is tied to the opportunity's contact and customer.
- Event / voucher invite: include the linked event and voucher.
- Event status maps from the event
status (scheduled→SCHEDULED,
confirmed→SCHEDULED or the template's "ready to invite" value,
live→ACTIVE, completed→COMPLETED, cancelled→CANCELLED). Read the
template enum and pick the matching value.
- Voucher:
voucher_code = code; discount/discount_amount = the numeric
discount_percent value rendered as a number (e.g. 100 → 100.00, 50 →
50.00); max_uses = max_redemptions; voucher_status from voucher
status (active→ACTIVE).
- Invite action: if the event is upcoming/active and the invite has not been
sent →
SEND_EVENT_INVITATION / SEND_BRIEFING_INVITE, owner
ACCOUNT_MANAGEMENT, carrying event_id, voucher_code, contact, customer.
- Follow-up due dates: a collection task is due on the unpaid milestone's
due_date. An event-invitation task is due shortly before the event date
(lead time) — match the template's convention; do not invent a date the
records cannot support.
For the precise field-name→API-field mapping, enum vocabularies, and worked
derivations for each template variant, read
references/field_mappings_and_enums.md.
§D — Payment terms & customer policy (all quote families)
Derive from the customer record + policies, not from guesswork:
- New NGO / first order / no approved credit (
segment: new_ngo,
is_recurring: false, payment_profile: NEW_CLIENT_REVIEW) → PREPAY_100
(POL-NEW-CLIENT-PAYMENT) and customer policy NEW_CLIENT / NEW_NGO.
- Recurring NGO or recurring commercial with net-after-PO grant terms
(
is_recurring: true, payment_profile: NET_30_AFTER_PO) → NET_30_AFTER_PO
(POL-RECURRING-NGO-PAYMENT) and customer policy RECURRING_NGO (or the
segment-appropriate value). Restricted grant terms can override to stricter.
- When in doubt, the customer's
payment_profile field is the authoritative
terms code; confirm it against the matching policy terms_code.
Use the controlled strings exactly as the template/policies spell them
(PREPAY_100, NET_30_AFTER_PO, EXW_ONLY, EXW_PLUS_FREIGHT_OPTIONS, etc.).
Common misjudgments — do NOT do these
- Do NOT trust loose list filters; they include distractors (
FR-DIS-*, stale,
mismatched shipment size). Always cross-check the governing id and dimensions.
- Do NOT use the quote line's
prior_unit_price_usd. Re-price at the current
catalog tier for the confirmed quantity.
- Do NOT split module RFQ lines into components, and do NOT add component lines —
the
component_composition_distractors are review-only.
- Do NOT recommend a freight mode just because it is cheapest. Exclude stale/
expired/HIGH-risk (and cold-chain-incapable for cold-chain products) options
first; recommend the cheapest valid, low-risk mode. Keep feasibility/validity
separate from cost.
- Do NOT keep a
due_date on a PAID milestone — null it. Only unpaid/outstanding
milestones carry a due date.
- Do NOT mark a paid milestone
RECOGNIZED unless an actual revenue journal row
exists for it; a paid-but-unjournaled milestone is MISSING_REVENUE_JOURNAL
and drives a RECORD_REVENUE accounting action.
- Do NOT add freight, insurance, or duty to an EXW-only / indicative quote.
- Do NOT emit markdown, comments, or prose around the JSON, and do NOT rename,
reorder, or drop template keys. Match enum casing exactly.
- Do NOT compute risk text or border flags from the mode name; derive from the
freight row's
route_risk and risk_notes.
1---2name: medbridge-sales-ops-23description: Produce account-ready JSON answers for the MedBridge Sales Ops domain (CRM, quotes, freight/logistics decisions, and milestone receivables). Use this whenever a task asks you to verify or reconcile MedBridge records against the Sales Ops HTTP API and return JSON matching an answer_template.json — including catalog-tier quote pricing, EXW + freight decision packages, route/border risk and recommended transport mode, module-only RFQ quotes, payment-terms selection, or opportunity↔invoice↔payment↔revenue-journal reconciliation with collection/event follow-up routing. Trigger it even when the prompt only names a quote/opportunity/RFQ id (e.g. "Q-...", "OPP-...", "RFQ-...") and asks for a decision package or reconciliation, or mentions freight options, milestones, outstanding balance, revenue recognition, vouchers, or follow-up tasks.4---56# MedBridge Sales Ops — Account-Ready JSON78You are filling a fixed JSON answer template from a read-only business API. The9template is the contract: copy its key names, nesting, enum vocabulary, and10example values exactly, and only change the data. The single source of truth is11the MedBridge Sales Ops API. Never invent numbers, dates, or ids — derive every12value from a fetched record and reconcile cross-references before you trust them.1314## 0. Always do this first15161. Read `environment_access.md` (or whatever the task calls it) for the base URL.17 It is typically `http://127.0.0.1:8094`, but the runner may inject it as18 `API_BASE_URL` / `BASE_URL` / a `${PORT}`. Confirm with `GET /health`.192. Read the task prompt AND `input/payloads/answer_template.json`. The template20 tells you the task family and the exact output shape. Identify which family21 (see below) before fetching anything.223. Fetch by id with detail endpoints (`/api/quotes/<id>`, `/api/opportunities/<id>`,23 etc.) whenever the prompt gives an id — that is the unambiguous record.244. Output rule (every family): return ONLY the JSON, no markdown fences, no prose.25 Keep money at 2-decimal cent precision, dates as ISO `YYYY-MM-DD`, and preserve26 the template's field order and casing.2728## The loose-filter pitfall (read before any list query)2930List endpoints accept `?field=value`, but the filter matches the value ANYWHERE31in the (nested) record, so it returns **extra unrelated rows**, including planted32**distractors**. For example `GET /api/freight-quotes?quote_id=Q-...` can return335 rows when only 3 belong to the quote.3435Defensive rule: **fetch the full collection and filter precisely yourself.** Match36on the exact field you mean (e.g. `record["quote_id"] == quote_id`) and then drop37distractors using the cross-checks in each SOP. Distractor records typically have38ids containing `DIS`, `status` of `stale`/`mismatch`, a `valid_until` already39expired, or shipment/quantity dimensions that do not match the governing record.40`GET /api/search?q=<text>` is fine for *finding* an id, never for selecting the41authoritative row.4243## Identifying the task family4445| Signal in prompt / template | Family | SOP |46| --- | --- | --- |47| Quote id, "freight options", EXW + grand totals, route risk, recommended mode, payment/freight flags | **Freight decision package** | §A |48| RFQ id, "module level", "indicative", "EXW only / freight excluded", line items + grand_total | **Module-only EXW quote** | §B |49| Opportunity id, milestones/phases, invoices, payments, revenue recognition, outstanding balance, event/voucher, follow-up tasks | **Engagement / receivables reconciliation** | §C |5051The freight-decision and reconciliation templates vary in field names across52tasks (e.g. `risk_level` vs `customs_border_risk`; `milestones[]` vs53`engagement_reconciliation.milestones[]`). Always bind to the template you were54given. The *logic* below is stable; the field names are whatever the template uses.5556## Shared catalog-tier pricing rule (used by §A and §B)5758Products carry `price_tiers[]`, each `{min_qty, max_qty, unit_price_usd,59lead_time_days, shelf_life_months?}`. Select the tier whose60`min_qty <= confirmed_quantity <= max_qty`, treating `max_qty == null` as61unbounded (open top tier). The catalog tier **overrides** any62`prior_unit_price_usd` on the quote line — a revision re-prices at the current63tier for the confirmed quantity. `lead_time_days` comes from the selected tier;64`shelf_life_months` is a product-level field. `exw_total = unit_price * quantity`65(then `line_total` / `grand_total` accordingly). Never use the prior price.6667---6869## §A — Freight decision package (quote + EXW + freight)7071Goal: revised EXW pricing for the confirmed quantity, the three current freight72options (one AIR, one SEA, one ROAD) with EXW-plus-freight grand totals, route/73border risk, recommended mode, freight validity/reconfirmation, and payment terms.74751. `GET /api/quotes/<id>` → `customer_id`, `quote_date`, primary product code,76 `confirmed_quantity`. Then fetch the customer and the product.772. **Price** via the shared catalog-tier rule. `exw_total = unit_price * qty`.783. **Select the 3 freight rows.** Fetch the full `/api/freight-quotes`79 collection. Keep rows where `quote_id == <this quote>`, then keep exactly the80 legitimate AIR/SEA/ROAD set and drop distractors:81 - Exclude ids containing `DIS` and any `status` of `stale`/`mismatch` **as the82 authoritative option for a mode**, unless the template explicitly wants you83 to *report* a stale option as one of the three (see step 5 — sometimes the84 genuine ROAD option is itself stale and must be shown with a STALE/`source_is_stale`85 flag). Distinguish: a genuine option matches the quote's shipment dimensions86 (same `shipment_cbm`/`shipment_weight_kg` family) and follows the quote's87 product token in its id (e.g. `FR-WC-*` for the WC quote); a distractor has88 mismatched dimensions or an unrelated id.89 - There should be exactly one row per mode after filtering. If two rows claim90 the same mode, the genuine one matches shipment size and is not a `DIS`/stale91 benchmark.924. **Per-option fields** (map to whatever the template names them):93 - `freight_cost = cost_usd`; `grand_total = exw_total + freight_cost`.94 - `transit_days`: copy the template's format — some templates want the text95 `"4-6 days"` (use `transit_days_text`), others want `"4-6"` (strip " days")96 or `min-max`. Match the template's example exactly.97 - `valid_until = valid_until`.98 - Risk: `route_risk` is lowercase in the API. Map to the template's enum,99 usually UPPERCASE: `low→LOW`, `medium→MEDIUM`, `high→HIGH`. If the template100 has a separate `risk_flag`, use `NONE` for low risk and a border flag such as101 `MEDIUM_BORDER_RISK` / `HIGH_BORDER_RISK` when `risk_notes` mention border/102 customs risk at that level (mirror the example value in the template).103 - Validity status (if the template has one): `VALID` when104 `valid_until >= quote_date` and `status == active`; `STALE`/`EXPIRED` (and105 `source_is_stale = true`) when `status == stale` or `valid_until < quote_date`.1065. **Recommended mode** — feasibility/validity first, then cost, then risk:107 - Consider only options that are VALID on the quote date (not stale, not108 expired) AND carry acceptable risk (avoid HIGH border/customs risk).109 - Among the remaining options, recommend the **lowest grand_total**.110 - A cheaper option that is stale/expired or HIGH-risk does NOT win — exclude it111 first, then compare cost. (E.g. a stale, high-risk ROAD that happens to be a112 few dollars cheaper must not be recommended; the cheapest *valid low-risk*113 mode wins instead.)114 - Cold-chain note: if the product has `cold_chain_required: true`, a mode whose115 freight row lacks `cold_chain_support` is not a safe recommendation.1166. **Flags / policy:**117 - `freight_reconfirmation_required = true` always (policy POL-FREIGHT-RECONFIRM:118 freight rates must be reconfirmed at final order).119 - `all_freight_options_valid_on_quote_date`: true only if all three reported120 options have `valid_until >= quote_date`.121 - `road_quote_invalid_or_stale` (if present): true when the ROAD option is122 expired/stale.123 - `quote_basis`: for these quotes it is EXW-plus-freight, e.g.124 `EXW_PLUS_FREIGHT_OPTIONS` (match the template's spelling/casing).125 - Payment terms and customer policy: see §D.126 - `freight_warning` (free-text, if present): state that rates need127 reconfirmation at final order, and name any expired/high-risk option with its128 expiry date and risk so it is not used without a fresh quote. Keep it factual.129130---131132## §B — Module-only EXW quote (RFQ)133134Goal: indicative EXW quote at module/SKU line level, freight excluded.1351361. `GET /api/rfqs/<id>` → `customer_id`, `quote_date`, and `requested_modules[]`137 (`product_code` + `quantity`). Fetch the customer and each requested product.1382. **Quote at the requested line level only.** RFQs include139 `component_composition_distractors` / "for medical review only" notes. Do NOT140 split modules into component SKUs and do NOT add component lines (policy141 POL-MODULE-GRANULARITY). One output line per requested module, in the requested142 order, using the requested quantity.1433. Per line: `unit_price` from the catalog tier for that module's quantity,144 `article_number` and `shelf_life_months` from the product, `lead_time_days`145 from the selected tier, `line_total = unit_price * quantity`.1464. `grand_total = sum(line_total)`. `freight_excluded = true`, `quote_basis =147 EXW_ONLY` (no destination ⇒ EXW only, policy POL-INDICATIVE-EXW).1485. Controls: `offer_validity_days = 30` (POL-QUOTE-VALIDITY, catalog pricing valid149 30 days). `who_documentation_required = true` for these humanitarian health150 kits unless the template/prompt says otherwise. Payment terms per §D (a new NGO151 ⇒ `PREPAY_100`).152153---154155## §C — Engagement / receivables reconciliation156157Goal: confirm the won opportunity total agrees with its milestone phases;158summarize invoice/payment/outstanding/revenue-recognition state per milestone;159include the related event + voucher; and route the required follow-up tasks.1601611. `GET /api/opportunities/<id>` → `stage`, `won_amount_usd`, `contact`,162 `outstanding_amount_usd`, and `phases[]` (each has `phase_id`, `amount_usd`,163 `invoice_id`, `completion_date`). Map `stage`: `closed_won → WON`, open/164 proposal/negotiation → `OPEN`, `closed_lost → LOST`.1652. **Match check:** `phase_total = sum(phase.amount_usd)`.166 `opportunity_matches_(milestones|phase_total) = (won_amount == phase_total)`.1673. **Per milestone** (one per phase, ordered ascending; if the template labels them168 `MS1/MS2/MS3`, assign by phase order). Cross-reference precisely (loose-filter169 pitfall): from the full `/api/invoices`, `/api/payments`, `/api/revenue-journals`170 collections, pick the rows whose `phase_id`/`invoice_id`/`opportunity_id`171 match THIS phase. Per milestone:172 - `amount / invoice_total = phase.amount_usd` (== matching invoice `amount_usd`).173 - `invoice_state` from the invoice `status` (`paid→PAID`, `unpaid/overdue/174 draft→OPEN`, etc. — map to the template's enum).175 - `paid_amount = sum(matching posted payments)` (or invoice `paid_amount_usd`).176 - `payment_state`: PAID if paid == amount, PARTIAL if 0 < paid < amount, UNPAID177 if paid == 0.178 - `due_date`: this is the load-bearing rule — **null out the due date for any179 PAID milestone**; only an unpaid/outstanding milestone keeps its invoice180 `due_date`. (Templates default paid due_date to null.)181 - `recognition_status` (revenue recognition; policy POL-REVREC):182 - `RECOGNIZED` — milestone is paid AND a revenue journal exists for it.183 - `MISSING_REVENUE_JOURNAL` / `REQUIRED_MISSING` — milestone is paid (and184 complete) but NO revenue journal exists. This is a finding that drives an185 accounting action.186 - `NOT_REQUIRED_UNPAID` — milestone is unpaid; recognition not yet required.1874. **Account totals:** `total_paid = sum(paid across milestones)`;188 `outstanding_balance = sum(amount - paid)` (should equal the opportunity's189 `outstanding_amount_usd`; if it disagrees, recompute from milestones and trust190 the reconciled figure). Carry the primary `contact` from the opportunity for191 all follow-up routing.1925. **Revenue-recognition rollup** (if the template has a block):193 - `COMPLETE_FOR_PAID_MILESTONES` — every paid milestone has a journal.194 - `MISSING_FOR_PAID_MILESTONES` — at least one paid milestone lacks a journal.195 - `recognized_milestones` = paid+journaled; `missing_required_milestones` =196 paid but unjournaled; `recognized_amount` = sum of journaled amounts.1976. **Follow-up / action routing** (use the template's exact enums):198 - **Accounting action:** if a paid milestone is missing its journal →199 `RECORD_REVENUE_<MSx>` for that milestone, `amount = milestone amount`,200 `debit_account = DEFERRED_REVENUE`, `credit_account =201 IMPLEMENTATION_SERVICES_REVENUE`, `owner_queue = ACCOUNTING`. If all paid202 milestones are already recognized → `VERIFY_REVENUE_ONLY`. If nothing is203 paid/recognizable → `NO_ACCOUNTING_ACTION` (accounts/owner `NONE`).204 - **Collection action** for the earliest unpaid milestone, compared to the205 business/as-of date (from the prompt; reconciliation tasks use the stated206 current date, e.g. `2026-06-01`):207 - unpaid and due_date in the future → `MONITOR_UNPAID_NOT_DUE`208 (`COLLECT_UNPAID_MILESTONE` as the next action where the template uses that209 enum), owner `ACCOUNT_MANAGEMENT`, due_date = invoice due_date.210 - unpaid and due_date today/past (overdue) → `SEND_COLLECTION_NOTICE`,211 owner `COLLECTIONS`.212 - no unpaid milestone → `NO_COLLECTION_ACTION`.213 The collection task is tied to the opportunity's contact and customer.214 - **Event / voucher invite:** include the linked event and voucher.215 - Event status maps from the event `status` (`scheduled→SCHEDULED`,216 `confirmed→SCHEDULED` or the template's "ready to invite" value,217 `live→ACTIVE`, `completed→COMPLETED`, `cancelled→CANCELLED`). Read the218 template enum and pick the matching value.219 - Voucher: `voucher_code` = code; `discount`/`discount_amount` = the numeric220 `discount_percent` value rendered as a number (e.g. 100 → 100.00, 50 →221 50.00); `max_uses` = `max_redemptions`; `voucher_status` from voucher222 `status` (`active→ACTIVE`).223 - Invite action: if the event is upcoming/active and the invite has not been224 sent → `SEND_EVENT_INVITATION` / `SEND_BRIEFING_INVITE`, owner225 `ACCOUNT_MANAGEMENT`, carrying event_id, voucher_code, contact, customer.226 - **Follow-up due dates:** a collection task is due on the unpaid milestone's227 due_date. An event-invitation task is due shortly *before* the event date228 (lead time) — match the template's convention; do not invent a date the229 records cannot support.230231For the precise field-name→API-field mapping, enum vocabularies, and worked232derivations for each template variant, read233`references/field_mappings_and_enums.md`.234235---236237## §D — Payment terms & customer policy (all quote families)238239Derive from the customer record + policies, not from guesswork:240- New NGO / first order / no approved credit (`segment: new_ngo`,241 `is_recurring: false`, `payment_profile: NEW_CLIENT_REVIEW`) → `PREPAY_100`242 (POL-NEW-CLIENT-PAYMENT) and customer policy `NEW_CLIENT` / `NEW_NGO`.243- Recurring NGO or recurring commercial with net-after-PO grant terms244 (`is_recurring: true`, `payment_profile: NET_30_AFTER_PO`) → `NET_30_AFTER_PO`245 (POL-RECURRING-NGO-PAYMENT) and customer policy `RECURRING_NGO` (or the246 segment-appropriate value). Restricted grant terms can override to stricter.247- When in doubt, the customer's `payment_profile` field is the authoritative248 terms code; confirm it against the matching policy `terms_code`.249250Use the controlled strings exactly as the template/policies spell them251(`PREPAY_100`, `NET_30_AFTER_PO`, `EXW_ONLY`, `EXW_PLUS_FREIGHT_OPTIONS`, etc.).252253---254255## Common misjudgments — do NOT do these256257- Do NOT trust loose list filters; they include distractors (`FR-DIS-*`, stale,258 mismatched shipment size). Always cross-check the governing id and dimensions.259- Do NOT use the quote line's `prior_unit_price_usd`. Re-price at the current260 catalog tier for the confirmed quantity.261- Do NOT split module RFQ lines into components, and do NOT add component lines —262 the `component_composition_distractors` are review-only.263- Do NOT recommend a freight mode just because it is cheapest. Exclude stale/264 expired/HIGH-risk (and cold-chain-incapable for cold-chain products) options265 first; recommend the cheapest *valid, low-risk* mode. Keep feasibility/validity266 separate from cost.267- Do NOT keep a `due_date` on a PAID milestone — null it. Only unpaid/outstanding268 milestones carry a due date.269- Do NOT mark a paid milestone `RECOGNIZED` unless an actual revenue journal row270 exists for it; a paid-but-unjournaled milestone is `MISSING_REVENUE_JOURNAL`271 and drives a `RECORD_REVENUE` accounting action.272- Do NOT add freight, insurance, or duty to an EXW-only / indicative quote.273- Do NOT emit markdown, comments, or prose around the JSON, and do NOT rename,274 reorder, or drop template keys. Match enum casing exactly.275- Do NOT compute risk text or border flags from the mode name; derive from the276 freight row's `route_risk` and `risk_notes`.