SKILL: MedBridge Sales-Ops — B2B Quote & Engagement-Reconciliation Solver
What this is / when it applies
You get ONE task: an email-style prompt.txt from an account manager plus an
answer_template.json. You must return ONLY a JSON object that exactly matches
the template (same keys, same shape, no markdown, no prose). All business facts
come from a single read-only HTTP API ("MedBridge Sales Ops"). There are no gold
answers and no judge — you must derive the answer yourself from live API data.
The domain has THREE task families (detect by the template + prompt):
- EXW + freight decision package — a confirmed catalog quote with a
destination; output catalog pricing + 3 freight options + recommended mode +
policy flags. (template has
freight_options[] with AIR/SEA/ROAD and a
policy_flags/transport_decisions block.)
- Module / RFQ EXW-only quote — an indicative RFQ with NO destination;
output module-level line items, EXW only, freight excluded. (template has
quote_header.quote_basis = "EXW_ONLY", line_items[], quote_controls.)
- CRM milestone / engagement reconciliation — reconcile a won opportunity's
phases vs invoices/payments/revenue-journals, plus a linked event+voucher,
then route follow-up tasks. (template has
milestones[], revenue_recognition,
event, follow_up_tasks / invoice_actions / event_actions.)
The API
Base URL: provided by the runner (env var like API_BASE_URL/BASE_URL/PORT,
or for reference <remote-env-url>). Read-only JSON over GET.
Use curl or python urllib (it is a NETWORK resource, querying is expected).
Collections (list = /api/<coll>, single = /api/<coll>/<id>):
customers, products, rfqs, quotes, freight-quotes, policies, opportunities, invoices, payments, revenue-journals, events, vouchers.
Also GET /api/search?q=<text> = full-text across all collections (great for
resolving an ID mentioned in the prompt to its record and linked records).
Filtering: list endpoints accept ?field=value, nested keys flattened, matched
case-insensitively, numbers also match 2-decimal form. KNOWN GOTCHAS:
freight-quotes are linked to quotes by quote_id, NOT customer_id.
Use ?quote_id=<QUOTE_ID> (filtering freight by customer_id returns 0).
invoices, payments, revenue-journals carry both opportunity_id and
phase_id (and invoice_id). Filter by ?opportunity_id=<OPP> or just pull
the whole small collection and match in code.
Resolving IDs from the prompt
The prompt names the key record IDs directly (quote Q-..., rfq RFQ-...,
opportunity OPP-..., customer CUST-..., event EVT-..., voucher code).
Fetch that record first, then follow its foreign keys:
- quote →
customer_id, primary_product_code, confirmed_quantity, quote_date.
- rfq →
customer_id, requested_modules[] (each product_code+quantity).
- opportunity →
customer_id, phases[] (each phase_id, amount_usd,
invoice_id, completion_date, name), won_amount_usd, stage, contact.
Use the API record's own values (e.g. customer name) even if the prompt's
wording differs slightly (prompts sometimes paraphrase the customer name).
FAMILY 1 — EXW + freight decision package (SOP)
Example shape: Q-TR-WC-1187, Q-TR-LD-5521.
- GET the quote. Read
customer_id, primary_product_code,
confirmed_quantity (use the CONFIRMED/revised qty, not prior_quote_quantity),
quote_date, destination.
- GET the product. Pick the price tier whose
[min_qty, max_qty] contains
the confirmed quantity (max_qty: null = open-ended top tier). From that tier
take unit_price_usd, lead_time_days, and the product's shelf_life_months.
The confirmed qty's tier OVERRIDES any prior unit price in the quote.
exw_total_usd = unit_price_usd * confirmed_quantity (round 2 decimals).
- GET freight via
?quote_id=<QUOTE_ID>. Keep exactly the real AIR/SEA/ROAD
option for this quote and DROP distractors. A record is a distractor if any:
id contains DIS, destination == "Distractor route", or status is
stale/mismatch (also expired valid_until). Keep status: active ones
that match the real destination/shipment. (You expect one each AIR/SEA/ROAD;
a stale/expired ROAD is still REPORTED but flagged — see below.)
- For each kept option compute
grand_total_usd = exw_total_usd + cost_usd.
Map fields: freight_id=id, mode = uppercased mode
(air→AIR, sea→SEA, road→ROAD), freight_cost_usd=cost_usd,
transit_days = transit_days_text (string like "4-6 days"),
valid_until, plus risk fields (below).
- Risk fields.
route_risk (low/medium/high) → risk_level
(LOW/MEDIUM/HIGH). risk_flag / customs_border_risk:
- low route_risk →
NONE (or "" / LOW depending on template default).
- medium route_risk with border/customs note →
MEDIUM_BORDER_RISK.
- high route_risk →
HIGH_BORDER_RISK / HIGH.
Follow the template's own enum spelling and per-row defaults: the template's
placeholder rows usually pre-fill the expected risk_level/risk_flag per
mode — honor that pattern.
- Validity / staleness. A freight option is VALID on the quote date iff
valid_until >= quote_date. If a ROAD (or any) quote's status is stale
or valid_until < quote_date, it is stale/expired:
validity_status = "VALID" vs "EXPIRED"/"STALE"; source_is_stale=true;
- set the family's stale-warning boolean (e.g.
road_quote_invalid_or_stale)
= true and write a freight_warning describing it (e.g. road quote expired
before quote date / customs risk high — reconfirm before PO).
all_freight_options_valid_on_quote_date = true only if EVERY reported
option is valid on the quote date.
- recommended_mode. Recommend the cheapest (lowest
grand_total_usd)
option that is BOTH valid on the quote date AND low route-risk (avoid
medium/high risk and stale options). Respect cold-chain: if the product has
cold_chain_required: true, the recommended option must have
cold_chain_support: true. (Worked examples: WC-360 → SEA is lowest-cost
LOW-risk valid → recommend SEA; LD-1000 with stale/high ROAD and medium SEA →
AIR is the cheapest LOW-risk valid cold-chain option → recommend AIR.)
- Policy flags.
freight_reconfirmation_required = true (policy
POL-FREIGHT-RECONFIRM — freight always reconfirmed at order). quote_basis
= "EXW" / "EXW plus freight options" per template. payment_terms &
customer_policy: see Payment-terms rules below.
FAMILY 2 — Module / RFQ EXW-only quote (SOP)
Example: RFQ-TR-IEHK-204 (new NGO, no destination).
- GET the rfq. Read
customer_id, quote_date, requested_modules[].
- Quote at module line level only. IGNORE component-composition /
distractor tables — do NOT split modules into component SKUs (policy
POL-MODULE-GRANULARITY). One
line_items[] entry per requested module.
- For each module GET the product:
product_code (=code), article_number,
quantity (from the RFQ line), single-tier unit_price,
lead_time_days, shelf_life_months, and
line_total = unit_price * quantity.
grand_total = sum(line_total). currency="USD",
quote_basis="EXW_ONLY", freight_excluded=true (no destination → EXW only,
policy POL-INDICATIVE-EXW / POL-EXW-SCOPE).
payment_terms = PREPAY_100 for a NEW NGO / new-client account
(payment_profile: NEW_CLIENT_REVIEW, policy POL-NEW-CLIENT-PAYMENT).
offer_validity_days = 30 (POL-QUOTE-VALIDITY, catalog pricing valid 30
calendar days). who_documentation_required = true for NGO/emergency-health
(WHO/donor docs) unless the data says otherwise.
FAMILY 3 — CRM milestone / engagement reconciliation (SOP)
Examples: OPP-TR-HELIOS, OPP-TR-MERIDIAN.
- GET the opportunity + customer.
stage: closed_won→WON, open→OPEN,
lost→LOST. won_amount = won_amount_usd. customer_name from the customer
record. primary_contact = opportunity contact (also the prompt names them).
- Phase ↔ milestone totals.
phase_total_amount = sum of phase
amount_usd. opportunity_matches_milestones/opportunity_matches_phase_total
= (won_amount == phase_total). (Both worked examples matched.)
- For EACH phase, gather its invoice (by
invoice_id/phase_id), its
payment(s), and its revenue-journal:
invoice_total/amount = invoice amount_usd.
invoice_state: invoice status → PAID/OPEN(unpaid)/VOID; map
unpaid→UNPAID/OPEN, paid→PAID, draft→OPEN/UNKNOWN, overdue→OPEN
(still unpaid). Use the template's enum set.
paid_amount/amount_paid = invoice paid_amount_usd (or sum of posted
payments for that invoice). amount_unpaid = outstanding_amount_usd.
payment_state: PAID if fully paid, PARTIAL if 0<paid<amount, else UNPAID.
due_date: invoice due_date. CONVENTION: for a PAID milestone many
templates expect due_date: null (it is settled — no due date applies);
for UNPAID/open milestones report the invoice due_date. Honor whichever
the template's wording implies; default = null once paid.
recognition_status per phase:
- paid milestone WITH a revenue-journal →
RECOGNIZED.
- paid milestone WITHOUT a revenue-journal →
REQUIRED_MISSING /
MISSING_REVENUE_JOURNAL.
- unpaid milestone →
NOT_REQUIRED_UNPAID (recognize only paid+complete
milestones — policy POL-REVREC).
- Rollups.
total_paid_amount = sum of fully/partly paid amounts.
outstanding_balance = sum of unpaid invoice outstanding =
opportunity outstanding_amount_usd. recognized_amount = sum of amounts of
milestones that actually have posted revenue-journals.
recognition_status (engagement-level):
- all paid milestones recognized →
COMPLETE_FOR_PAID_MILESTONES.
- some paid milestone missing its journal →
MISSING_FOR_PAID_MILESTONES.
- no paid milestones →
NOT_REQUIRED.
- Event + voucher. GET the event (by id or
?opportunity_id=) and its
voucher_code → GET voucher. event_date, voucher_discount/discount_amount
= discount_percent, voucher_max_uses/max_uses = max_redemptions.
event_status: live→ACTIVE, scheduled→SCHEDULED, confirmed→SCHEDULED,
completed→COMPLETED, cancelled→CANCELLED, else UNKNOWN.
voucher_status: active→ACTIVE, draft→DRAFT, expired→EXPIRED,
disabled→DISABLED, else UNKNOWN.
- Action routing (use the task's
as_of_date/current business date — read
it from the prompt, e.g. "treat 2026-06-01 as current"):
- Accounting action: if a paid milestone is MISSING its revenue journal →
RECORD_REVENUE_<MSx> (e.g. RECORD_REVENUE_MS2), milestone_id=that MS,
amount=its amount, debit_account=DEFERRED_REVENUE,
credit_account=IMPLEMENTATION_SERVICES_REVENUE, owner_queue=ACCOUNTING.
If all paid milestones already recognized → VERIFY_REVENUE_ONLY (or
NO_ACCOUNTING_ACTION) with milestone NONE / amount 0 / accounts NONE.
- Collection action: take the earliest UNPAID milestone with a future
invoice. If its
due_date is in the future (as-of < due) →
MONITOR_UNPAID_NOT_DUE, owner ACCOUNT_MANAGEMENT. If overdue
(as-of > due) → SEND_COLLECTION_NOTICE, owner COLLECTIONS. If nothing
unpaid → NO_COLLECTION_ACTION/NONE. Fill milestone_id, amount,
due_date, contact_name.
- Event/invite action: scheduled/confirmed event not yet sent →
SEND_BRIEFING_INVITE/SEND_EVENT_INVITATION, owner ACCOUNT_MANAGEMENT,
with event_id, voucher_code, contact_name, customer_id. Completed →
VERIFY/NO action.
follow_up_tasks[] (HELIOS-style template): emit ONE COLLECTION task per
unpaid-and-due milestone (next_action=COLLECT_UNPAID_MILESTONE,
amount_due, milestone_id, due_date=invoice due date) and ONE
EVENT_INVITATION task (next_action=SEND_EVENT_INVITATION, event_id,
voucher_code, due_date=event_date or a sensible lead date), both tied to
the named account contact and customer/opportunity IDs.
Milestone-ID naming (IMPORTANT, differs by template)
- If the template's
milestone_id enum is MS1 | MS2 | MS3, NORMALIZE: order
phases ascending by phase order and label them MS1, MS2, MS3 (NOT the raw
phase_id). The accounting action then says e.g. RECORD_REVENUE_MS2.
- If the template says "stable phase or invoice milestone id", use the raw
phase_id (e.g. HEL-P1) or invoice id as given.
Output conventions (all families)
- Return ONLY the JSON object matching the template — no markdown fences, no
commentary. Keep every key in the template; do not add keys.
- Money: numbers with 2 decimals (e.g.
42480.0), USD. Totals MUST reconcile:
exw_total = unit_price*qty; grand_total = exw_total + freight_cost;
phase_total = sum(phase amounts); outstanding = sum(unpaid outstanding);
recognized_amount = sum(amounts with posted journals).
- Dates: ISO
YYYY-MM-DD. Enums: use EXACTLY the controlled values listed in
that template field's annotation (their spelling/casing wins over this doc).
null usage: use null where the template says "string or null" / "number or
null" and the fact doesn't apply (e.g. paid-milestone due_date,
no-collection milestone_id/amount, no-event fields). Don't invent zeros/blanks
where null is the right "not applicable" marker.
transit_days is a STRING (e.g. "4-6 days"), taken from transit_days_text.
Common misjudgments to avoid
- Filtering freight by
customer_id (returns 0) — use quote_id.
- Including distractor / stale / wrong-shipment freight rows (
id has DIS,
destination "Distractor route", status stale/mismatch). Keep the real
active AIR/SEA/ROAD for the quote.
- Using
prior_quote_quantity/prior_unit_price_usd instead of the CONFIRMED
quantity and its catalog tier (the tier overrides prior pricing).
- Expanding module RFQs into component SKUs — keep module-level lines only.
- Adding freight to an indicative/no-destination RFQ — EXW only, freight excluded.
- Recommending a cheaper but stale/expired or higher-risk freight mode — the
recommendation must be valid AND low-risk (and cold-chain-safe if required).
- Recognizing revenue for UNPAID milestones — recognize only PAID+complete ones;
unpaid = NOT_REQUIRED_UNPAID and drives collection (not accounting) tasks.
- Treating a paid milestone that is simply missing its journal as "complete" —
that is the MISSING/REQUIRED_MISSING case and triggers RECORD_REVENUE.
- Confusing collection vs accounting routing: missing-journal-on-paid → ACCOUNTING
(record revenue); unpaid invoice → ACCOUNT_MANAGEMENT/COLLECTIONS (collect).
- Not normalizing milestone IDs to MS1/MS2/MS3 when the template's enum demands it.
Quick reference: policies (from /api/policies)
- POL-NEW-CLIENT-PAYMENT: new NGO → PREPAY_100 before production.
- POL-RECURRING-NGO-PAYMENT: recurring NGO → NET_30_AFTER_PO (unless grant
restricts). Recurring commercial milestone-billing accounts likewise follow
their
payment_profile.
- POL-INDICATIVE-EXW: no destination → EXW only, exclude freight.
- POL-FREIGHT-RECONFIRM: freight valid only to
valid_until; reconfirm at order.
- POL-MODULE-GRANULARITY: quote module RFQs at module line level.
- POL-REVREC: recognize revenue only for paid+complete milestones; unpaid future
milestones stay outstanding and drive collection tasks when due/overdue.
- POL-QUOTE-VALIDITY: catalog pricing valid 30 calendar days from quote date.
- POL-EXW-SCOPE: EXW excludes freight, insurance, duty, customs, last-mile.
Payment terms by account (set payment_terms/customer_policy)
Derive from the customer's payment_profile + segment, cross-checked vs policy:
NEW_CLIENT_REVIEW / new NGO → PREPAY_100 (POL-NEW-CLIENT-PAYMENT).
NET_30_AFTER_PO recurring NGO/commercial → NET_30_AFTER_PO
(POL-RECURRING-NGO-PAYMENT); name the matching policy id in customer_policy.
MILESTONE_BILLING implementation-services accounts → milestone billing terms.
Always prefer the customer record's stated payment_profile; use the policy to
confirm/justify it.
1---2name: self-attempt-01-213description: SKILL: MedBridge Sales-Ops — B2B Quote & Engagement-Reconciliation Solver4---5# SKILL: MedBridge Sales-Ops — B2B Quote & Engagement-Reconciliation Solver67## What this is / when it applies8You get ONE task: an email-style `prompt.txt` from an account manager plus an9`answer_template.json`. You must return ONLY a JSON object that exactly matches10the template (same keys, same shape, no markdown, no prose). All business facts11come from a single read-only HTTP API ("MedBridge Sales Ops"). There are no gold12answers and no judge — you must derive the answer yourself from live API data.1314The domain has THREE task families (detect by the template + prompt):151. **EXW + freight decision package** — a confirmed catalog quote with a16 destination; output catalog pricing + 3 freight options + recommended mode +17 policy flags. (template has `freight_options[]` with AIR/SEA/ROAD and a18 `policy_flags`/`transport_decisions` block.)192. **Module / RFQ EXW-only quote** — an indicative RFQ with NO destination;20 output module-level line items, EXW only, freight excluded. (template has21 `quote_header.quote_basis = "EXW_ONLY"`, `line_items[]`, `quote_controls`.)223. **CRM milestone / engagement reconciliation** — reconcile a won opportunity's23 phases vs invoices/payments/revenue-journals, plus a linked event+voucher,24 then route follow-up tasks. (template has `milestones[]`, `revenue_recognition`,25 `event`, `follow_up_tasks` / `invoice_actions` / `event_actions`.)2627---2829## The API30Base URL: provided by the runner (env var like `API_BASE_URL`/`BASE_URL`/`PORT`,31or for reference `<remote-env-url>`). Read-only JSON over GET.32Use `curl` or python `urllib` (it is a NETWORK resource, querying is expected).3334Collections (list = `/api/<coll>`, single = `/api/<coll>/<id>`):35`customers, products, rfqs, quotes, freight-quotes, policies, opportunities,36invoices, payments, revenue-journals, events, vouchers`.37Also `GET /api/search?q=<text>` = full-text across all collections (great for38resolving an ID mentioned in the prompt to its record and linked records).3940Filtering: list endpoints accept `?field=value`, nested keys flattened, matched41case-insensitively, numbers also match 2-decimal form. KNOWN GOTCHAS:42- `freight-quotes` are linked to quotes by **`quote_id`**, NOT `customer_id`.43 Use `?quote_id=<QUOTE_ID>` (filtering freight by customer_id returns 0).44- `invoices`, `payments`, `revenue-journals` carry both `opportunity_id` and45 `phase_id` (and `invoice_id`). Filter by `?opportunity_id=<OPP>` or just pull46 the whole small collection and match in code.4748### Resolving IDs from the prompt49The prompt names the key record IDs directly (quote `Q-...`, rfq `RFQ-...`,50opportunity `OPP-...`, customer `CUST-...`, event `EVT-...`, voucher code).51Fetch that record first, then follow its foreign keys:52- quote → `customer_id`, `primary_product_code`, `confirmed_quantity`, `quote_date`.53- rfq → `customer_id`, `requested_modules[]` (each `product_code`+`quantity`).54- opportunity → `customer_id`, `phases[]` (each `phase_id`, `amount_usd`,55 `invoice_id`, `completion_date`, `name`), `won_amount_usd`, `stage`, `contact`.56Use the **API record's own values** (e.g. customer `name`) even if the prompt's57wording differs slightly (prompts sometimes paraphrase the customer name).5859---6061## FAMILY 1 — EXW + freight decision package (SOP)62Example shape: `Q-TR-WC-1187`, `Q-TR-LD-5521`.63641. GET the quote. Read `customer_id`, `primary_product_code`,65 `confirmed_quantity` (use the CONFIRMED/revised qty, not `prior_quote_quantity`),66 `quote_date`, `destination`.672. GET the product. Pick the **price tier** whose `[min_qty, max_qty]` contains68 the confirmed quantity (`max_qty: null` = open-ended top tier). From that tier69 take `unit_price_usd`, `lead_time_days`, and the product's `shelf_life_months`.70 The confirmed qty's tier OVERRIDES any prior unit price in the quote.713. `exw_total_usd = unit_price_usd * confirmed_quantity` (round 2 decimals).724. GET freight via `?quote_id=<QUOTE_ID>`. Keep exactly the **real** AIR/SEA/ROAD73 option for this quote and DROP distractors. A record is a distractor if any:74 `id` contains `DIS`, `destination == "Distractor route"`, or `status` is75 `stale`/`mismatch` (also `expired` valid_until). Keep `status: active` ones76 that match the real destination/shipment. (You expect one each AIR/SEA/ROAD;77 a stale/expired ROAD is still REPORTED but flagged — see below.)785. For each kept option compute `grand_total_usd = exw_total_usd + cost_usd`.79 Map fields: `freight_id=id`, `mode` = uppercased `mode`80 (air→AIR, sea→SEA, road→ROAD), `freight_cost_usd=cost_usd`,81 `transit_days = transit_days_text` (string like "4-6 days"),82 `valid_until`, plus risk fields (below).836. **Risk fields.** `route_risk` (low/medium/high) → `risk_level`84 (LOW/MEDIUM/HIGH). `risk_flag` / `customs_border_risk`:85 - low route_risk → `NONE` (or "" / LOW depending on template default).86 - medium route_risk with border/customs note → `MEDIUM_BORDER_RISK`.87 - high route_risk → `HIGH_BORDER_RISK` / `HIGH`.88 Follow the template's own enum spelling and per-row defaults: the template's89 placeholder rows usually pre-fill the expected `risk_level`/`risk_flag` per90 mode — honor that pattern.917. **Validity / staleness.** A freight option is VALID on the quote date iff92 `valid_until >= quote_date`. If a ROAD (or any) quote's `status` is `stale`93 or `valid_until < quote_date`, it is stale/expired:94 - `validity_status` = "VALID" vs "EXPIRED"/"STALE"; `source_is_stale=true`;95 - set the family's stale-warning boolean (e.g. `road_quote_invalid_or_stale`)96 = true and write a `freight_warning` describing it (e.g. road quote expired97 before quote date / customs risk high — reconfirm before PO).98 `all_freight_options_valid_on_quote_date` = true only if EVERY reported99 option is valid on the quote date.1008. **recommended_mode.** Recommend the **cheapest** (lowest `grand_total_usd`)101 option that is BOTH valid on the quote date AND low route-risk (avoid102 medium/high risk and stale options). Respect cold-chain: if the product has103 `cold_chain_required: true`, the recommended option must have104 `cold_chain_support: true`. (Worked examples: WC-360 → SEA is lowest-cost105 LOW-risk valid → recommend SEA; LD-1000 with stale/high ROAD and medium SEA →106 AIR is the cheapest LOW-risk valid cold-chain option → recommend AIR.)1079. **Policy flags.** `freight_reconfirmation_required = true` (policy108 POL-FREIGHT-RECONFIRM — freight always reconfirmed at order). `quote_basis`109 = "EXW" / "EXW plus freight options" per template. `payment_terms` &110 `customer_policy`: see Payment-terms rules below.111112## FAMILY 2 — Module / RFQ EXW-only quote (SOP)113Example: `RFQ-TR-IEHK-204` (new NGO, no destination).1141151. GET the rfq. Read `customer_id`, `quote_date`, `requested_modules[]`.1162. Quote at **module line level only**. IGNORE component-composition /117 distractor tables — do NOT split modules into component SKUs (policy118 POL-MODULE-GRANULARITY). One `line_items[]` entry per requested module.1193. For each module GET the product: `product_code` (=code), `article_number`,120 `quantity` (from the RFQ line), single-tier `unit_price`,121 `lead_time_days`, `shelf_life_months`, and122 `line_total = unit_price * quantity`.1234. `grand_total = sum(line_total)`. `currency="USD"`,124 `quote_basis="EXW_ONLY"`, `freight_excluded=true` (no destination → EXW only,125 policy POL-INDICATIVE-EXW / POL-EXW-SCOPE).1265. `payment_terms` = `PREPAY_100` for a NEW NGO / new-client account127 (`payment_profile: NEW_CLIENT_REVIEW`, policy POL-NEW-CLIENT-PAYMENT).1286. `offer_validity_days` = 30 (POL-QUOTE-VALIDITY, catalog pricing valid 30129 calendar days). `who_documentation_required` = true for NGO/emergency-health130 (WHO/donor docs) unless the data says otherwise.131132## FAMILY 3 — CRM milestone / engagement reconciliation (SOP)133Examples: `OPP-TR-HELIOS`, `OPP-TR-MERIDIAN`.1341351. GET the opportunity + customer. `stage`: `closed_won`→WON, open→OPEN,136 lost→LOST. `won_amount` = `won_amount_usd`. `customer_name` from the customer137 record. `primary_contact` = opportunity `contact` (also the prompt names them).1382. **Phase ↔ milestone totals.** `phase_total_amount` = sum of phase139 `amount_usd`. `opportunity_matches_milestones`/`opportunity_matches_phase_total`140 = (won_amount == phase_total). (Both worked examples matched.)1413. For EACH phase, gather its invoice (by `invoice_id`/`phase_id`), its142 payment(s), and its revenue-journal:143 - `invoice_total`/`amount` = invoice `amount_usd`.144 - `invoice_state`: invoice `status` → PAID/OPEN(unpaid)/VOID; map145 `unpaid`→UNPAID/OPEN, `paid`→PAID, `draft`→OPEN/UNKNOWN, `overdue`→OPEN146 (still unpaid). Use the template's enum set.147 - `paid_amount`/`amount_paid` = invoice `paid_amount_usd` (or sum of posted148 payments for that invoice). `amount_unpaid` = `outstanding_amount_usd`.149 - `payment_state`: PAID if fully paid, PARTIAL if 0<paid<amount, else UNPAID.150 - `due_date`: invoice `due_date`. CONVENTION: for a PAID milestone many151 templates expect `due_date: null` (it is settled — no due date applies);152 for UNPAID/open milestones report the invoice `due_date`. Honor whichever153 the template's wording implies; default = null once paid.154 - `recognition_status` per phase:155 * paid milestone WITH a revenue-journal → `RECOGNIZED`.156 * paid milestone WITHOUT a revenue-journal → `REQUIRED_MISSING` /157 `MISSING_REVENUE_JOURNAL`.158 * unpaid milestone → `NOT_REQUIRED_UNPAID` (recognize only paid+complete159 milestones — policy POL-REVREC).1604. **Rollups.** `total_paid_amount` = sum of fully/partly paid amounts.161 `outstanding_balance` = sum of unpaid invoice outstanding =162 opportunity `outstanding_amount_usd`. `recognized_amount` = sum of amounts of163 milestones that actually have posted revenue-journals.164 `recognition_status` (engagement-level):165 * all paid milestones recognized → `COMPLETE_FOR_PAID_MILESTONES`.166 * some paid milestone missing its journal → `MISSING_FOR_PAID_MILESTONES`.167 * no paid milestones → `NOT_REQUIRED`.1685. **Event + voucher.** GET the event (by id or `?opportunity_id=`) and its169 `voucher_code` → GET voucher. `event_date`, `voucher_discount`/`discount_amount`170 = `discount_percent`, `voucher_max_uses`/`max_uses` = `max_redemptions`.171 `event_status`: live→ACTIVE, scheduled→SCHEDULED, confirmed→SCHEDULED,172 completed→COMPLETED, cancelled→CANCELLED, else UNKNOWN.173 `voucher_status`: active→ACTIVE, draft→DRAFT, expired→EXPIRED,174 disabled→DISABLED, else UNKNOWN.1756. **Action routing** (use the task's `as_of_date`/current business date — read176 it from the prompt, e.g. "treat 2026-06-01 as current"):177 - **Accounting action**: if a paid milestone is MISSING its revenue journal →178 `RECORD_REVENUE_<MSx>` (e.g. RECORD_REVENUE_MS2), `milestone_id`=that MS,179 `amount`=its amount, `debit_account=DEFERRED_REVENUE`,180 `credit_account=IMPLEMENTATION_SERVICES_REVENUE`, `owner_queue=ACCOUNTING`.181 If all paid milestones already recognized → `VERIFY_REVENUE_ONLY` (or182 NO_ACCOUNTING_ACTION) with milestone NONE / amount 0 / accounts NONE.183 - **Collection action**: take the earliest UNPAID milestone with a future184 invoice. If its `due_date` is in the future (as-of < due) →185 `MONITOR_UNPAID_NOT_DUE`, owner `ACCOUNT_MANAGEMENT`. If overdue186 (as-of > due) → `SEND_COLLECTION_NOTICE`, owner `COLLECTIONS`. If nothing187 unpaid → `NO_COLLECTION_ACTION`/NONE. Fill `milestone_id`, `amount`,188 `due_date`, `contact_name`.189 - **Event/invite action**: scheduled/confirmed event not yet sent →190 `SEND_BRIEFING_INVITE`/`SEND_EVENT_INVITATION`, owner `ACCOUNT_MANAGEMENT`,191 with `event_id`, `voucher_code`, `contact_name`, `customer_id`. Completed →192 VERIFY/NO action.193 - `follow_up_tasks[]` (HELIOS-style template): emit ONE COLLECTION task per194 unpaid-and-due milestone (`next_action=COLLECT_UNPAID_MILESTONE`,195 `amount_due`, `milestone_id`, `due_date`=invoice due date) and ONE196 EVENT_INVITATION task (`next_action=SEND_EVENT_INVITATION`, `event_id`,197 `voucher_code`, `due_date`=event_date or a sensible lead date), both tied to198 the named account contact and customer/opportunity IDs.199200### Milestone-ID naming (IMPORTANT, differs by template)201- If the template's `milestone_id` enum is `MS1 | MS2 | MS3`, NORMALIZE: order202 phases ascending by phase order and label them MS1, MS2, MS3 (NOT the raw203 `phase_id`). The accounting action then says e.g. `RECORD_REVENUE_MS2`.204- If the template says "stable phase or invoice milestone id", use the raw205 `phase_id` (e.g. `HEL-P1`) or invoice id as given.206207---208209## Output conventions (all families)210- Return ONLY the JSON object matching the template — no markdown fences, no211 commentary. Keep every key in the template; do not add keys.212- Money: numbers with 2 decimals (e.g. `42480.0`), USD. Totals MUST reconcile:213 `exw_total = unit_price*qty`; `grand_total = exw_total + freight_cost`;214 `phase_total = sum(phase amounts)`; `outstanding = sum(unpaid outstanding)`;215 `recognized_amount = sum(amounts with posted journals)`.216- Dates: ISO `YYYY-MM-DD`. Enums: use EXACTLY the controlled values listed in217 that template field's annotation (their spelling/casing wins over this doc).218- `null` usage: use null where the template says "string or null" / "number or219 null" and the fact doesn't apply (e.g. paid-milestone due_date,220 no-collection milestone_id/amount, no-event fields). Don't invent zeros/blanks221 where null is the right "not applicable" marker.222- `transit_days` is a STRING (e.g. "4-6 days"), taken from `transit_days_text`.223224## Common misjudgments to avoid225- Filtering freight by `customer_id` (returns 0) — use `quote_id`.226- Including distractor / stale / wrong-shipment freight rows (`id` has DIS,227 destination "Distractor route", `status` stale/mismatch). Keep the real228 active AIR/SEA/ROAD for the quote.229- Using `prior_quote_quantity`/`prior_unit_price_usd` instead of the CONFIRMED230 quantity and its catalog tier (the tier overrides prior pricing).231- Expanding module RFQs into component SKUs — keep module-level lines only.232- Adding freight to an indicative/no-destination RFQ — EXW only, freight excluded.233- Recommending a cheaper but stale/expired or higher-risk freight mode — the234 recommendation must be valid AND low-risk (and cold-chain-safe if required).235- Recognizing revenue for UNPAID milestones — recognize only PAID+complete ones;236 unpaid = NOT_REQUIRED_UNPAID and drives collection (not accounting) tasks.237- Treating a paid milestone that is simply missing its journal as "complete" —238 that is the MISSING/REQUIRED_MISSING case and triggers RECORD_REVENUE.239- Confusing collection vs accounting routing: missing-journal-on-paid → ACCOUNTING240 (record revenue); unpaid invoice → ACCOUNT_MANAGEMENT/COLLECTIONS (collect).241- Not normalizing milestone IDs to MS1/MS2/MS3 when the template's enum demands it.242243## Quick reference: policies (from /api/policies)244- POL-NEW-CLIENT-PAYMENT: new NGO → PREPAY_100 before production.245- POL-RECURRING-NGO-PAYMENT: recurring NGO → NET_30_AFTER_PO (unless grant246 restricts). Recurring commercial milestone-billing accounts likewise follow247 their `payment_profile`.248- POL-INDICATIVE-EXW: no destination → EXW only, exclude freight.249- POL-FREIGHT-RECONFIRM: freight valid only to `valid_until`; reconfirm at order.250- POL-MODULE-GRANULARITY: quote module RFQs at module line level.251- POL-REVREC: recognize revenue only for paid+complete milestones; unpaid future252 milestones stay outstanding and drive collection tasks when due/overdue.253- POL-QUOTE-VALIDITY: catalog pricing valid 30 calendar days from quote date.254- POL-EXW-SCOPE: EXW excludes freight, insurance, duty, customs, last-mile.255256### Payment terms by account (set `payment_terms`/`customer_policy`)257Derive from the customer's `payment_profile` + segment, cross-checked vs policy:258- `NEW_CLIENT_REVIEW` / new NGO → `PREPAY_100` (POL-NEW-CLIENT-PAYMENT).259- `NET_30_AFTER_PO` recurring NGO/commercial → `NET_30_AFTER_PO`260 (POL-RECURRING-NGO-PAYMENT); name the matching policy id in `customer_policy`.261- `MILESTONE_BILLING` implementation-services accounts → milestone billing terms.262Always prefer the customer record's stated `payment_profile`; use the policy to263confirm/justify it.