MedBridge Sales Ops — CRM Reconciliation & Quote Decision SOP
Transferable skill for two task families in the MedBridge Sales Ops domain:
- QUOTE tasks — reconcile a customer RFQ/quote against the API (customer, quote, product catalog tiers, freight quotes, policies) into an account-ready quote decision package (EXW pricing, freight options with grand totals, route risk flags, recommended transport mode, freight-validity warnings, payment terms).
- RECONCILIATION tasks — reconcile a won opportunity (CRM opportunity, milestone invoices, payments, revenue-recognition journals, events, vouchers) into a finance-ready reconciliation (paid/unpaid state, outstanding balance, revenue-recognition coverage, CRM follow-up tasks, event/voucher linkage).
All rules below were distilled from actually solving the 5 train tasks against the live remote API. They transfer to unseen test tasks.
0. Environment & API access
- Base URL:
<remote-env-url>(fromenvironment_access.md). All business records come ONLY through this HTTP API. Do not read any localenv/directory. GET /health— health check.GET /api— listscollectionsandendpoints.GET /api/<collection>— list all records ({collection, count, records}).GET /api/<collection>?<key>=<value>— case-insensitive filter; matches nested keys; multiple filters AND. Numeric tolerance 2dp.GET /api/<collection>/<id>— detail by id. ONLY available for:customers,products,rfqs,quotes,freight-quotes,opportunities. For all other collections use listing/filter or/api/search.GET /api/search?q=<text>— substring search across ALL collections (up to 100 hits, each tagged withcollection+id+ fullrecord). Best tool for finding linked events/vouchers by customer or opportunity name.- Collections:
customers,products,rfqs,quotes,freight-quotes,policies,opportunities,invoices,payments,revenue-journals,events,vouchers. - Money = USD, 2 decimals. Dates = ISO
YYYY-MM-DD. Use stable record IDs exactly as they appear. - Trust the API over prompt narrative when they conflict (e.g. prompt says "Health Horizon Aid" but API customer name is "HealthHands Alliance"; prompt says "GreenHarvest Labs" but API name is "Global Health Laboratories"). Always use API record values for output. But read the prompt carefully for WHICH entity (quote_id / opportunity_id / customer_id) to reconcile.
Query strategy by task family
- QUOTE task: fetch
quoteby id → getcustomer_id,primary_product_code/line items,confirmed_quantity,quote_date,destination. Then fetchcustomerby id,productby code,freight-quotes?quote_id=<quote_id>, and list/readpolicies. - RECONCILIATION task: fetch
opportunityby id → getcustomer_id, phases,won_amount_usd,stage,contact. Then:invoices?opportunity_id=<opp_id>(or?customer_id=)payments?opportunity_id=<opp_id>revenue-journals?opportunity_id=<opp_id>— see pitfall below/api/search?q=<customer/opportunity name>to find linkedeventsandvouchers, ORevents?customer_id=/vouchers?customer_id=.
1. QUOTE TASK SOP
1.1 Identify the entity & fetch records
- Read the prompt for the quote_id (e.g.
Q-TR-WC-1187,Q-TR-LD-5521) or rfq_id (e.g.RFQ-TR-IEHK-204). GET /api/quotes/<quote_id>(or/api/rfqs/<rfq_id>). Extract:customer_id,primary_product_code/line_items[].product_code,confirmed_quantity(quote) orrequested_modules[].quantity(rfq),quote_date,destination,incoterm.GET /api/customers/<customer_id>— getname,customer_type,is_recurring,segment,payment_profile,grant_terms,contacts.GET /api/products/<product_code>— getprice_tiers,shelf_life_months,cold_chain_required,article_number,components,family,unit.GET /api/freight-quotes?quote_id=<quote_id>— get all freight options (including distractors; filter per §1.5).GET /api/policies— read the relevant policy records (payment_terms, quote_scope, freight, quote_lines, incoterms, quote_validity).
1.2 Catalog tier by quantity (CRITICAL — overrides prior price)
- A product has
price_tierseach withmin_qty,max_qty(null = unbounded),unit_price_usd,lead_time_days. - Select the tier where
min_qty <= confirmed_quantity <= max_qty(treatmax_qty: nullas infinity). - Use THAT tier's
unit_price_usdandlead_time_days.shelf_life_monthsis product-level (same across tiers). - EXCLUDE the quote's
prior_unit_price_usd/ lineprior_quote_quantity— these are the OLD price at the OLD quantity. The prompt/source_notes explicitly state "catalog tier overrides prior unit price." Using the prior price is the #1 quote misjudgment. (Example: Q-TR-WC-1187 prior_unit_price 124.0 was the 150-299 tier; at 360 units the correct tier is 300-499 @ 118.0.) - For module RFQs (no quantity tiers, single tier with
min_qty:1/max_qty:null): unit_price is the flat module price; quantity comes from the RFQrequested_modules[].quantity.
1.3 EXW pricing & totals
quote_basis=EXWfor freight-option quotes;EXW_ONLYfor indicative quotes with no destination.- EXW excludes freight, insurance, import duty, customs clearance, and last-mile (policy
POL-EXW-SCOPE/EXW_EXCLUSIONS). exw_total_usd=unit_price_usd × confirmed_quantity(single-line quote), orΣ(unit_price × quantity)across module line items.- Per-freight
grand_total_usd=exw_total_usd + freight_cost_usd. - For EXW-only/module quotes:
freight_excluded = true,grand_total = exw_total(no freight added), because there is no destination (policyPOL-INDICATIVE-EXW/EXW_ONLY_EXCLUDE_FREIGHT).
1.4 Freight option selection & distractor exclusion
A freight-quotes?quote_id= response contains REAL options PLUS distractor/benchmark quotes. Select the real options and exclude distractors:
Exclude a freight quote when ANY of:
destinationcontains "Distractor" (e.g. "Distractor route") — it's a benchmark, not the real route.idcontainsDIS-(distractor naming convention) AND it does not match the 3 transport modes for the real destination.- It has wrong shipment dimensions that don't match the other real options (distractors often carry a different
shipment_cbm/shipment_weight_kgthan the consistent set).
Keep the quote's three transport modes (AIR, SEA, ROAD) for the real destination, even if one is stale/expired — a stale road quote is still REPORTED (with validity flags) because the customer asked to compare all three; it is just excluded from the recommendation.
1.5 Freight validity vs quote date
- A freight option is valid on the quote date iff
status == "active"ANDvalid_until >= quote_date. valid_until < quote_date→ EXPIRED.status == "stale"→ source is stale.- Report per-option:
valid_until(the date).validity_status/all_freight_options_valid_on_quote_date:VALIDif active and valid_until >= quote_date, elseEXPIRED(orSTALE).source_is_stale(bool) = (status == "stale").
freight_reconfirmation_required= always true (policyPOL-FREIGHT-RECONFIRM: "Freight rates need reconfirmation at final order and are valid only through the freight quote valid_until date."). Set true for every freight-bearing quote.road_quote_invalid_or_stale(train_004 template) = true when the ROAD freight option is stale or valid_until < quote_date.
1.6 Route risk enums & flags
Map the freight record's route_risk (lowercase: low/medium/high) to template enums (uppercase: LOW/MEDIUM/HIGH).
- train_001-style template (
risk_level+risk_flag):risk_level= uppercase(route_risk).risk_flag:NONEwhen low; derive fromrisk_noteswhen medium/high — e.g. notes mentioning "border risk medium" →MEDIUM_BORDER_RISK. UseNONEfor low-risk options with no specific flag.
- train_004-style template (
customs_border_risk):customs_border_risk= uppercase(route_risk). ROAD high-risk notes ("customs risk high") →HIGH.
1.7 Recommended transport mode rule
- Start from the REAL freight options (distractors excluded, §1.4).
- Filter to options VALID on the quote date (
status==activeANDvalid_until >= quote_date). Stale/expired options are never recommended. - If the product
cold_chain_required == true, keep only options withcold_chain_support == true. - Recommend the remaining option with the lowest
grand_total_usd. - Safety override: if that lowest-cost option has
route_risk == high, escalate to the next-cheapest valid option whose risk is not high.
Result on train data: Q-TR-WC-1187 → SEA (cheapest valid, low risk). Q-TR-LD-5521 → SEA (ROAD is stale/excluded; SEA cheapest valid + cold-chain supported). Risk levels are reported as flags but cost drives the recommendation unless a HIGH-risk safety valve triggers.
1.8 Payment terms & customer policy
Determine payment_terms from the customer record + policies:
- New NGO client (
is_recurring == false,segment == new_ngo,status == prospect,payment_profile == NEW_CLIENT_REVIEW,client_since == null) →PREPAY_100(policyPOL-NEW-CLIENT-PAYMENT: "New NGO clients require PREPAY_100 before production release."). Grant terms like "restricted donor review" reinforce prepay. - Recurring NGO (
is_recurring == true,segment == recurring_ngo,payment_profile == NET_30_AFTER_PO) →NET_30_AFTER_PO(policyPOL-RECURRING-NGO-PAYMENT), UNLESSgrant_termsexplicitly restrict net terms. - Recurring commercial (
segment == recurring_commercial,payment_profile == NET_30_AFTER_PO) →NET_30_AFTER_PO. customer_policyfield (where present) = the segment/policy that applied (e.g.recurring_ngo,new_ngo,recurring_commercial) or the policy id (POL-RECURRING-NGO-PAYMENT).
1.9 Module-level quote rule (no component expansion)
- For module RFQs (request_type
indicative_module_quote), quote at the module line level only (policyPOL-MODULE-GRANULARITY/MODULE_LINES). - DO NOT expand
product.componentsinto component line items, and ignorerfq.component_composition_distractors— those are explicitly "for medical review only." - Each
requested_modules[]entry = one line item:product_code,article_number(from product record),quantity,unit_price(single flat tier),lead_time_days,shelf_life_months,line_total = unit_price × quantity. grand_total = Σ line_total. EXW only, freight excluded (no destination).
1.10 WHO documentation flag
who_documentation_requiredis NOT a field on product records. Derive it from the product family/name:family == "emergency_health_kit"OR name contains "IEHK" (Interagency Emergency Health Kit, a WHO-standardized kit) → true.- Otherwise → false (or omit if the template has no such field).
- Set
offer_validity_days = 30(policyPOL-QUOTE-VALIDITY: "Catalog quote pricing is valid for 30 calendar days from quote date; freight validity may expire sooner.").
2. RECONCILIATION TASK SOP
2.1 Identify the entity & fetch records
- Prompt gives
opportunity_id(e.g.OPP-TR-HELIOS,OPP-TR-MERIDIAN) andcustomer_id. GET /api/opportunities/<opp_id>→customer_id,stage,won_amount_usd,outstanding_amount_usd,contact,phases[](each withphase_id,amount_usd,completion_date,invoice_id).GET /api/customers/<customer_id>→name,contacts,segment,payment_profile.GET /api/invoices?opportunity_id=<opp_id>→ one invoice per phase:amount_usd,status,paid_amount_usd,outstanding_amount_usd,due_date,phase_id,issue_date.GET /api/payments?opportunity_id=<opp_id>→ payments withinvoice_id,amount_usd,status,payment_date.GET /api/revenue-journals?opportunity_id=<opp_id>→ revenue journals keyed byphase_id/invoice_id. PITFALL: see §2.4./api/search?q=<customer or opportunity name>orevents?customer_id=/vouchers?customer_id=→ linked event + voucher.
2.2 Opportunity & milestone mapping
stage:closed_won→WON;open/in_progress→OPEN;lost/closed_lost→LOST.won_amount=opportunity.won_amount_usd.phase_total_amount=Σ phases[].amount_usd.opportunity_matches_phase_total/opportunity_matches_milestones= (won_amount == phase_total). True when they agree.- Milestones output in ascending phase order. Two id conventions seen:
- train_003-style:
milestone_id= the stable phase id (e.g.HEL-P1,HEL-P2) + aphase_number(1, 2, …). - train_005-style:
milestone_id=MS1/MS2/MS3(one per phase, ascending). Map phase 1 → MS1, etc. The enum is explicitlyMS1 | MS2 | MS3.
- train_003-style:
milestone.amount/invoice_total= the phaseamount_usd(== invoiceamount_usd).
2.3 Invoice & payment state mapping
Per milestone/phase, map invoice + payment data to output enums:
train_003-style (payment_status: PAID | PARTIAL | UNPAID):
- invoice
status == "paid"ANDpaid_amount_usd == amount_usd→PAID. paid_amount_usd > 0AND< amount_usd→PARTIAL.statusin("unpaid","overdue")ANDpaid_amount_usd == 0→UNPAID. (Overdue invoices are still unpaid.)amount_paid= invoicepaid_amount_usd;amount_unpaid= invoiceoutstanding_amount_usd.
train_005-style (invoice_state: PAID | OPEN | VOID | UNKNOWN; payment_state: PAID | PARTIAL | UNPAID | UNKNOWN):
invoice_state:paid→ PAID;unpaid/overdue→ OPEN;draft/anything not finalized → UNKNOWN (or VOID if explicitly voided).payment_state:paid+full → PAID; partial → PARTIAL;unpaid/overdue→ UNPAID;draft→ UNKNOWN.
2.4 Revenue-recognition state mapping (CRITICAL PITFALL)
PITFALL — revenue-journals has NO customer_id field. Filtering revenue-journals?customer_id=<id> returns count: 0 even when journals exist. You MUST filter by opportunity_id (or invoice_id/phase_id), or list all revenue-journals and match on phase_id/invoice_id, or use /api/search?q=<name>. A returned count of 0 from a customer_id filter is a false negative — do NOT conclude "no revenue recognized" from it.
A revenue-journal record looks like: {id, opportunity_id, invoice_id, phase_id, amount_usd, debit_account:"Deferred Revenue", credit_account:"Implementation Services Revenue", posted_date, status:"posted", memo}.
Per-milestone recognition_status:
- Milestone PAID (or PARTIAL with a payment) AND a posted revenue-journal exists for its
phase_id/invoice_id→RECOGNIZED(train_005) /RECOGNIZED(train_003). - Milestone PAID but NO revenue-journal found →
MISSING_REVENUE_JOURNAL(train_005) /REQUIRED_MISSING(train_003). This is the key train_005 case: Meridian P2 is paid (PAY-MERIDIAN-P2 posted) but has no RJ-MERIDIAN-P2. PolicyPOL-REVRECrequires recognizing revenue when a milestone is complete and paid. - Milestone UNPAID/PARTIAL-not-fully-paid →
NOT_REQUIRED_UNPAID(recognition not required until paid). No unpaid milestone should be flagged missing a journal. - Unknown/no invoice →
UNKNOWN.
Overall recognition_status / recognition_status (revenue_recognition block):
- All PAID milestones have journals →
COMPLETE_FOR_PAID_MILESTONES. - At least one PAID milestone lacks a journal →
MISSING_FOR_PAID_MILESTONES. - No paid milestones at all →
NOT_REQUIRED. recognized_milestones= list of phase ids with posted journals;missing_required_milestones= paid phase ids lacking journals;recognized_amount=Σof posted journal amounts.
Examples: HELIOS → P1 paid+recognized, P2 unpaid → COMPLETE_FOR_PAID_MILESTONES, recognized=[HEL-P1], missing=[], recognized_amount=50000. MERIDIAN → P1 recognized, P2 paid+MISSING, P3 unpaid → MISSING_FOR_PAID_MILESTONES, recognized=[MER-P1](or MS1), missing=[MS2], recognized_amount=30000.
2.5 Outstanding balance & paid totals
outstanding_balance=Σ invoice.outstanding_amount_usdacross all milestone invoices ==opportunity.outstanding_amount_usd(they should agree).total_paid_amount=Σ invoice.paid_amount_usd==Σ payment.amount_usd(posted payments).won_amountshould equaltotal_paid + outstanding_balance(and equalphase_total).
2.6 Due-date convention — PAID milestones get null due_date
- OUTPUT convention: in the milestones array,
due_date= null for PAID milestones (the obligation is settled; "due_date when applicable" means only outstanding milestones carry a due date). For UNPAID/PARTIAL/open milestones,due_date= the invoicedue_date. - The API invoices all carry a populated
due_dateregardless of status — nulling it is an output formatting rule, not an API fact. - Examples: HELIOS P1 (paid) → due_date null; P2 (unpaid) → 2026-06-27. MERIDIAN MS1/MS2 (paid) → null; MS3 (unpaid) → 2026-07-15.
2.7 Follow-up task routing (collection vs accounting vs event-invitation)
Distinguish THREE routing lanes; do not conflate them:
(A) COLLECTION lane — for any milestone with amount_unpaid > 0. Task ties to the unpaid milestone + the account contact.
- train_003-style (
follow_up_tasks[]):task_type=COLLECTION,next_action=COLLECT_UNPAID_MILESTONE,milestone_id=<unpaid phase id>,amount_due=<outstanding>,due_date=<invoice due_date>,event_id=null,voucher_code=null,contact_name=<account contact>,linked_customer_id,linked_opportunity_id. - train_005-style (
collection_task+collection_actionenum:MONITOR_UNPAID_NOT_DUE | SEND_COLLECTION_NOTICE | NO_COLLECTION_ACTION):- Establish the
as_of_datefrom the prompt (e.g. 2026-06-01). - Unpaid milestone whose
due_date <= as_of_date(or invoicestatus=="overdue") →SEND_COLLECTION_NOTICE,owner_queue=COLLECTIONS. - Unpaid milestone whose
due_date > as_of_date(not yet due) →MONITOR_UNPAID_NOT_DUE,owner_queue=ACCOUNT_MANAGEMENT. (MERIDIAN MS3 due 2026-07-15 as of 2026-06-01 → MONITOR.) - No unpaid milestone →
NO_COLLECTION_ACTION, milestone_idNONE, owner_queueNONE.
- Establish the
(B) ACCOUNTING lane — revenue recognition. primary_accounting_action / accounting_action enum: RECORD_REVENUE_MS2 | VERIFY_REVENUE_ONLY | NO_ACCOUNTING_ACTION.
- If a PAID milestone lacks a revenue journal →
RECORD_REVENUE_<MSn>. Action object:milestone_id=<that MSn>,amount=<milestone amount>,debit_account=DEFERRED_REVENUE,credit_account=IMPLEMENTATION_SERVICES_REVENUE,owner_queue=ACCOUNTING. (MERIDIAN MS2 paid+missing →RECORD_REVENUE_MS2, 45000, debit DEFERRED_REVENUE, credit IMPLEMENTATION_SERVICES_REVENUE, ACCOUNTING.) The debit/credit accounts come straight from existing posted journal records in the API. - If all paid milestones already have journals →
VERIFY_REVENUE_ONLY(verify, don't create),milestone_id=NONE, amount 0, debit/creditNONE, owner_queueACCOUNTING. - If no paid milestones →
NO_ACCOUNTING_ACTION.
(C) EVENT-INVITATION lane — for the linked event/voucher.
- train_003-style:
task_type=EVENT_INVITATION,next_action=SEND_EVENT_INVITATION,event_id=<event id>,voucher_code=<voucher code>,milestone_id=null,amount_due=null,due_date=<event date or pre-event date>, contact = account contact. - train_005-style (
invite_actionenum:SEND_BRIEFING_INVITE | VERIFY_INVITE_SENT | NO_INVITE_ACTION;invite_task):- Event
status=="scheduled"/confirmedAND voucherredemptions_used == 0(invite not yet sent) →SEND_BRIEFING_INVITE(MERIDIAN briefing: scheduled, 0 redemptions). - Invite already sent (redemptions > 0 / status shows invited) →
VERIFY_INVITE_SENT. - No linked event →
NO_INVITE_ACTION. invite_task.owner_queue= map eventfollow_up_owner("Account Management" →ACCOUNT_MANAGEMENT; "Events" →EVENTS).contact_name= account contact;customer_id= customer;event_id+voucher_codecarried through.
- Event
2.8 Event & voucher linkage
- Event record:
{id, customer_id, opportunity_id, event_date, status, primary_contact, follow_up_owner, voucher_code, name}. - Voucher record:
{code, customer_id, opportunity_id, event_id, discount_percent, max_redemptions, redemptions_used, status, valid_until, description}. - Output mapping:
event_id,event_datefrom event.voucher_codefrom event.voucher_code (== voucher.code).voucher_discount= voucherdiscount_percent(an integer percent, e.g. 50 or 100).voucher_max_uses/voucher_max_uses= vouchermax_redemptions.event_statusenum (SCHEDULED|ACTIVE|COMPLETED|CANCELLED|UNKNOWN): mapscheduled→SCHEDULED,confirmed→ACTIVE,completed→COMPLETED,cancelled→CANCELLED.voucher_statusenum (ACTIVE|DRAFT|EXPIRED|DISABLED|UNKNOWN): map voucherstatus("active"→ACTIVE, "draft"→DRAFT, "expired"→EXPIRED, "disabled"→DISABLED).
2.9 Contact linkage
- The account contact named in the prompt (e.g. "Mara Okafor", "Daniel Rees") must appear on every follow-up task and in the primary_contact block.
- Source:
opportunity.contactand/orcustomer.contacts[].name(match by name). Tie tocustomer_idandopportunity_id. - train_003
contactblock:{name, linked_customer_id, linked_opportunity_id}. - train_005
primary_contactblock:{contact_name, customer_id}.
3. Output field definitions & controlled enums (reference)
QUOTE — train_001 style (quote_summary + freight_options + policy_flags)
quote_summary:quote_id,customer_id,quote_date,product_code,confirmed_quantity,unit_price_usd(tier price),lead_time_days(tier),shelf_life_months(product),quote_basis="EXW",exw_total_usd.freight_options[](one per real mode AIR/SEA/ROAD):freight_id,mode(AIR|SEA|ROAD),freight_cost_usd,transit_days(use the APItransit_days_texte.g. "4-6 days"),valid_until,risk_level(LOW|MEDIUM|HIGH),risk_flag(NONE|MEDIUM_BORDER_RISK|…),grand_total_usd.policy_flags:recommended_mode,freight_reconfirmation_required(true),all_freight_options_valid_on_quote_date(bool),customer_policy,payment_terms.
QUOTE — train_002 style (module/EXW-only)
quote_header:rfq_id,customer_id,quote_date,currency="USD",quote_basis="EXW_ONLY".line_items[]:product_code,article_number,quantity,unit_price,lead_time_days,shelf_life_months,line_total.quote_controls:grand_total(= Σ line_total),freight_excluded=true,payment_terms(PREPAY_100 for new NGO),offer_validity_days=30,who_documentation_required=true for IEHK/emergency_health_kit.
QUOTE — train_004 style (pricing + transport_decisions + client_warnings)
pricing:quote_id,customer_id,quote_date,product_code,confirmed_quantity,catalog_tier{min_quantity, max_quantity, unit_price_usd, lead_time_days, shelf_life_months},exw_total_usd,payment_terms.transport_decisions.freight_options[]:freight_id,mode,freight_cost_usd,transit_days,valid_until,validity_status(VALID|EXPIRED|STALE),source_is_stale(bool),customs_border_risk(LOW|MEDIUM/HIGH),grand_total_usd.recommended_mode,freight_reconfirmation_required(true).client_warnings:road_quote_invalid_or_stale(bool),freight_warning(string),policy_terms{quote_basis, payment_terms, freight_reconfirmation_required}.
RECONCILIATION — train_003 style
account_status: customer_id, customer_name, opportunity_id, opportunity_stage (WON|OPEN|LOST), won_amount, opportunity_matches_milestones (bool), outstanding_balance, contact{name, linked_customer_id, linked_opportunity_id}.milestones[]: milestone_id (phase id), phase_number, invoice_total, payment_status (PAID|PARTIAL|UNPAID), amount_paid, amount_unpaid, due_date (null if paid), revenue_recognition_status (RECOGNIZED|REQUIRED_MISSING|NOT_REQUIRED_UNPAID).revenue_recognition: recognition_status (COMPLETE_FOR_PAID_MILESTONES|MISSING_FOR_PAID_MILESTONES|NOT_REQUIRED), recognized_milestones[], missing_required_milestones[], recognized_amount.event: event_id, event_date, voucher_code, voucher_discount (discount_percent), voucher_max_uses (max_redemptions).follow_up_tasks[]: task_type (COLLECTION|EVENT_INVITATION), task_title, linked_customer_id, linked_opportunity_id, contact_name, due_date, next_action (COLLECT_UNPAID_MILESTONE|SEND_EVENT_INVITATION), milestone_id (or null), amount_due (or null), event_id (or null), voucher_code (or null).
RECONCILIATION — train_005 style
engagement_reconciliation: as_of_date, opportunity_id, customer_id, customer_name, stage (WON|OPEN|LOST), won_amount, phase_total_amount, opportunity_matches_phase_total (bool), total_paid_amount, outstanding_balance, primary_contact{contact_name, customer_id}, milestones[] {milestone_id (MS1|MS2|MS3 ascending), amount, invoice_state (PAID|OPEN|VOID|UNKNOWN), payment_state (PAID|PARTIAL|UNPAID|UNKNOWN), paid_amount, due_date (null if paid), recognition_status (RECOGNIZED|MISSING_REVENUE_JOURNAL|NOT_REQUIRED_UNPAID|UNKNOWN)}.invoice_actions: primary_accounting_action (RECORD_REVENUE_MS2|VERIFY_REVENUE_ONLY|NO_ACCOUNTING_ACTION), collection_action (MONITOR_UNPAID_NOT_DUE|SEND_COLLECTION_NOTICE|NO_COLLECTION_ACTION), accounting_action{action, milestone_id(MS1|MS2|MS3|NONE), amount, debit_account(DEFERRED_REVENUE|ACCOUNTS_RECEIVABLE|CASH|NONE), credit_account(IMPLEMENTATION_SERVICES_REVENUE|DEFERRED_REVENUE|ACCOUNTS_RECEIVABLE|NONE), owner_queue(ACCOUNTING|ACCOUNT_MANAGEMENT|NONE)}, collection_task{action, milestone_id, amount, due_date, owner_queue(ACCOUNT_MANAGEMENT|COLLECTIONS|NONE), contact_name}.event_actions: event_id, event_status (SCHEDULED|ACTIVE|COMPLETED|CANCELLED|UNKNOWN), voucher{voucher_code, voucher_status(ACTIVE|DRAFT|EXPIRED|DISABLED|UNKNOWN), discount_amount, max_uses}, invite_action (SEND_BRIEFING_INVITE|VERIFY_INVITE_SENT|NO_INVITE_ACTION), invite_task{action, event_id, voucher_code, owner_queue(ACCOUNT_MANAGEMENT|EVENTS|NONE), contact_name, customer_id}.
4. Common misjudgments & exclusions (checklist)
- Using prior_unit_price instead of catalog tier. The quote's
prior_unit_price_usd/prior_quote_quantityis the OLD price at the OLD qty. Always recompute the tier fromconfirmed_quantityagainstproduct.price_tiers. - EXW-only quotes including freight. Indicative/module quotes with no destination are EXW_ONLY —
freight_excluded=true, no freight options,grand_total=exw_total. Only quotes with a destination carry freight options. - EXW total including freight.
exw_totalis product-only (excludes freight/insurance/duty/customs/last-mile per POL-EXW-SCOPE).grand_total = exw_total + freight_costper option, separately. - Expanding module RFQs into component lines. Module RFQs stay at module line level (POL-MODULE-GRANULARITY).
product.componentsandrfq.component_composition_distractorsare distractors — do not emit component SKUs. - Treating distractor freight quotes as real options. Exclude
destinationcontaining "Distractor",idwithDIS-, or wrong shipment dimensions. But KEEP a stale ROAD option (it's a real mode the customer asked to compare) — just flag it stale/expired. - Recommending a stale/expired freight option. Stale (
status=="stale") orvalid_until < quote_dateoptions are never the recommended mode. - Forgetting freight reconfirmation.
freight_reconfirmation_requiredis always true (POL-FREIGHT-RECONFIRM) for any freight-bearing quote. - Filtering revenue-journals by customer_id. That collection has NO
customer_idfield — the filter returns 0 (false negative). Useopportunity_id(orinvoice_id/phase_id), list-all-and-match, or/api/search. Concluding "no journal exists" from a customer_id filter = wrong. - Flagging an UNPAID milestone as missing a revenue journal. Recognition is only required for PAID milestones. Unpaid →
NOT_REQUIRED_UNPAID, neverMISSING. - Missing a PAID milestone that lacks a journal. A paid milestone with no matching revenue-journal (by phase_id/invoice_id) →
MISSING_REVENUE_JOURNAL/REQUIRED_MISSINGand an accountingRECORD_REVENUEaction. (Meridian P2 is the canonical case.) - Due_date on paid milestones. Output
due_date=nullfor PAID milestones; only outstanding milestones carry the invoice due_date. - New-client payment terms. New NGO (not recurring, prospect, NEW_CLIENT_REVIEW, client_since null) →
PREPAY_100, not net terms, even if they're an NGO. Grant-term restrictions reinforce prepay. - WHO documentation flag. Derive
who_documentation_requiredfromfamily=="emergency_health_kit"/IEHK name — it's not a product-record field. True for IEHK modules. - offer_validity_days. = 30 (POL-QUOTE-VALIDITY), not the freight valid_until.
- Narrative vs API name mismatch. Prompt customer names are often paraphrased ("Health Horizon Aid" vs API "HealthHands Alliance"; "GreenHarvest Labs" vs API "Global Health Laboratories"). Use the API
name. Match the entity by the ID given in the prompt. - Conflating collection / accounting / event-invitation routing. Collection = unpaid milestone (owner COLLECTIONS if overdue, ACCOUNT_MANAGEMENT if not-yet-due). Accounting = missing revenue journal for a PAID milestone (owner ACCOUNTING, debit DEFERRED_REVENUE / credit IMPLEMENTATION_SERVICES_REVENUE). Event-invitation = linked event/voucher (owner from event.follow_up_owner). Each is a separate task; an unpaid milestone does not create an accounting action, and a missing journal does not create a collection task.
5. Step-by-step execution recipe
For a QUOTE task
- Parse quote_id/rfq_id + confirmed quantity + quote_date + destination from prompt.
GET /api/quotes/<id>(or/rfqs/<id>);GET /api/customers/<customer_id>;GET /api/products/<product_code>;GET /api/freight-quotes?quote_id=<id>;GET /api/policies.- Select catalog tier by quantity; compute
exw_total = unit_price × qty. - If destination present: select real freight options (exclude distractors); for each compute
grand_total = exw_total + freight_cost; map risk + validity. Pick recommended_mode (§1.7). Setfreight_reconfirmation_required=true. - If no destination (EXW_ONLY):
freight_excluded=true,grand_total=exw_total, no freight options. - Determine
payment_termsfrom customer segment + policies (§1.8). For module RFQ setwho_documentation_required(§1.10) andoffer_validity_days=30. - Fill the template exactly (field names, enums, nesting). Output JSON only.
For a RECONCILIATION task
- Parse opportunity_id + customer_id (+ as_of_date if given) + account contact name from prompt.
GET /api/opportunities/<opp_id>;GET /api/customers/<customer_id>;GET /api/invoices?opportunity_id=<opp_id>;GET /api/payments?opportunity_id=<opp_id>;GET /api/revenue-journals?opportunity_id=<opp_id>;/api/search?q=<name>for events/vouchers.- Map stage → WON/OPEN/LOST; verify
won_amount == Σ phase amounts. - For each phase (ascending): map invoice_state/payment_state; set paid_amount/outstanding; set due_date=null if paid else invoice.due_date; set recognition_status by checking for a matching revenue-journal (§2.4).
- Compute total_paid, outstanding_balance, overall recognition_status.
- Route follow-ups: COLLECTION for unpaid milestones (MONITOR vs NOTICE by as_of_date vs due_date); ACCOUNTING for paid+missing-journal milestones (RECORD_REVENUE, DEFERRED_REVENUE→IMPLEMENTATION_SERVICES_REVENUE, ACCOUNTING); EVENT_INVITATION for the linked event+voucher (SEND vs VERIFY by redemptions/status).
- Fill the template exactly. Output JSON only (no prose, no markdown fences).