MedBridge Sales Ops API — Transferable SKILL.md
1. Environment & API Entrypoint
- The API base URL is provided by the runner (env,
API_BASE_URL, BASE_URL, http://34.46.77.124:8002, etc.). Always use the runner-provided URL.
- All endpoints are read-only
GET. No authentication required in this staging environment.
- Root discovery:
GET /api returns the collection list and all available endpoints.
2. API Collections & Navigation Pattern
| Collection |
Endpoint |
Join Keys |
customers |
/api/customers, /api/customers/<id> |
id |
products |
/api/products, /api/products/<code> |
code |
rfqs |
/api/rfqs, /api/rfqs/<id> |
id, customer_id |
quotes |
/api/quotes, /api/quotes/<id> |
id, customer_id |
freight-quotes |
/api/freight-quotes, /api/freight-quotes/<id> |
id, quote_id |
policies |
/api/policies |
id, terms_code |
opportunities |
/api/opportunities, /api/opportunities/<id> |
id, customer_id |
invoices |
/api/invoices |
id, customer_id, opportunity_id, phase_id |
payments |
/api/payments |
id, customer_id, invoice_id, opportunity_id |
revenue-journals |
/api/revenue-journals |
id, invoice_id, opportunity_id, phase_id |
events |
/api/events |
id, customer_id, opportunity_id |
vouchers |
/api/vouchers |
code, customer_id, event_id, opportunity_id |
search |
/api/search?q=<text> |
Cross-collection full-text search |
Usage habit: First fetch the collection list, then directly fetch individual records by their known IDs. For multi-entity tasks, use /api/search?q=<id> to pull all records linked to a customer, quote, or opportunity in one call.
3. Product Catalog & Price Tier Resolution
Tier Selection Rule
Every product has price_tiers[] with min_qty, max_qty, and unit_price_usd. To select the correct tier:
- Find the tier where
confirmed_quantity falls in [min_qty, max_qty]. max_qty: null means no upper bound.
- Catalog tier always overrides any prior unit price. A quote's
prior_unit_price_usd is historical context only — never use it for the current quote. The source_notes field on quotes often explicitly states this ("catalog tier overrides prior unit price").
- Single-tier products (min_qty=1, max_qty=null) are common for IEHK and cholera modules — no tier logic needed.
Fields Retrieved from the Matched Tier
unit_price_usd → unit price for the quote
lead_time_days → lead time in days
- Also required from product root:
shelf_life_months, article_number, cold_chain_required
Calculation: EXW Total
exw_total_usd = confirmed_quantity × unit_price_usd
Round to 2 decimal places (cents). All amounts are in USD.
4. Freight Quote Filtering & Validation
Primary Filter
Freight records link to quotes via freight-quotes.quote_id. Always filter freight by quote_id matching the quote being processed.
Status-Based Filtering
Freight records have a status field:
active — valid for use; check valid_until against quote_date
stale — expired, do not use; its valid_until is before quote_date
mismatch — wrong shipment size/weight (based on prior quantity); do not use
Rule: Only use freight where status == "active" AND valid_until >= quote_date. If valid_until < quote_date, the freight is stale/invalid regardless of status value.
Distractor Detection
The API contains distractor freight records (IDs prefixed FR-DIS-*) that share a quote_id with the target quote but have:
- Wrong
shipment_cbm or shipment_weight_kg (mismatched to actual shipment)
status: "stale" with valid_until before the quote date
- Different
forwarder or destination than the real routes
route_risk notes indicating they are benchmarks or archived estimates
Pitfall: Freight for a given quote_id returns ALL records — not just the 3 valid ones. You MUST filter by status and valid_until.
Validity Status Label
For each freight option, determine:
"valid" — status is active AND valid_until >= quote_date
"stale" — valid_until < quote_date OR status is stale
source_is_stale: true when valid_until < quote_date
Grand Total Calculation
grand_total_usd = exw_total_usd + freight_cost_usd
5. Route Risk & Mode Recommendation
Risk Assessment
route_risk: "low" → risk_level: "LOW", no flag
route_risk: "medium" → risk_level: "MEDIUM", flag based on notes (e.g., MEDIUM_BORDER_RISK)
route_risk: "high" → risk_level: "HIGH", flag based on notes (e.g., HIGH_BORDER_RISK, CUSTOMS_RISK)
risk_flag: "NONE" for low-risk routes
Recommended Mode Heuristic
- If any freight is stale/invalid, it disqualifies that mode
- Prefer mode with lowest risk → then fastest transit
- For cold-chain products (
cold_chain_required: true), air is strongly preferred (temperature control, shorter transit within shelf-life window)
- If road has border risk or is stale, do not recommend road
- Sea is rarely recommended for cold-chain or urgent deliveries due to long transit
Freight Reconfirmation
Policy POL-FREIGHT-RECONFIRM: All freight rates must be reconfirmed at final order. The freight_reconfirmation_required flag should be true when any freight has a valid_until that could expire before the customer's PO date, or when road risks are flagged. In practice, this is almost always true for revision quotes.
All-Freight-Valid Check
Check whether ALL freight options are valid on the quote date:
all_valid = all(f.valid_until >= quote_date AND f.status == "active" for f in freight_options)
6. Customer Record & Payment Terms
Customer Lookup
- Start from the quote/RFQ/opportunity's
customer_id, then fetch /api/customers/<customer_id>
- The
segment field determines policy application: recurring_ngo, new_ngo, recurring_commercial, government_program, implementation_services, etc.
Payment Term Resolution
Match the customer's payment_profile and segment against policies:
| Customer Segment/Payment Profile |
Payment Terms |
Policy |
new_ngo / NEW_CLIENT_REVIEW |
PREPAY_100 |
POL-NEW-CLIENT-PAYMENT |
recurring_ngo / NET_30_AFTER_PO |
NET_30_AFTER_PO |
POL-RECURRING-NGO-PAYMENT |
recurring_commercial / NET_30_AFTER_PO |
NET_30_AFTER_PO |
(from payment_profile) |
government_program / NET_30_AFTER_PO |
NET_30_AFTER_PO |
(from payment_profile) |
implementation_services / MILESTONE_BILLING |
MILESTONE_BILLING |
(from payment_profile) |
distributor / NET_45_APPROVED |
NET_45_APPROVED |
(from payment_profile) |
Precedence: The customer's payment_profile field is authoritative. Cross-reference with policies for the terms_code.
Name Discrepancy Warning
Task text may use a different human-readable customer name than the API record's name field. Always use the API record's name as authoritative. For example, a task may say "Health Horizon Aid" for CUST-HHA whose API name is "HealthHands Alliance", or "GreenHarvest Labs" for CUST-GHL whose API name is "Global Health Laboratories". Match by customer_id, not by name string.
7. Quote Types & Scope Rules
Quote Types
quote_revision_with_freight: Revised pricing with transport comparison (tasks 001, 004)
indicative_module_quote: RFQ-based, EXW only, no freight (task 002)
module_quote_with_freight_advisory: Advisory freight only (destination uncertain)
- Product quotes, freight comparisons, etc.
Indicative/NGO Module Quotes (Task 002 Pattern)
- EXW only, freight excluded — policy POL-INDICATIVE-EXW
- Module granularity — quote at module line level only, do NOT split into components (policy POL-MODULE-GRANULARITY)
- RFQ
requested_modules[] drives the line items; each module product_code → product catalog → single price tier
quote_basis: "EXW_ONLY", freight_excluded: true
payment_terms: Look up customer segment → policy
offer_validity_days: 30 (policy POL-QUOTE-VALIDITY)
who_documentation_required: true for IEHK-family products
- Use
article_number from the product record
8. Opportunity Reconciliation (Tasks 003, 005)
Data Assembly
For a given opportunity_id:
- Fetch opportunity → get
stage, won_amount_usd, phases[], customer_id, contact
- Fetch customer → get name, contacts, payment_profile
- Fetch invoices → filter by
opportunity_id, match to phases via phase_id
- Fetch payments → filter by
opportunity_id, match to invoices via invoice_id
- Fetch revenue-journals → filter by
opportunity_id, match to phases via phase_id
- Fetch events → filter by
opportunity_id
- Fetch vouchers → filter by
opportunity_id or event_id
Won Amount vs Phase Total
phase_total = sum(phase.amount_usd for phase in opportunity.phases)
opportunity_matches_phases = (won_amount_usd == phase_total)
Milestone/Invoice State Mapping
Join opportunity phases → invoices → payments → revenue journals:
| Invoice Status |
Payment |
Revenue Journal |
Recognition Status |
paid |
posted with full amount |
exists for phase |
RECOGNIZED |
paid |
posted with full amount |
missing |
MISSING_REVENUE_JOURNAL |
unpaid |
none |
— |
NOT_REQUIRED_UNPAID |
overdue |
none |
— |
NOT_REQUIRED_UNPAID (but drives collection) |
Payment State
PAID — invoice status is paid, payment posted and matches
PARTIAL — payment exists but less than invoice amount
UNPAID — no payment or invoice status is unpaid/overdue
Revenue Recognition Accounting Rule (POL-REVREC)
When a milestone is complete AND paid, a revenue journal must exist:
- Debit:
Deferred Revenue
- Credit:
Implementation Services Revenue
- This moves the amount from the balance-sheet liability to the P&L income line.
Revenue Recognition Summary
COMPLETE_FOR_PAID_MILESTONES — all paid phases have journals
MISSING_FOR_PAID_MILESTONES — at least one paid phase is missing its journal
NOT_REQUIRED — no phases are paid (nothing to recognize)
Outstanding Balance
Sum of invoice.outstanding_amount_usd across all invoices for the opportunity, or use opportunity.outstanding_amount_usd.
Collection Task Logic
| Condition |
Action |
| Unpaid, due date > business date (future) |
MONITOR_UNPAID_NOT_DUE |
| Unpaid, due date ≤ business date (past/overdue) |
SEND_COLLECTION_NOTICE |
| Paid in full |
NO_COLLECTION_ACTION |
Business date defaults to the quote date or 2026-06-01 unless specified.
Accounting Action Logic
| Condition |
Action |
| Paid milestone with missing revenue journal |
RECORD_REVENUE_<MILESTONE> (e.g., RECORD_REVENUE_MS2) |
| All paid milestones have journals |
VERIFY_REVENUE_ONLY |
| No paid milestones |
NO_ACCOUNTING_ACTION |
Journal entry template for RECORD_REVENUE_*:
debit_account: "DEFERRED_REVENUE"
credit_account: "IMPLEMENTATION_SERVICES_REVENUE"
amount: the paid-but-unrecognized milestone amount
owner_queue: "ACCOUNTING"
Event & Voucher Integration
- Match event to opportunity via
opportunity_id
- Match voucher to event via
event_id or voucher_code on event → voucher code
- Voucher
discount_percent is the discount value (e.g., 50 means 50% off)
- Voucher
max_redemptions (called max_uses in some templates)
- Event statuses:
scheduled, confirmed, live, completed, cancelled
- Voucher statuses:
active, draft, expired, disabled
Invite Action Logic
| Event Status |
Voucher Status |
Action |
scheduled or confirmed |
active |
SEND_BRIEFING_INVITE or SEND_EVENT_INVITATION |
| Already in past or completed |
— |
VERIFY_INVITE_SENT |
| No event linked |
— |
NO_INVITE_ACTION |
9. Common Pitfalls & Edge Cases
Distractor Records
- Freight: Records with
FR-DIS-* ID prefix are distractors; they share a quote_id but have wrong dimensions, stale status, or mismatched forwarder/destination. Always cross-check shipment_cbm/shipment_weight_kg against the actual order.
- RFQs: Older RFQs (e.g.,
RFQ-DIS-004 through RFQ-DIS-010) with different quantities, dates, or statuses (superseded, closed_lost, archived) are distractors. Only use the RFQ explicitly named in the task.
- Quotes: Distractor quotes exist for other customers/products/dates. Match by exact
quote_id or rfq_id.
- Component composition distractors: RFQs and quotes may have
component_composition_distractors[] listing sub-components — these are for medical/operations review only. Quote at the module/product level.
Prior Price vs Catalog Tier
Quotes carry prior_unit_price_usd from earlier revisions with lower quantities. Never carry forward the prior price. The catalog tier at the new confirmed_quantity always wins. The source_notes often explicitly states this.
RFQ Quantity Override for Consolidation
When an RFQ's narrative mentions a quantity discrepancy (e.g., "ORS appears twice in the field note as 8 plus 4"), use the consolidated quantity from the requested_modules[] array — that is the authoritative source.
Milestone Completion Dates
For milestones with future completion_date (after the business date), treat as not yet complete. Only completed-and-paid milestones require revenue recognition. Future-phase invoices that are unpaid do not need recognition yet.
Cold-Chain Products
Products with cold_chain_required: true (e.g., LD-REAGENT-44) require:
- Cold-chain-capable freight (freight has
cold_chain_support: true)
- Shorter transit to stay within shelf-life
- Air recommendation is stronger (temperature-controlled lanes)
- Shelf-life check: product shelf-life must exceed transit time significantly
IEHK & WHO Documentation
Products in the emergency_health_kit family require who_documentation_required: true on quotes. This is a standard compliance flag for IEHK-style modules.
Currency
All monetary values are in USD. Always output with 2 decimal places (cents). Use 0.00 for zero amounts, never 0.
Date Format
All dates use ISO 8601 YYYY-MM-DD. This applies to: quote_date, valid_until, due_date, event_date, completion_date, as_of_date.
Quote Validity
Per policy POL-QUOTE-VALIDITY: standard quote pricing is valid for 30 calendar days from quote_date. Freight validity (valid_until) may be shorter.
10. Task-to-API Mapping Summary
| Task Pattern |
Starting Entity |
Navigation Path |
| Quote revision with freight |
quote_id → quote → customer + product + freight |
quote → product (tier by qty), freight (filter by quote_id + status), customer → policy/payment |
| Indicative RFQ quote |
rfq_id → RFQ → customer + requested_modules[] |
RFQ → product per module (single tier), customer → policy, NO freight |
| Opportunity reconciliation |
opportunity_id → opportunity + invoices + payments + revenue + events + vouchers |
opportunity → phases → invoices → payments + revenue-journals; opportunity → events → vouchers |
| Engagement reconciliation |
Same as opportunity + events/vouchers deeply integrated |
Same as above with additional event-action routing logic |
11. Output Structure Rules
- Match the
answer_template.json structure exactly — field names, nesting, and enum values
- Enum values are uppercase with underscores (
PAID, RECOGNIZED, MISSING_REVENUE_JOURNAL)
- Null fields: use
null (JSON null) when not applicable; use "NONE" for enum-style "none" sentinels
- Money: always a number with 2 decimal places, not a string
- Arrays: ordered as specified (freight: AIR → SEA → ROAD; milestones: ascending by phase number or milestone ID)
- Boolean fields:
true/false (JSON literals, not strings)
- Always use the stable IDs from API records (
id, code, phase_id, invoice_id) — don't generate synthetic IDs
12. API Behavior Notes
- The API returns
collection, count, and records[] for list endpoints
- Single-record endpoints return the record object directly (no wrapper)
- Search (
/api/search?q=) normalizes query to lowercase and matches across all collections
- The
generated_at timestamp on /api is fixed per seed — data is static
- All collections are read-only; the API is a staging/seed data source
1---2name: self-attempt-02-433description: Self Attempt 024---5# MedBridge Sales Ops API — Transferable SKILL.md67## 1. Environment & API Entrypoint89- The API base URL is provided by the runner (env, `API_BASE_URL`, `BASE_URL`, `http://34.46.77.124:8002`, etc.). Always use the runner-provided URL.10- All endpoints are read-only `GET`. No authentication required in this staging environment.11- **Root discovery**: `GET /api` returns the collection list and all available endpoints.1213## 2. API Collections & Navigation Pattern1415| Collection | Endpoint | Join Keys |16|---|---|---|17| `customers` | `/api/customers`, `/api/customers/<id>` | `id` |18| `products` | `/api/products`, `/api/products/<code>` | `code` |19| `rfqs` | `/api/rfqs`, `/api/rfqs/<id>` | `id`, `customer_id` |20| `quotes` | `/api/quotes`, `/api/quotes/<id>` | `id`, `customer_id` |21| `freight-quotes` | `/api/freight-quotes`, `/api/freight-quotes/<id>` | `id`, `quote_id` |22| `policies` | `/api/policies` | `id`, `terms_code` |23| `opportunities` | `/api/opportunities`, `/api/opportunities/<id>` | `id`, `customer_id` |24| `invoices` | `/api/invoices` | `id`, `customer_id`, `opportunity_id`, `phase_id` |25| `payments` | `/api/payments` | `id`, `customer_id`, `invoice_id`, `opportunity_id` |26| `revenue-journals` | `/api/revenue-journals` | `id`, `invoice_id`, `opportunity_id`, `phase_id` |27| `events` | `/api/events` | `id`, `customer_id`, `opportunity_id` |28| `vouchers` | `/api/vouchers` | `code`, `customer_id`, `event_id`, `opportunity_id` |29| `search` | `/api/search?q=<text>` | Cross-collection full-text search |3031**Usage habit**: First fetch the collection list, then directly fetch individual records by their known IDs. For multi-entity tasks, use `/api/search?q=<id>` to pull all records linked to a customer, quote, or opportunity in one call.3233## 3. Product Catalog & Price Tier Resolution3435### Tier Selection Rule36Every product has `price_tiers[]` with `min_qty`, `max_qty`, and `unit_price_usd`. To select the correct tier:37381. Find the tier where `confirmed_quantity` falls in `[min_qty, max_qty]`. `max_qty: null` means no upper bound.392. **Catalog tier always overrides any prior unit price.** A quote's `prior_unit_price_usd` is historical context only — never use it for the current quote. The `source_notes` field on quotes often explicitly states this ("catalog tier overrides prior unit price").403. Single-tier products (min_qty=1, max_qty=null) are common for IEHK and cholera modules — no tier logic needed.4142### Fields Retrieved from the Matched Tier43- `unit_price_usd` → unit price for the quote44- `lead_time_days` → lead time in days45- Also required from product root: `shelf_life_months`, `article_number`, `cold_chain_required`4647### Calculation: EXW Total48```49exw_total_usd = confirmed_quantity × unit_price_usd50```51Round to 2 decimal places (cents). All amounts are in USD.5253## 4. Freight Quote Filtering & Validation5455### Primary Filter56Freight records link to quotes via `freight-quotes.quote_id`. **Always filter freight by `quote_id` matching the quote being processed.**5758### Status-Based Filtering59Freight records have a `status` field:60- `active` — valid for use; check `valid_until` against `quote_date`61- `stale` — expired, do not use; its `valid_until` is before `quote_date`62- `mismatch` — wrong shipment size/weight (based on prior quantity); do not use6364**Rule**: Only use freight where `status == "active"` AND `valid_until >= quote_date`. If `valid_until < quote_date`, the freight is stale/invalid regardless of `status` value.6566### Distractor Detection67The API contains distractor freight records (IDs prefixed `FR-DIS-*`) that share a `quote_id` with the target quote but have:68- Wrong `shipment_cbm` or `shipment_weight_kg` (mismatched to actual shipment)69- `status: "stale"` with `valid_until` before the quote date70- Different `forwarder` or `destination` than the real routes71- `route_risk` notes indicating they are benchmarks or archived estimates7273**Pitfall**: Freight for a given `quote_id` returns ALL records — not just the 3 valid ones. You MUST filter by status and valid_until.7475### Validity Status Label76For each freight option, determine:77- `"valid"` — status is `active` AND `valid_until >= quote_date`78- `"stale"` — `valid_until < quote_date` OR status is `stale`79- `source_is_stale: true` when `valid_until < quote_date`8081### Grand Total Calculation82```83grand_total_usd = exw_total_usd + freight_cost_usd84```8586## 5. Route Risk & Mode Recommendation8788### Risk Assessment89- `route_risk: "low"` → `risk_level: "LOW"`, no flag90- `route_risk: "medium"` → `risk_level: "MEDIUM"`, flag based on notes (e.g., `MEDIUM_BORDER_RISK`)91- `route_risk: "high"` → `risk_level: "HIGH"`, flag based on notes (e.g., `HIGH_BORDER_RISK`, `CUSTOMS_RISK`)92- `risk_flag: "NONE"` for low-risk routes9394### Recommended Mode Heuristic951. If any freight is stale/invalid, it disqualifies that mode962. Prefer mode with lowest risk → then fastest transit973. For cold-chain products (`cold_chain_required: true`), air is strongly preferred (temperature control, shorter transit within shelf-life window)984. If road has border risk or is stale, do not recommend road995. Sea is rarely recommended for cold-chain or urgent deliveries due to long transit100101### Freight Reconfirmation102Policy `POL-FREIGHT-RECONFIRM`: All freight rates must be reconfirmed at final order. The `freight_reconfirmation_required` flag should be `true` when any freight has a `valid_until` that could expire before the customer's PO date, or when road risks are flagged. In practice, this is almost always `true` for revision quotes.103104### All-Freight-Valid Check105Check whether ALL freight options are valid on the quote date:106```107all_valid = all(f.valid_until >= quote_date AND f.status == "active" for f in freight_options)108```109110## 6. Customer Record & Payment Terms111112### Customer Lookup113- Start from the quote/RFQ/opportunity's `customer_id`, then fetch `/api/customers/<customer_id>`114- The `segment` field determines policy application: `recurring_ngo`, `new_ngo`, `recurring_commercial`, `government_program`, `implementation_services`, etc.115116### Payment Term Resolution117Match the customer's `payment_profile` and `segment` against policies:118119| Customer Segment/Payment Profile | Payment Terms | Policy |120|---|---|---|121| `new_ngo` / `NEW_CLIENT_REVIEW` | `PREPAY_100` | POL-NEW-CLIENT-PAYMENT |122| `recurring_ngo` / `NET_30_AFTER_PO` | `NET_30_AFTER_PO` | POL-RECURRING-NGO-PAYMENT |123| `recurring_commercial` / `NET_30_AFTER_PO` | `NET_30_AFTER_PO` | (from payment_profile) |124| `government_program` / `NET_30_AFTER_PO` | `NET_30_AFTER_PO` | (from payment_profile) |125| `implementation_services` / `MILESTONE_BILLING` | `MILESTONE_BILLING` | (from payment_profile) |126| `distributor` / `NET_45_APPROVED` | `NET_45_APPROVED` | (from payment_profile) |127128**Precedence**: The customer's `payment_profile` field is authoritative. Cross-reference with policies for the `terms_code`.129130### Name Discrepancy Warning131Task text may use a different human-readable customer name than the API record's `name` field. **Always use the API record's `name` as authoritative.** For example, a task may say "Health Horizon Aid" for `CUST-HHA` whose API name is "HealthHands Alliance", or "GreenHarvest Labs" for `CUST-GHL` whose API name is "Global Health Laboratories". Match by `customer_id`, not by name string.132133## 7. Quote Types & Scope Rules134135### Quote Types136- `quote_revision_with_freight`: Revised pricing with transport comparison (tasks 001, 004)137- `indicative_module_quote`: RFQ-based, EXW only, no freight (task 002)138- `module_quote_with_freight_advisory`: Advisory freight only (destination uncertain)139- Product quotes, freight comparisons, etc.140141### Indicative/NGO Module Quotes (Task 002 Pattern)142- **EXW only, freight excluded** — policy POL-INDICATIVE-EXW143- **Module granularity** — quote at module line level only, do NOT split into components (policy POL-MODULE-GRANULARITY)144- RFQ `requested_modules[]` drives the line items; each module `product_code` → product catalog → single price tier145- `quote_basis: "EXW_ONLY"`, `freight_excluded: true`146- `payment_terms`: Look up customer segment → policy147- `offer_validity_days: 30` (policy POL-QUOTE-VALIDITY)148- `who_documentation_required: true` for IEHK-family products149- Use `article_number` from the product record150151## 8. Opportunity Reconciliation (Tasks 003, 005)152153### Data Assembly154For a given `opportunity_id`:1551561. **Fetch opportunity** → get `stage`, `won_amount_usd`, `phases[]`, `customer_id`, `contact`1572. **Fetch customer** → get name, contacts, payment_profile1583. **Fetch invoices** → filter by `opportunity_id`, match to phases via `phase_id`1594. **Fetch payments** → filter by `opportunity_id`, match to invoices via `invoice_id`1605. **Fetch revenue-journals** → filter by `opportunity_id`, match to phases via `phase_id`1616. **Fetch events** → filter by `opportunity_id`1627. **Fetch vouchers** → filter by `opportunity_id` or `event_id`163164### Won Amount vs Phase Total165```166phase_total = sum(phase.amount_usd for phase in opportunity.phases)167opportunity_matches_phases = (won_amount_usd == phase_total)168```169170### Milestone/Invoice State Mapping171Join opportunity phases → invoices → payments → revenue journals:172173| Invoice Status | Payment | Revenue Journal | Recognition Status |174|---|---|---|---|175| `paid` | posted with full amount | exists for phase | `RECOGNIZED` |176| `paid` | posted with full amount | missing | `MISSING_REVENUE_JOURNAL` |177| `unpaid` | none | — | `NOT_REQUIRED_UNPAID` |178| `overdue` | none | — | `NOT_REQUIRED_UNPAID` (but drives collection) |179180### Payment State181- `PAID` — invoice status is `paid`, payment posted and matches182- `PARTIAL` — payment exists but less than invoice amount183- `UNPAID` — no payment or invoice status is `unpaid`/`overdue`184185### Revenue Recognition Accounting Rule (POL-REVREC)186When a milestone is **complete AND paid**, a revenue journal must exist:187- **Debit**: `Deferred Revenue`188- **Credit**: `Implementation Services Revenue`189- This moves the amount from the balance-sheet liability to the P&L income line.190191### Revenue Recognition Summary192- `COMPLETE_FOR_PAID_MILESTONES` — all paid phases have journals193- `MISSING_FOR_PAID_MILESTONES` — at least one paid phase is missing its journal194- `NOT_REQUIRED` — no phases are paid (nothing to recognize)195196### Outstanding Balance197Sum of `invoice.outstanding_amount_usd` across all invoices for the opportunity, or use `opportunity.outstanding_amount_usd`.198199### Collection Task Logic200| Condition | Action |201|---|---|202| Unpaid, due date > business date (future) | `MONITOR_UNPAID_NOT_DUE` |203| Unpaid, due date ≤ business date (past/overdue) | `SEND_COLLECTION_NOTICE` |204| Paid in full | `NO_COLLECTION_ACTION` |205206Business date defaults to the quote date or `2026-06-01` unless specified.207208### Accounting Action Logic209| Condition | Action |210|---|---|211| Paid milestone with missing revenue journal | `RECORD_REVENUE_<MILESTONE>` (e.g., `RECORD_REVENUE_MS2`) |212| All paid milestones have journals | `VERIFY_REVENUE_ONLY` |213| No paid milestones | `NO_ACCOUNTING_ACTION` |214215Journal entry template for `RECORD_REVENUE_*`:216- `debit_account: "DEFERRED_REVENUE"`217- `credit_account: "IMPLEMENTATION_SERVICES_REVENUE"`218- `amount`: the paid-but-unrecognized milestone amount219- `owner_queue: "ACCOUNTING"`220221### Event & Voucher Integration222- Match event to opportunity via `opportunity_id`223- Match voucher to event via `event_id` or `voucher_code` on event → voucher `code`224- Voucher `discount_percent` is the discount value (e.g., 50 means 50% off)225- Voucher `max_redemptions` (called `max_uses` in some templates)226- Event statuses: `scheduled`, `confirmed`, `live`, `completed`, `cancelled`227- Voucher statuses: `active`, `draft`, `expired`, `disabled`228229### Invite Action Logic230| Event Status | Voucher Status | Action |231|---|---|---|232| `scheduled` or `confirmed` | `active` | `SEND_BRIEFING_INVITE` or `SEND_EVENT_INVITATION` |233| Already in past or completed | — | `VERIFY_INVITE_SENT` |234| No event linked | — | `NO_INVITE_ACTION` |235236## 9. Common Pitfalls & Edge Cases237238### Distractor Records239- **Freight**: Records with `FR-DIS-*` ID prefix are distractors; they share a `quote_id` but have wrong dimensions, stale status, or mismatched forwarder/destination. Always cross-check `shipment_cbm`/`shipment_weight_kg` against the actual order.240- **RFQs**: Older RFQs (e.g., `RFQ-DIS-004` through `RFQ-DIS-010`) with different quantities, dates, or statuses (`superseded`, `closed_lost`, `archived`) are distractors. Only use the RFQ explicitly named in the task.241- **Quotes**: Distractor quotes exist for other customers/products/dates. Match by exact `quote_id` or `rfq_id`.242- **Component composition distractors**: RFQs and quotes may have `component_composition_distractors[]` listing sub-components — these are for medical/operations review only. Quote at the module/product level.243244### Prior Price vs Catalog Tier245Quotes carry `prior_unit_price_usd` from earlier revisions with lower quantities. **Never carry forward the prior price.** The catalog tier at the new `confirmed_quantity` always wins. The `source_notes` often explicitly states this.246247### RFQ Quantity Override for Consolidation248When an RFQ's narrative mentions a quantity discrepancy (e.g., "ORS appears twice in the field note as 8 plus 4"), use the consolidated quantity from the `requested_modules[]` array — that is the authoritative source.249250### Milestone Completion Dates251For milestones with future `completion_date` (after the business date), treat as not yet complete. Only completed-and-paid milestones require revenue recognition. Future-phase invoices that are unpaid do not need recognition yet.252253### Cold-Chain Products254Products with `cold_chain_required: true` (e.g., `LD-REAGENT-44`) require:255- Cold-chain-capable freight (freight has `cold_chain_support: true`)256- Shorter transit to stay within shelf-life257- Air recommendation is stronger (temperature-controlled lanes)258- Shelf-life check: product shelf-life must exceed transit time significantly259260### IEHK & WHO Documentation261Products in the `emergency_health_kit` family require `who_documentation_required: true` on quotes. This is a standard compliance flag for IEHK-style modules.262263### Currency264All monetary values are in USD. Always output with 2 decimal places (cents). Use `0.00` for zero amounts, never `0`.265266### Date Format267All dates use ISO 8601 `YYYY-MM-DD`. This applies to: `quote_date`, `valid_until`, `due_date`, `event_date`, `completion_date`, `as_of_date`.268269### Quote Validity270Per policy POL-QUOTE-VALIDITY: standard quote pricing is valid for 30 calendar days from `quote_date`. Freight validity (`valid_until`) may be shorter.271272## 10. Task-to-API Mapping Summary273274| Task Pattern | Starting Entity | Navigation Path |275|---|---|---|276| Quote revision with freight | `quote_id` → quote → customer + product + freight | quote → product (tier by qty), freight (filter by quote_id + status), customer → policy/payment |277| Indicative RFQ quote | `rfq_id` → RFQ → customer + requested_modules[] | RFQ → product per module (single tier), customer → policy, NO freight |278| Opportunity reconciliation | `opportunity_id` → opportunity + invoices + payments + revenue + events + vouchers | opportunity → phases → invoices → payments + revenue-journals; opportunity → events → vouchers |279| Engagement reconciliation | Same as opportunity + events/vouchers deeply integrated | Same as above with additional event-action routing logic |280281## 11. Output Structure Rules282283- Match the `answer_template.json` structure exactly — field names, nesting, and enum values284- Enum values are uppercase with underscores (`PAID`, `RECOGNIZED`, `MISSING_REVENUE_JOURNAL`)285- Null fields: use `null` (JSON null) when not applicable; use `"NONE"` for enum-style "none" sentinels286- Money: always a number with 2 decimal places, not a string287- Arrays: ordered as specified (freight: AIR → SEA → ROAD; milestones: ascending by phase number or milestone ID)288- Boolean fields: `true`/`false` (JSON literals, not strings)289- Always use the stable IDs from API records (`id`, `code`, `phase_id`, `invoice_id`) — don't generate synthetic IDs290291## 12. API Behavior Notes292293- The API returns `collection`, `count`, and `records[]` for list endpoints294- Single-record endpoints return the record object directly (no wrapper)295- Search (`/api/search?q=`) normalizes query to lowercase and matches across all collections296- The `generated_at` timestamp on `/api` is fixed per seed — data is static297- All collections are read-only; the API is a staging/seed data source