MedBridge Sales Ops — account-ready JSON packages
You verify records against one read-only JSON API and return JSON that exactly
matches a provided answer_template.json. The data is small, internally
consistent, and seeded — every required value is derivable from the API, never
invented. Your job is to find the right records (the API loosely over-returns)
and apply the business conventions the templates encode.
0. Universal rules (apply to every task family)
- The template is the contract. Read
input/payloads/answer_template.json
first. Reproduce its exact key names, nesting, ordering, and especially its
controlled enum values (e.g. EXW, NET_30_AFTER_PO, RECORD_REVENUE_MS2).
Templates list allowed enums inline as "enum: A | B | C" — pick one of those
literally. Output only JSON, no markdown, no prose, no trailing commentary.
- API only. Never read local business files. Reach the API with
curl. The
base URL is given in the prompt (env var like API_BASE_URL / BASE_URL or
http://127.0.0.1:<PORT>). Confirm reachability with GET /health.
- Fetch full collections, then filter yourself. List filters
(
?quote_id=..., ?customer_id=...) match a value anywhere/nested in the
record, so they return extra unrelated rows. Pull the whole collection and
match precisely on the exact field. See the distractor rules in each family.
- Money: USD, cent-level. Render template-required decimals (e.g.
2420.00).
Dates: ISO YYYY-MM-DD. IDs: copy the API's stable IDs verbatim.
- The "business date" matters. The prompt states a quote date / as-of date
(commonly
2026-06-01). Use it for all validity / due / staleness comparisons,
not today's real date.
- Catalog tier overrides prior pricing. Quotes carry a
prior_unit_price_usd
/ prior_quote_quantity. These are history — never quote them. Always reprice
from the current product price_tiers at the confirmed quantity.
Identify the task family from the template/prompt, then follow its SOP:
- A. Freight / transport decision package — template has
freight_options,
grand_total, recommended_mode, risk/validity fields. (Q-* quotes with freight.)
- B. Module-only indicative quote — template has
line_items, EXW_ONLY,
who_documentation_required. (RFQ-* module requests, no destination.)
- C. Engagement / receivables reconciliation — template has
milestones,
outstanding_balance, revenue_recognition, follow-up/event actions. (OPP-*.)
A. Freight / transport decision package
Inputs: a quote ID (Q-...), confirmed quantity, quote date. Output: catalog
pricing + EXW total, 3 freight options (AIR/SEA/ROAD) with EXW-plus-freight grand
totals, risk + validity flags, a recommended mode, and policy/payment flags.
A1. Pricing
GET /api/quotes/<id> → read customer_id, primary_product_code,
confirmed_quantity, quote_date. Cross-check the prompt's quantity/date.
GET /api/products/<product_code> → choose the catalog tier whose band
contains the confirmed quantity: min_qty <= qty <= max_qty. A max_qty of
null means open-ended (no upper bound). Take unit_price_usd,
lead_time_days, shelf_life_months from that tier/product.
exw_total_usd = confirmed_quantity * tier.unit_price_usd.
quote_basis is EXW with freight shown as separate options. Use the exact
string the template wants ("EXW", or "EXW_PLUS_FREIGHT_OPTIONS" for the
policy_terms.quote_basis field).
A2. Selecting the REAL freight options (loose-filter trap)
GET /api/freight-quotes, then keep only rows where quote_id exactly equals
the quote and that are the genuine lane set. Exclude distractors:
- Any
id starting with FR-DIS- — these are benchmark/archived/wrong-size rows.
- Rows whose
status is stale/mismatch AND that duplicate a mode you already
have a clean active row for, or whose shipment_cbm/shipment_weight_kg differ
from the consistent real shipment dimensions of the genuine set.
The genuine set is exactly three rows — one mode each of air, sea, road —
sharing one consistent shipment_cbm/shipment_weight_kg. Output mode uppercased
(AIR/SEA/ROAD). Note: a real lane can still be status: "stale" / expired
(e.g. a road quote that genuinely lapsed) — that one stays in the output and gets
flagged stale; it is NOT a FR-DIS- distractor. Distinguish "real but expired"
from "fake distractor."
A3. Per-option fields
freight_cost_usd = cost_usd; transit_days = transit_days_text (copy the
template's format — some want "4-6 days", some want "3-5"; mirror the example
value style in the template).
valid_until = freight valid_until.
grand_total_usd = exw_total_usd + freight_cost_usd.
- Risk from
route_risk: uppercase it for risk_level/customs_border_risk
(low→LOW, medium→MEDIUM, high→HIGH). For risk_flag:
low→NONE, medium→MEDIUM_BORDER_RISK, high→HIGH_BORDER_RISK.
- Validity vs the quote date:
validity_status = VALID if
valid_until >= quote_date, else STALE. source_is_stale is true if the
freight status is stale or it is expired on the quote date, else false.
A4. Recommended mode (decision basis — order matters)
Pick the mode to recommend by filtering, then optimizing:
- Eligibility filter (feasibility/safety): drop options that are STALE/expired
on the quote date, and drop HIGH border-risk options. (MEDIUM risk is allowed but
disfavored vs LOW.)
- Among the eligible, prefer LOW route risk over MEDIUM.
- Tie-break by lowest
grand_total_usd (cheapest landed cost).
This typically yields SEA when AIR and SEA are both LOW/valid (SEA is cheaper)
and ROAD is medium/high or stale. Recommend by transport mode string ("SEA" etc).
Keep risk/validity separate from recommendation: you still list every real
option with its true flags even though only one is recommended.
A5. Policy / payment flags
freight_reconfirmation_required = true always (policy
POL-FREIGHT-RECONFIRM: freight rates reconfirm at final order).
all_freight_options_valid_on_quote_date = true only if all three real
options are VALID on the quote date (false if any is stale/expired).
payment_terms from the customer + payment policies (see Payment terms table).
customer_policy / segment label: map the customer segment to the controlled
value the template shows (e.g. recurring NGO → RECURRING_NGO).
road_quote_invalid_or_stale = true if the ROAD option is stale/expired.
freight_warning: when something is stale or high-risk, write a short factual
sentence: rates require reconfirmation at final order; name the stale freight ID,
its expiry date, and its risk, and say it should not be used without a fresh
quote. Keep it specific and grounded in the records.
B. Module-only indicative quote (RFQ, EXW only)
Inputs: an RFQ (RFQ-...) listing requested_modules, no confirmed destination.
Output: one EXW-only quote with one line per requested module.
B1. SOP
GET /api/rfqs/<id> → customer_id, quote_date, requested_modules
(each {product_code, quantity}), incoterm_requested.
- For each requested module,
GET /api/products/<code>:
article_number, unit_price from the product price tier at the requested
quantity (module products usually have a single open tier), lead_time_days,
shelf_life_months from that tier/product.
line_total = quantity * unit_price.
- Emit one line item per module, in the RFQ's module order.
grand_total = sum(line_total).
freight_excluded = true, quote_basis = "EXW_ONLY" (policy POL-INDICATIVE-EXW
POL-EXW-SCOPE: no destination ⇒ EXW only, exclude freight).
offer_validity_days = 30 (policy POL-QUOTE-VALIDITY).
who_documentation_required = true for these emergency-health-kit/WHO module RFQs.
payment_terms: new NGO ⇒ PREPAY_100 (see Payment terms table).
B2. Critical exclusion — module lines ONLY
RFQs and products carry component_composition_distractors /
components (e.g. "Paracetamol tabs", "gauze and splints", "RDT buffer packs").
These are medical-review context, not quote lines. Never split a module into
component SKUs, never add component lines, never re-price by component. One line per
requested module, period (policy POL-MODULE-GRANULARITY). Quantities come straight
from the RFQ — do not infer extra units from composition tables.
C. Engagement / receivables reconciliation (opportunity → cash → revenue)
Inputs: an opportunity (OPP-...), its customer, a named contact, an as-of date,
and (usually) a linked event + voucher. Output: account status, per-milestone
state, revenue-recognition status, and routed follow-up tasks.
C1. Gather and link
GET /api/opportunities/<id> → stage, won_amount_usd, phases[]
(phase_id, amount_usd, invoice_id, completion_date), contact,
outstanding_amount_usd.
GET /api/invoices, GET /api/payments, GET /api/revenue-journals; filter
each to this opportunity_id precisely (the collections contain many other
accounts — match exactly). Link by invoice_id / phase_id.
- Number milestones in phase order. Output milestone IDs in the convention the
template uses — some templates label them
MS1/MS2/MS3 (ascending), others use
the API phase_id. Match the template's enum/example exactly.
C2. Per-milestone derivation
invoice_total / amount = invoice (or phase) amount_usd.
- Payment state from the matched payment(s) and invoice paid/outstanding:
fully paid ⇒
PAID; partially paid ⇒ PARTIAL; none ⇒ UNPAID. paid_amount
= sum of posted payments (or invoice paid_amount_usd); amount_unpaid =
invoice outstanding_amount_usd.
invoice_state (when the template asks): mirror the invoice status
mapped to the template enum (paid⇒PAID, unpaid/overdue⇒OPEN, etc.).
- Due-date nulling: for a PAID milestone set
due_date to null (it is
settled — no due date is actionable). For an unpaid milestone use the invoice
due_date.
- Revenue recognition status (policy
POL-REVREC — recognize paid+complete
milestones; unpaid stay outstanding):
- PAID and a matching revenue journal exists ⇒
RECOGNIZED.
- PAID but no revenue journal ⇒ recognition is required but missing
(
REQUIRED_MISSING or MISSING_REVENUE_JOURNAL, per template enum).
- UNPAID ⇒ recognition not yet required ⇒
NOT_REQUIRED_UNPAID.
C3. Account-level rollups
opportunity_matches_milestones / opportunity_matches_phase_total = true iff
won_amount == sum(phase amounts).
outstanding_balance = sum of unpaid invoice outstanding amounts (= opportunity
outstanding_amount_usd; verify they agree).
total_paid_amount = sum of posted payments / paid invoice amounts.
- Contact: use the named contact; link it to this customer_id and opportunity_id.
C4. Revenue-recognition rollup block
recognized_milestones = paid milestones that have a journal.
missing_required_milestones = paid milestones lacking a journal.
recognized_amount = sum of recognized milestone amounts.
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 nothing is paid yet. (Watch for the classic seeded case: a
milestone that is paid but has no journal ⇒ flag missing, and the primary
accounting action becomes "record revenue for that milestone".)
C5. Action routing (the decision layer)
Templates differ; produce the actions their enums demand. General logic:
Accounting / revenue action — driven by the paid-but-unrecognized milestone:
- If a paid milestone lacks a journal ⇒ action
RECORD_REVENUE_MS<n>, on that
milestone, amount = its value, debit_account = DEFERRED_REVENUE,
credit_account = IMPLEMENTATION_SERVICES_REVENUE, owner_queue = ACCOUNTING.
- If all paid milestones are already recognized ⇒
VERIFY_REVENUE_ONLY/
NO_ACCOUNTING_ACTION per template (nothing to post).
Collection action — driven by the unpaid milestone vs the as-of date:
- Unpaid and not yet due (
due_date > as_of_date) ⇒ MONITOR_UNPAID_NOT_DUE
/ COLLECT_UNPAID_MILESTONE task but not overdue; owner ACCOUNT_MANAGEMENT.
- Unpaid and due/overdue (
due_date <= as_of_date) ⇒ SEND_COLLECTION_NOTICE;
owner COLLECTIONS.
- No unpaid milestone ⇒
NO_COLLECTION_ACTION.
- The collection task carries the unpaid milestone ID, its amount, and its due date.
Event / voucher invitation action — from the linked event + voucher:
GET /api/events and GET /api/vouchers; match the event by opportunity_id
(or the ID named in the prompt) and the voucher by event_id/code.
- Map event
status to the template enum (scheduled→SCHEDULED,
confirmed/live→ACTIVE, completed→COMPLETED, etc.). Map voucher status
(active→ACTIVE). discount_amount = voucher discount_percent value;
max_uses = max_redemptions.
- If the event is upcoming/scheduled ⇒ invite action
SEND_EVENT_INVITATION /
SEND_BRIEFING_INVITE; owner ACCOUNT_MANAGEMENT (or EVENTS); attach
event_id, voucher_code, contact, customer_id.
- Task titles: short, human, name the customer (e.g.
"Milestone 2 collection - <Customer Name>",
"Send celebration invite - <Customer Name>").
- Task
due_date: collection tasks use the unpaid invoice due date; invitation
tasks use a sensible pre-event date when the template expects one — prefer a date
the records justify (e.g. shortly before the event); never invent far-future dates.
Customer name caveat: use the customer name from the API record. The prompt's
friendly name may differ slightly (e.g. "Helios Health Alliance" in the prompt vs a
different stored name) — follow the convention the template/answer expects, which is
usually the human-readable account name the prompt uses for titles and the API
name for the canonical customer_name field. When in doubt, match the API record.
Payment terms — controlled values
Derive from customer payment_profile / segment and the payment policies:
| Situation |
Customer signal |
payment_terms value |
Policy |
| New NGO, no approved credit / first order |
segment: new_ngo, payment_profile: NEW_CLIENT_REVIEW, is_recurring:false |
PREPAY_100 |
POL-NEW-CLIENT-PAYMENT |
| Recurring NGO |
segment: recurring_ngo, is_recurring:true |
NET_30_AFTER_PO |
POL-RECURRING-NGO-PAYMENT |
| Recurring commercial |
payment_profile: NET_30_AFTER_PO |
NET_30_AFTER_PO |
(account terms) |
| Milestone implementation |
payment_profile: MILESTONE_BILLING |
milestone billing per phases |
POL-REVREC |
If grant terms restrict net terms, honor the restriction over the default.
Common misjudgments — do NOT do these
- Do not quote
prior_unit_price_usd or prior_quote_quantity; reprice from
the current catalog tier at the confirmed quantity.
- Do not include
FR-DIS-* freight rows or wrong-shipment-size rows just
because the loose filter returned them. There are exactly three real lanes.
- Do not confuse "real but expired/stale" lanes (keep + flag) with fake
FR-DIS- distractors (drop entirely).
- Do not split modules into component SKUs or add component lines — module
line level only; composition tables are medical context.
- Do not put a
due_date on a PAID milestone — null it.
- Do not mark a paid milestone
RECOGNIZED if no revenue journal exists —
that is the "missing journal" case that drives the record-revenue action.
- Do not recommend a stale/expired or HIGH-risk lane even if it is cheapest;
eligibility (validity + risk) gates the cheapest-cost tie-break.
- Do not compare validity/staleness against today's real date — use the
prompt's stated quote/as-of date.
- Do not invent IDs, amounts, dates, or enum values not present in the API or
template; every value must trace to a record or a stated convention.
- Do not emit markdown, code fences, or explanation — only the JSON object that
matches the template's structure exactly.
Quick verification before returning
- Every template key present, correct nesting/order, enums are literal allowed values.
- All money at required decimal precision;
grand_total = EXW + freight and line
sums reconcile; outstanding = won - paid and matches the opportunity.
- Exactly three real freight lanes; recommended mode is eligible (valid + not high
risk) and lowest landed cost among eligibles.
- Paid milestones with journals = RECOGNIZED; paid without = missing → action set;
unpaid = NOT_REQUIRED_UNPAID with due date carried into the collection task.
1---2name: medbridge-sales-ops3description: Produce account-ready JSON decision packages from the MedBridge Sales Ops API (CRM, quotes, freight/logistics, milestone receivables). Use this skill WHENEVER a task asks you to verify customer/quote/RFQ/opportunity records against a MedBridge Sales Ops API (base like http://127.0.0.1:<PORT>) and emit JSON matching an answer_template.json — including: catalog-tier quote pricing and EXW totals; EXW-plus-freight transport comparisons with route/border risk, freight validity and reconfirmation, and a recommended mode; module-only (IEHK / kit) indicative quotes; and opportunity-to-invoice-to-payment-to-revenue-journal reconciliation with outstanding balance, revenue recognition state, and CRM follow-up routing (collection, event/voucher invitation). Trigger even when the user only says "quote decision package", "freight comparison", "engagement / account reconciliation", "revenue recognition check", or names a MedBridge quote/RFQ/ opportunity ID (Q-*, RFQ-*, OPP-*), and never freelance the business rules below.4---56# MedBridge Sales Ops — account-ready JSON packages78You verify records against one read-only JSON API and return JSON that exactly9matches a provided `answer_template.json`. The data is small, internally10consistent, and seeded — every required value is **derivable from the API**, never11invented. Your job is to find the *right* records (the API loosely over-returns)12and apply the business conventions the templates encode.1314## 0. Universal rules (apply to every task family)15161. **The template is the contract.** Read `input/payloads/answer_template.json`17 first. Reproduce its exact key names, nesting, ordering, and especially its18 **controlled enum values** (e.g. `EXW`, `NET_30_AFTER_PO`, `RECORD_REVENUE_MS2`).19 Templates list allowed enums inline as `"enum: A | B | C"` — pick one of those20 literally. Output **only** JSON, no markdown, no prose, no trailing commentary.212. **API only.** Never read local business files. Reach the API with `curl`. The22 base URL is given in the prompt (env var like `API_BASE_URL` / `BASE_URL` or23 `http://127.0.0.1:<PORT>`). Confirm reachability with `GET /health`.243. **Fetch full collections, then filter yourself.** List filters25 (`?quote_id=...`, `?customer_id=...`) match a value **anywhere/nested** in the26 record, so they return extra unrelated rows. Pull the whole collection and27 match precisely on the exact field. See the distractor rules in each family.284. **Money:** USD, cent-level. Render template-required decimals (e.g. `2420.00`).29 **Dates:** ISO `YYYY-MM-DD`. **IDs:** copy the API's stable IDs verbatim.305. **The "business date" matters.** The prompt states a quote date / as-of date31 (commonly `2026-06-01`). Use it for all validity / due / staleness comparisons,32 not today's real date.336. **Catalog tier overrides prior pricing.** Quotes carry a `prior_unit_price_usd`34 / `prior_quote_quantity`. These are history — never quote them. Always reprice35 from the current product `price_tiers` at the confirmed quantity.3637Identify the task family from the template/prompt, then follow its SOP:38- **A. Freight / transport decision package** — template has `freight_options`,39 `grand_total`, `recommended_mode`, risk/validity fields. (Q-* quotes with freight.)40- **B. Module-only indicative quote** — template has `line_items`, `EXW_ONLY`,41 `who_documentation_required`. (RFQ-* module requests, no destination.)42- **C. Engagement / receivables reconciliation** — template has `milestones`,43 `outstanding_balance`, `revenue_recognition`, follow-up/`event` actions. (OPP-*.)4445---4647## A. Freight / transport decision package4849Inputs: a quote ID (`Q-...`), confirmed quantity, quote date. Output: catalog50pricing + EXW total, 3 freight options (AIR/SEA/ROAD) with EXW-plus-freight grand51totals, risk + validity flags, a recommended mode, and policy/payment flags.5253### A1. Pricing541. `GET /api/quotes/<id>` → read `customer_id`, `primary_product_code`,55 `confirmed_quantity`, `quote_date`. Cross-check the prompt's quantity/date.562. `GET /api/products/<product_code>` → choose the **catalog tier** whose band57 contains the confirmed quantity: `min_qty <= qty <= max_qty`. A `max_qty` of58 `null` means open-ended (no upper bound). Take `unit_price_usd`,59 `lead_time_days`, `shelf_life_months` from that tier/product.603. `exw_total_usd = confirmed_quantity * tier.unit_price_usd`.614. `quote_basis` is EXW with freight shown as separate options. Use the exact62 string the template wants (`"EXW"`, or `"EXW_PLUS_FREIGHT_OPTIONS"` for the63 `policy_terms.quote_basis` field).6465### A2. Selecting the REAL freight options (loose-filter trap)66`GET /api/freight-quotes`, then keep only rows where `quote_id` **exactly** equals67the quote and that are the genuine lane set. **Exclude distractors:**68- Any `id` starting with `FR-DIS-` — these are benchmark/archived/wrong-size rows.69- Rows whose `status` is `stale`/`mismatch` AND that duplicate a mode you already70 have a clean active row for, or whose `shipment_cbm`/`shipment_weight_kg` differ71 from the consistent real shipment dimensions of the genuine set.72The genuine set is exactly three rows — one `mode` each of `air`, `sea`, `road` —73sharing one consistent `shipment_cbm`/`shipment_weight_kg`. Output `mode` uppercased74(`AIR`/`SEA`/`ROAD`). Note: a *real* lane can still be `status: "stale"` / expired75(e.g. a road quote that genuinely lapsed) — that one stays in the output and gets76flagged stale; it is NOT a `FR-DIS-` distractor. Distinguish "real but expired"77from "fake distractor."7879### A3. Per-option fields80- `freight_cost_usd` = `cost_usd`; `transit_days` = `transit_days_text` (copy the81 template's format — some want `"4-6 days"`, some want `"3-5"`; mirror the example82 value style in the template).83- `valid_until` = freight `valid_until`.84- `grand_total_usd = exw_total_usd + freight_cost_usd`.85- **Risk** from `route_risk`: uppercase it for `risk_level`/`customs_border_risk`86 (`low→LOW`, `medium→MEDIUM`, `high→HIGH`). For `risk_flag`:87 `low→NONE`, `medium→MEDIUM_BORDER_RISK`, `high→HIGH_BORDER_RISK`.88- **Validity** vs the quote date: `validity_status = VALID` if89 `valid_until >= quote_date`, else `STALE`. `source_is_stale` is `true` if the90 freight `status` is `stale` **or** it is expired on the quote date, else `false`.9192### A4. Recommended mode (decision basis — order matters)93Pick the mode to recommend by filtering, then optimizing:941. **Eligibility filter (feasibility/safety):** drop options that are STALE/expired95 on the quote date, and drop HIGH border-risk options. (MEDIUM risk is allowed but96 disfavored vs LOW.)972. **Among the eligible, prefer LOW route risk over MEDIUM.**983. **Tie-break by lowest `grand_total_usd`** (cheapest landed cost).99This typically yields **SEA** when AIR and SEA are both LOW/valid (SEA is cheaper)100and ROAD is medium/high or stale. Recommend by transport mode string (`"SEA"` etc).101Keep *risk/validity* separate from *recommendation*: you still list every real102option with its true flags even though only one is recommended.103104### A5. Policy / payment flags105- `freight_reconfirmation_required` = **true** always (policy106 `POL-FREIGHT-RECONFIRM`: freight rates reconfirm at final order).107- `all_freight_options_valid_on_quote_date` = true only if **all three** real108 options are VALID on the quote date (false if any is stale/expired).109- `payment_terms` from the customer + payment policies (see Payment terms table).110- `customer_policy` / segment label: map the customer `segment` to the controlled111 value the template shows (e.g. recurring NGO → `RECURRING_NGO`).112- `road_quote_invalid_or_stale` = true if the ROAD option is stale/expired.113- `freight_warning`: when something is stale or high-risk, write a short factual114 sentence: rates require reconfirmation at final order; name the stale freight ID,115 its expiry date, and its risk, and say it should not be used without a fresh116 quote. Keep it specific and grounded in the records.117118---119120## B. Module-only indicative quote (RFQ, EXW only)121122Inputs: an RFQ (`RFQ-...`) listing `requested_modules`, no confirmed destination.123Output: one EXW-only quote with one line **per requested module**.124125### B1. SOP1261. `GET /api/rfqs/<id>` → `customer_id`, `quote_date`, `requested_modules`127 (each `{product_code, quantity}`), `incoterm_requested`.1282. For **each requested module**, `GET /api/products/<code>`:129 - `article_number`, `unit_price` from the product price tier at the requested130 quantity (module products usually have a single open tier), `lead_time_days`,131 `shelf_life_months` from that tier/product.132 - `line_total = quantity * unit_price`.133 - Emit one line item per module, **in the RFQ's module order**.1343. `grand_total = sum(line_total)`.1354. `freight_excluded = true`, `quote_basis = "EXW_ONLY"` (policy `POL-INDICATIVE-EXW`136 + `POL-EXW-SCOPE`: no destination ⇒ EXW only, exclude freight).1375. `offer_validity_days = 30` (policy `POL-QUOTE-VALIDITY`).1386. `who_documentation_required = true` for these emergency-health-kit/WHO module RFQs.1397. `payment_terms`: new NGO ⇒ `PREPAY_100` (see Payment terms table).140141### B2. Critical exclusion — module lines ONLY142RFQs and products carry `component_composition_distractors` /143`components` (e.g. "Paracetamol tabs", "gauze and splints", "RDT buffer packs").144These are **medical-review context, not quote lines.** Never split a module into145component SKUs, never add component lines, never re-price by component. One line per146requested module, period (policy `POL-MODULE-GRANULARITY`). Quantities come straight147from the RFQ — do not infer extra units from composition tables.148149---150151## C. Engagement / receivables reconciliation (opportunity → cash → revenue)152153Inputs: an opportunity (`OPP-...`), its customer, a named contact, an as-of date,154and (usually) a linked event + voucher. Output: account status, per-milestone155state, revenue-recognition status, and routed follow-up tasks.156157### C1. Gather and link1581. `GET /api/opportunities/<id>` → `stage`, `won_amount_usd`, `phases[]`159 (`phase_id`, `amount_usd`, `invoice_id`, `completion_date`), `contact`,160 `outstanding_amount_usd`.1612. `GET /api/invoices`, `GET /api/payments`, `GET /api/revenue-journals`; filter162 each to this `opportunity_id` precisely (the collections contain many other163 accounts — match exactly). Link by `invoice_id` / `phase_id`.1643. Number milestones in phase order. Output milestone IDs in the convention the165 template uses — some templates label them `MS1/MS2/MS3` (ascending), others use166 the API `phase_id`. Match the template's enum/example exactly.167168### C2. Per-milestone derivation169- `invoice_total` / `amount` = invoice (or phase) `amount_usd`.170- **Payment state** from the matched payment(s) and invoice paid/outstanding:171 fully paid ⇒ `PAID`; partially paid ⇒ `PARTIAL`; none ⇒ `UNPAID`. `paid_amount`172 = sum of posted payments (or invoice `paid_amount_usd`); `amount_unpaid` =173 invoice `outstanding_amount_usd`.174- **`invoice_state`** (when the template asks): mirror the invoice `status`175 mapped to the template enum (paid⇒`PAID`, unpaid/overdue⇒`OPEN`, etc.).176- **Due-date nulling:** for a **PAID** milestone set `due_date` to `null` (it is177 settled — no due date is actionable). For an unpaid milestone use the invoice178 `due_date`.179- **Revenue recognition status** (policy `POL-REVREC` — recognize paid+complete180 milestones; unpaid stay outstanding):181 - PAID **and** a matching revenue journal exists ⇒ `RECOGNIZED`.182 - PAID **but no** revenue journal ⇒ recognition is *required but missing*183 (`REQUIRED_MISSING` or `MISSING_REVENUE_JOURNAL`, per template enum).184 - UNPAID ⇒ recognition not yet required ⇒ `NOT_REQUIRED_UNPAID`.185186### C3. Account-level rollups187- `opportunity_matches_milestones` / `opportunity_matches_phase_total` = true iff188 `won_amount == sum(phase amounts)`.189- `outstanding_balance` = sum of unpaid invoice outstanding amounts (= opportunity190 `outstanding_amount_usd`; verify they agree).191- `total_paid_amount` = sum of posted payments / paid invoice amounts.192- Contact: use the named contact; link it to this customer_id and opportunity_id.193194### C4. Revenue-recognition rollup block195- `recognized_milestones` = paid milestones that have a journal.196- `missing_required_milestones` = paid milestones lacking a journal.197- `recognized_amount` = sum of recognized milestone amounts.198- `recognition_status`: `COMPLETE_FOR_PAID_MILESTONES` if every paid milestone is199 recognized; `MISSING_FOR_PAID_MILESTONES` if any paid milestone lacks a journal;200 `NOT_REQUIRED` if nothing is paid yet. (Watch for the classic seeded case: a201 milestone that is **paid but has no journal** ⇒ flag missing, and the primary202 accounting action becomes "record revenue for that milestone".)203204### C5. Action routing (the decision layer)205Templates differ; produce the actions their enums demand. General logic:206207**Accounting / revenue action** — driven by the paid-but-unrecognized milestone:208- If a paid milestone lacks a journal ⇒ action `RECORD_REVENUE_MS<n>`, on that209 milestone, `amount` = its value, `debit_account = DEFERRED_REVENUE`,210 `credit_account = IMPLEMENTATION_SERVICES_REVENUE`, `owner_queue = ACCOUNTING`.211- If all paid milestones are already recognized ⇒ `VERIFY_REVENUE_ONLY`/212 `NO_ACCOUNTING_ACTION` per template (nothing to post).213214**Collection action** — driven by the unpaid milestone vs the as-of date:215- Unpaid and **not yet due** (`due_date > as_of_date`) ⇒ `MONITOR_UNPAID_NOT_DUE`216 / `COLLECT_UNPAID_MILESTONE` task but not overdue; owner `ACCOUNT_MANAGEMENT`.217- Unpaid and **due/overdue** (`due_date <= as_of_date`) ⇒ `SEND_COLLECTION_NOTICE`;218 owner `COLLECTIONS`.219- No unpaid milestone ⇒ `NO_COLLECTION_ACTION`.220- The collection task carries the unpaid milestone ID, its amount, and its due date.221222**Event / voucher invitation action** — from the linked event + voucher:223- `GET /api/events` and `GET /api/vouchers`; match the event by `opportunity_id`224 (or the ID named in the prompt) and the voucher by `event_id`/`code`.225- Map event `status` to the template enum (`scheduled→SCHEDULED`,226 `confirmed`/`live`→`ACTIVE`, `completed→COMPLETED`, etc.). Map voucher `status`227 (`active→ACTIVE`). `discount_amount` = voucher `discount_percent` value;228 `max_uses` = `max_redemptions`.229- If the event is upcoming/scheduled ⇒ invite action `SEND_EVENT_INVITATION` /230 `SEND_BRIEFING_INVITE`; owner `ACCOUNT_MANAGEMENT` (or `EVENTS`); attach231 `event_id`, `voucher_code`, contact, customer_id.232- Task titles: short, human, name the customer (e.g.233 `"Milestone 2 collection - <Customer Name>"`,234 `"Send celebration invite - <Customer Name>"`).235- Task `due_date`: collection tasks use the unpaid invoice due date; invitation236 tasks use a sensible pre-event date when the template expects one — prefer a date237 the records justify (e.g. shortly before the event); never invent far-future dates.238239**Customer name caveat:** use the customer `name` from the API record. The prompt's240friendly name may differ slightly (e.g. "Helios Health Alliance" in the prompt vs a241different stored name) — follow the convention the template/answer expects, which is242usually the human-readable account name the prompt uses for *titles* and the API243`name` for the canonical `customer_name` field. When in doubt, match the API record.244245---246247## Payment terms — controlled values248249Derive from customer `payment_profile` / `segment` and the payment policies:250251| Situation | Customer signal | `payment_terms` value | Policy |252|---|---|---|---|253| New NGO, no approved credit / first order | `segment: new_ngo`, `payment_profile: NEW_CLIENT_REVIEW`, `is_recurring:false` | `PREPAY_100` | POL-NEW-CLIENT-PAYMENT |254| Recurring NGO | `segment: recurring_ngo`, `is_recurring:true` | `NET_30_AFTER_PO` | POL-RECURRING-NGO-PAYMENT |255| Recurring commercial | `payment_profile: NET_30_AFTER_PO` | `NET_30_AFTER_PO` | (account terms) |256| Milestone implementation | `payment_profile: MILESTONE_BILLING` | milestone billing per phases | POL-REVREC |257258If grant terms restrict net terms, honor the restriction over the default.259260## Common misjudgments — do NOT do these261262- **Do not** quote `prior_unit_price_usd` or `prior_quote_quantity`; reprice from263 the current catalog tier at the confirmed quantity.264- **Do not** include `FR-DIS-*` freight rows or wrong-shipment-size rows just265 because the loose filter returned them. There are exactly three real lanes.266- **Do not** confuse "real but expired/stale" lanes (keep + flag) with fake267 `FR-DIS-` distractors (drop entirely).268- **Do not** split modules into component SKUs or add component lines — module269 line level only; composition tables are medical context.270- **Do not** put a `due_date` on a PAID milestone — null it.271- **Do not** mark a paid milestone `RECOGNIZED` if no revenue journal exists —272 that is the "missing journal" case that drives the record-revenue action.273- **Do not** recommend a stale/expired or HIGH-risk lane even if it is cheapest;274 eligibility (validity + risk) gates the cheapest-cost tie-break.275- **Do not** compare validity/staleness against today's real date — use the276 prompt's stated quote/as-of date.277- **Do not** invent IDs, amounts, dates, or enum values not present in the API or278 template; every value must trace to a record or a stated convention.279- **Do not** emit markdown, code fences, or explanation — only the JSON object that280 matches the template's structure exactly.281282## Quick verification before returning283- Every template key present, correct nesting/order, enums are literal allowed values.284- All money at required decimal precision; `grand_total = EXW + freight` and line285 sums reconcile; `outstanding = won - paid` and matches the opportunity.286- Exactly three real freight lanes; recommended mode is eligible (valid + not high287 risk) and lowest landed cost among eligibles.288- Paid milestones with journals = RECOGNIZED; paid without = missing → action set;289 unpaid = NOT_REQUIRED_UNPAID with due date carried into the collection task.