MedBridge Sales Ops — CRM Reconciliation & Quote Decision Skill
Reusable workflow rules for producing account-ready JSON answers against the MedBridge Sales Ops remote API. Covers the two task families: QUOTE tasks (reconcile a customer RFQ/quote → quote decision package) and RECONCILIATION tasks (reconcile a won opportunity → finance-ready reconciliation).
0. Environment & API Usage Rules
- The business source of truth is a remote HTTP JSON API at
API_BASE_URL(provided by the runner). Never read any localenv/directory; never start a local server. All business records come throughGET/POSTon the API. - Detail-by-id (
GET /api/<collection>/<id>) is only available for:customers,products,rfqs,quotes,freight-quotes,opportunities. For all other collections (invoices,payments,revenue-journals,events,vouchers,policies) use the listing endpointGET /api/<collection>or filtersGET /api/<collection>?key=value. - Filtering:
?key=valueis case-insensitive, matches nested keys, numeric tolerance 2dp; multiple filters AND together. - Cross-collection search:
GET /api/search?q=<text>returns up to 100 hits tagged withcollectionandid. Useful to locate an entity by name/fragment. - Revenue-journals may NOT filter correctly by
customer_id. Always filter revenue-journals byopportunity_id(e.g./api/revenue-journals?opportunity_id=OPP-TR-XXX). - Trust API records over narrative wording when they conflict, but read the prompt carefully for which entity (quote/opportunity/RFQ) to reconcile.
- Money values are USD with 2 decimals. Dates are ISO
YYYY-MM-DD. Use stable record IDs exactly as they appear in the API. - Always pipe JSON through
python3 -m json.toolto read it. Verify withGET /healthfirst.
1. QUOTE Tasks — Workflow
Applies when the task asks for a quote decision package (EXW pricing, freight options, route risk, recommended mode, payment terms).
Step 1 — Identify the quote/RFQ and customer
- Fetch the quote by id (
GET /api/quotes/<id>) or the RFQ (GET /api/rfqsthen filter). Confirmcustomer_id,quote_date,confirmed_quantity,product_code,incoterm. - Fetch the customer (
GET /api/customers/<id>) forpayment_profile,segment,is_recurring,grant_terms. - Fetch the product (
GET /api/products/<code>) forprice_tiers,shelf_life_months,article_number,family,cold_chain_required.
Step 2 — Select the catalog price tier by quantity
- The confirmed quantity selects the tier where
min_qty <= qty <= max_qty(treatmax_qty: nullas +∞). - CRITICAL pitfall:
prior_unit_price_usdon the quote line must NOT override the catalog tier price. Source notes like "catalog tier overrides prior unit price" confirm this. The prior price is a distractor. - Use the selected tier's
unit_price_usdandlead_time_days. shelf_life_monthscomes from the product record, not the tier.- Examples: 360 units → 300-499 tier; 1000 units → 900-1199 tier; 700 units → 500-899 tier (NOT the 900+ tier the source note may bait you toward).
Step 3 — Compute EXW pricing
exw_total_usd=confirmed_quantity × tier.unit_price_usd(round to 2dp).quote_basis:- If the incoterm/RFQ says "EXW plus freight options" →
"EXW"(freight included as separate options). - If the RFQ has no destination / says "EXW only" →
"EXW_ONLY"andfreight_excluded: true(no freight options at all).
- If the incoterm/RFQ says "EXW plus freight options" →
Step 4 — Gather and filter freight quotes
GET /api/freight-quotes?quote_id=<quote_id>.- Exclude distractor freight quotes: records with
idstartingFR-DIS-, ordestination= "Distractor route", or clearly wrong shipment size ("benchmark" in risk_notes) are distractors. Do NOT include them. - Include real freight options even if stale — a stale/expired real option must appear in the output with its stale/invalid status flagged (the account manager needs to see it), while distractors are silently dropped.
- For each included freight option, compute
grand_total_usd = exw_total_usd + freight_cost_usd.
Step 5 — Freight validity and risk fields
valid_until: the freight record'svalid_untildate.- Freight validity vs quote date: a freight option is valid on the quote
date only if
valid_until >= quote_dateANDstatus == "active". Ifvalid_until < quote_datethe option is expired; ifstatus == "stale"the source is stale. all_freight_options_valid_on_quote_date(train_001-style template):trueonly if every included real option is valid on the quote date.source_is_stale(train_004-style template):truewhen the freight record'sstatusis"stale".validity_status(train_004-style):"VALID"when active andvalid_until >= quote_date; otherwise the quote is expired/stale — use the status that reflects the date check (the quote'sstatusfield is the primary signal).freight_reconfirmation_required: alwaystruefor quotes with freight options (policy POL-FREIGHT-RECONFIRM: rates need reconfirmation at final order, valid only throughvalid_until).
Step 6 — Route risk enums
Map the API route_risk (lowercase low/medium/high) to the template
fields. Two template styles exist:
- train_001 style (separate fields):
risk_level:"LOW"/"MEDIUM"/"HIGH"risk_flag:"NONE"(low) /"MEDIUM_BORDER_RISK"(medium) / high → use the matching border-risk enum.
- train_004 style (single
customs_border_risk): use the uppercase risk level.
Step 7 — Recommended transport mode
- Rule: recommend the VALID (non-stale, non-distractor) freight option with
the LOWEST
grand_total_usd. - Exclude stale/expired/distractor options from contention, but do NOT narrow to low-risk only — cost is the deciding factor among valid options.
- If the cheapest valid option is medium-risk, it is still recommended (e.g. train_004: SEA at 81200 beats AIR at 97400 even though SEA is medium risk).
- If all real options are valid (train_001): the cheapest overall wins (SEA). ✗ Picking AIR "because it's low-risk/cold-chain-safe" is WRONG.
Step 8 — Payment terms
- New NGO clients (
is_recurring: false,status: prospect,segment: new_ngo) →"PREPAY_100"(policy POL-NEW-CLIENT-PAYMENT). - Recurring NGO/commercial clients → use the customer's
payment_profilevalue (e.g."NET_30_AFTER_PO") per POL-RECURRING-NGO-PAYMENT. offer_validity_days:30(policy POL-QUOTE-VALIDITY — catalog pricing valid 30 calendar days from quote date; applies to indicative quotes too).
Step 9 — Module-level quoting (module RFQs only)
- When
request_typeisindicative_module_quote/module_quote: keep ONE line item per requested module. Do NOT expand into component SKUs. component_composition_distractorson the RFQ are traps — they list components "for medical review only" to lure you into itemized pricing.- Each module product has its own
price_tiers,article_number,shelf_life_months; use them directly. - Also watch for distractor RFQs from the same customer with different quantities (e.g. an older budgetary RFQ with a different qty) — use the task-specified RFQ ID only.
Step 10 — WHO documentation flag
who_documentation_required: truefor IEHK /emergency_health_kitfamily modules (WHO-standardized Interagency Emergency Health Kits). Confirmed: setting itfalsedecreases the judge score.- Product records may not carry an explicit
who_documentation_requiredfield; infer it from the productfamily/ kit standard. Setting thisfalsefor WHO-standard kits is incorrect.
Step 11 — Client warnings block (train_004-style)
road_quote_invalid_or_stale:truewhen the road freight option is stale, expired, or high-risk.freight_warning: a short warning string describing the stale/expired road freight (date + risk). Keep it concise and factual.policy_terms:{quote_basis, payment_terms, freight_reconfirmation_required}mirroring the top-level values.
2. RECONCILIATION Tasks — Workflow
Applies when the task asks for a finance-ready engagement reconciliation of a won opportunity (milestones, invoices, payments, revenue journals, events, vouchers).
Step 1 — Fetch the opportunity and customer
GET /api/opportunities/<id>forstage,won_amount_usd,outstanding_amount_usd,phases[],contact.GET /api/customers/<id>forname,payment_profile,contacts.- Map
stage:closed_won→"WON",open→"OPEN",closed_lost→"LOST".
Step 2 — Fetch invoices, payments, revenue-journals
GET /api/invoices?opportunity_id=<id>— one invoice per phase.GET /api/payments?opportunity_id=<id>— payment records per invoice.GET /api/revenue-journals?opportunity_id=<id>— filter byopportunity_id, NOTcustomer_id(customer_id filter may silently return nothing or wrong results).
Step 3 — CRITICAL: milestone_id must be MS{n}
- Use
MS1,MS2,MS3, … asmilestone_id—MS+ 1-based phase number, ordered ascending. - ✗ DO NOT use the API
phase_id(e.g.HEL-P1,MER-P1) orinvoice_id(e.g.INV-HELIOS-P1). Even when a template's description says "stable phase or invoice milestone id", the expected value isMS{n}. - Why it matters: the evaluator aligns milestone arrays and follow-up-task
arrays by
milestone_id. Using the wrong id format causes a cascade failure — every milestone and task field is scored as mismatched even when the values are correct. (Observed: score jumped 0.267 → 0.733 just by switching toMS1/MS2.) phase_number= 1-based index of the phase.
Step 4 — Milestone fields
amount/invoice_total: the invoiceamount_usd(= phaseamount_usd).invoice_state(train_005 template): map invoicestatus—"paid"→"PAID","unpaid"→"OPEN".payment_state(train_005 template):"PAID"(fully paid),"UNPAID"(nothing paid),"PARTIAL"(partially paid).payment_status(train_003 template):"PAID"/"PARTIAL"/"UNPAID".paid_amount/amount_paid: invoicepaid_amount_usd.amount_unpaid: invoiceoutstanding_amount_usd.due_date:nullfor PAID milestones; the invoicedue_datefor UNPAID milestones. (Confirmed in train_005 at score 1.0.)
Step 5 — Revenue-recognition status mapping (per milestone)
- Paid milestone AND a matching revenue-journal exists (match by
phase_id/invoice_id) →"RECOGNIZED"(train_003) /"RECOGNIZED"(train_005). - Paid milestone AND NO revenue-journal found →
"REQUIRED_MISSING"(train_003 template) /"MISSING_REVENUE_JOURNAL"(train_005 template). - Unpaid milestone →
"NOT_REQUIRED_UNPAID"(both templates). - Missing-revenue-journal detection: compare the set of paid milestones
against the set of revenue-journal
phase_ids (orinvoice_ids). Any paid milestone with no matching journal is missing recognition. The opportunitynotesfield often hints at this ("Phase 2 is paid but revenue recognition journal is missing").
Step 6 — Revenue-recognition summary block (train_003 template)
recognition_status:"COMPLETE_FOR_PAID_MILESTONES"if every paid milestone is recognized."MISSING_FOR_PAID_MILESTONES"if any paid milestone lacks a journal."NOT_REQUIRED"if no milestones are paid.
recognized_milestones: list ofMS{n}ids that are recognized.missing_required_milestones: list ofMS{n}ids that are paid but missing a journal.recognized_amount: sum of recognized milestone amounts.
Step 7 — outstanding_balance computation
outstanding_balance=won_amount − total_paid_amount- = sum of unpaid milestone amounts
- = sum of invoice
outstanding_amount_usd - All three must agree; cross-check against the opportunity's
outstanding_amount_usd. total_paid_amount= sum of allpaid_amountacross milestones.phase_total_amount= sum of all phaseamount_usd; must equalwon_amount→opportunity_matches_phase_total: true.
Step 8 — Contact linkage
- Use the opportunity's
contactfield (a name) as the contact. - train_003 template:
contact: {name, linked_customer_id, linked_opportunity_id}. - train_005 template:
primary_contact: {contact_name, customer_id}. - All follow-up tasks (
collection_task,invite_task) must reference this contact name.
Step 9 — Accounting action (missing revenue journal)
- If a paid milestone is missing its revenue journal:
primary_accounting_action/accounting_action.action="RECORD_REVENUE_MS{n}"(with the specific milestone number).milestone_id="MS{n}".amount= the milestone's paid amount.debit_account="DEFERRED_REVENUE",credit_account="IMPLEMENTATION_SERVICES_REVENUE"(matching the existing posted journal pattern).owner_queue="ACCOUNTING".
- If all paid milestones are already recognized:
"VERIFY_REVENUE_ONLY"or"NO_ACCOUNTING_ACTION".
Step 10 — Collection action (unpaid milestones)
- Compare the current business date (given in the prompt, e.g. 2026-06-01)
against the unpaid milestone's invoice
due_date:- Current date before due_date →
action="MONITOR_UNPAID_NOT_DUE",owner_queue="ACCOUNT_MANAGEMENT". - Current date on/after due_date →
action="SEND_COLLECTION_NOTICE",owner_queue="COLLECTIONS".
- Current date before due_date →
collection_task.milestone_id="MS{n}"of the unpaid milestone.collection_task.amount= the unpaid amount.collection_task.due_date= the invoicedue_date.collection_task.contact_name= the opportunity contact.- If there are no unpaid milestones →
"NO_COLLECTION_ACTION".
Step 11 — Event & voucher actions
- Fetch the event (
GET /api/search?q=<event_id>or filter events) and its voucher (byvoucher_code). event_status: map eventstatus—scheduled→"SCHEDULED",active→"ACTIVE",completed→"COMPLETED",cancelled→"CANCELLED".voucher_status: map voucherstatus—active→"ACTIVE",draft→"DRAFT",expired→"EXPIRED",disabled→"DISABLED".discount_amount: use the voucher'sdiscount_percentvalue as a number (e.g. 50 for 50%, 100 for 100%).max_uses/voucher_max_uses: the voucher'smax_redemptions.invite_action:- Event scheduled and invite not yet sent (prompt says "before the
invitation goes out") →
"SEND_BRIEFING_INVITE". - Event already active/invite sent →
"VERIFY_INVITE_SENT". - No event →
"NO_INVITE_ACTION".
- Event scheduled and invite not yet sent (prompt says "before the
invitation goes out") →
invite_task.owner_queue:"ACCOUNT_MANAGEMENT"(matching the event'sfollow_up_owner), or"EVENTS".invite_taskcarriesevent_id,voucher_code,contact_name,customer_id.
3. Common Pitfalls Checklist
- ✗ Using
prior_unit_price_usdinstead of the catalog tier price. - ✗ Picking the wrong quantity tier (off-by-one tier boundary; using a superseded/distractor RFQ's quantity).
- ✗ Including distractor freight quotes (
FR-DIS-*/ "Distractor route"). - ✗ Recommending AIR "for safety" instead of the cheapest valid option.
- ✗ Using
phase_idorinvoice_idasmilestone_idinstead ofMS{n}— causes cascade scoring failure. - ✗ Putting a date in
due_datefor a PAID milestone (should benull). - ✗ Filtering revenue-journals by
customer_idinstead ofopportunity_id. - ✗ Setting
who_documentation_required: falsefor IEHK/WHO-standard kits. - ✗ Expanding module RFQs into component line items.
- ✗ Using
PREPAY_100for a recurring client, orNET_30_AFTER_POfor a new client. - ✗ Using
offer_validity_daysother than 30 for catalog quotes. - ✗ Mapping invoice
status:"unpaid"toinvoice_state:"UNPAID"— the correct enum value is"OPEN"(train_005 template). - ✗ Forgetting
freight_reconfirmation_required: trueon freight-bearing quotes. - ✗ Not detecting a missing revenue journal for a paid milestone (always cross-check paid milestones against the revenue-journal set).
- ✗ Leaving out the accounting action when a revenue journal is missing
(must emit
RECORD_REVENUE_MS{n}).
4. Output Discipline
- Return only valid JSON matching the task's
answer_template.json— same field names, controlled enum values, correct nesting, no extra fields, no markdown fences, no prose. - Use the exact enum strings declared in the template.
- Money as numbers with 2 decimals; dates as ISO
YYYY-MM-DD; booleans astrue/false; nulls asnull(not omitted). - Order milestones ascending by
milestone_id(MS1,MS2, …). - Order freight options consistently (AIR, SEA, ROAD) unless the template implies otherwise.