ProcureOps procure-to-pay solver skill
You answer procurement / receiving / accounts-payable questions against the
ProcureOps read-only ERP API and a per-task local payload. Output is always a
single JSON object that must match the task's answer_template.json shape exactly.
0. Golden rules (read first)
- The API is the source of truth. Local payloads (memos, packets,
chargeback registers, export notes) only tell you the business request and
the watch set (which POs/receipts/invoices/suppliers to look at). Whenever a
number disagrees, trust the API record, not the payload. One documented
exception: a local chargeback / credit register is authoritative for
chargeback amounts and approval status because those debits do not live in the
ERP yet (see Task-5 pattern).
- Base URL is supplied by the runner / environment_access.md. The prompt's
http://127.0.0.1:8006 or localhost:8006 is a placeholder — replace it with
the real base URL from environment_access.md (e.g.
<remote-env-url>).
- Match the template byte-for-byte. Same top-level keys, same nested keys,
same enum spellings (copy enum strings verbatim, including UPPER_SNAKE vs
lower_snake). Do not add keys the template does not list. Read whether
task_id is a literal required_value and emit exactly that string.
- Rounding & types. USD amounts → round to 2 decimals (cents). Ratios with a
stated precision (e.g.
precision: 4) → round to that many places. Quantities
are integers. Percentages → the stated decimals (often 1). Booleans are real
JSON booleans, not strings.
- Lists are sets unless a sort rule is given. If the template says
"sorted ascending" / "ordering: X ascending", sort. If it says
"set; evaluator sorts values", order does not matter but de-duplicate. When in
doubt, sort ascending — it never hurts a set.
- Respect the as-of / close / review date. Records dated after the as-of
date are out of scope. The date filter is inclusive of the as-of day
(a record dated exactly on the as-of date is IN scope). This is the single
most common scoping mistake — see §5.
1. API usage
Endpoints (GET only):
GET / (endpoint list), GET /health, GET /manifest (counts + anchor IDs),
GET /<collection> → {"count": N, "results": [...]},
GET /<collection>/<id> → one object (404 if missing).
Collections: programs, suppliers, items, contracts,
purchase_requisitions, purchase_orders, receipts, ap_invoices,
payments, approval_events, budget_snapshots, vendor_risk_events.
Aliases also work: purchase-orders, ap-invoices / /ap/invoices,
/ap/payments, approvals, budgets, vendor-risks, purchase-requests.
Filtering: append any record field as a query param (case-insensitive), e.g.
/receipts?po_id=PO-AX17-4481, /ap_invoices?supplier_id=SUP-LUMA,
/vendor_risk_events?supplier_id=SUP-LUMA, /approval_events?object_id=REQ-AX17-141,
/purchase_orders?contract_id=CR-LMP-228, /purchase_orders?sku=LMP-228.
Date window via start/end on the collection's primary date field
(receipts→receipt_date, ap_invoices→invoice_date, payments→scheduled_date,
purchase_orders→order_date, approval_events→event_date,
budget_snapshots→snapshot_date, vendor_risk_events→event_date,
purchase_requisitions→need_by, contracts→effective_date). end=<as_of> is the
clean way to scope to the as-of date and it is inclusive.
Workflow: pull the named anchors by ID, then fan out by filter (all receipts
for a PO, all invoices for a PO, all risk events for a supplier, all POs for a
contract). Never assume one PO has one receipt or one invoice — multiples are
common and scoping them correctly is where points are won/lost.
2. Record schemas (fields you will use)
- programs: program_id, name, owner, cost_center, budget_cap,
committed_amount, priority, status, region.
- suppliers: supplier_id, name, risk_rating (
low|watch|medium|high),
status (active|quality_hold), payment_terms, region.
- items: sku, description, category, standard_cost, preferred_supplier_id,
uom, active.
- contracts: contract_id, program_id, sku, supplier_id, status
(
active|draft|...), price_type (fixed|...), unit_price, ceiling_amount,
effective_date, expiry_date, buyer.
- purchase_requisitions: requisition_id, program_id, sku, quantity, requester,
need_by, priority, status (
approved|converted|...).
- purchase_orders: po_id, program_id, supplier_id, contract_id (may be null),
requisition_id, status (
open|confirmed|partial_receipt|received|closed|cancelled),
order_date, due_date, ship_to, currency, subtotal, tax, total,
lines:[{line_id, sku, description, quantity, unit_price}].
- receipts: receipt_id, po_id, supplier_id, warehouse_id, receipt_date,
packing_slip, receiver, status (
accepted|accepted_with_note|inspection_hold),
lines:[{po_line_id, sku, quantity_received, quantity_rejected, inspection_status (passed|variance)}].
- ap_invoices: invoice_id, po_id, supplier_id, receipt_id (may be null),
invoice_date, status (
approved|paid|on_hold|pending_receipt|entered),
hold_code (null|NO_RECEIPT|PRICE_VARIANCE|QTY_VARIANCE|SUPPLIER_REVIEW),
currency, subtotal, freight, tax, total,
lines:[{po_line_id, sku, quantity_billed, unit_price}].
- payments: payment_id, invoice_id, supplier_id, amount, scheduled_date,
status (
scheduled|...), currency.
- approval_events: event_id, object_id (e.g. a requisition_id), object_type,
action (
submitted|approved|escalated|returned), actor, event_date, note_code.
- budget_snapshots: snapshot_id, program_id, budget_cap, committed_amount,
pending_invoice_amount, snapshot_date, currency.
- vendor_risk_events: event_id, supplier_id, event_type
(
bank_change|invoice_variance|late_delivery|duplicate_invoice_review|quality_hold),
severity (low|medium|high), status (open|monitoring|closed),
related_object_id, event_date.
3. Core business rules (transferable)
3.1 Three-way match (PO ↔ receipt ↔ invoice)
Match on the same po_line_id/sku. Compute per line:
short_qty_vs_po = ordered_qty - received_qty (PO line qty minus receipt qty).
unreceived_billed_qty = billed_qty - received_qty (positive = invoiced more
than received → the dangerous case).
receipt_completion_ratio = received_qty / ordered_qty.
quantity_variance = billed_qty - received_qty;
quantity_variance_pct = variance / PO_line_quantity * 100 (variance is taken
as a % of the PO quantity, not of billed — read the template wording).
A clean three-way match = invoice approved, no hold_code, billed == received ==
ordered (or billed <= received), and prices consistent.
3.2 Quantity_received: which receipts count
- For an invoice-level reconciliation, the invoice usually carries a
receipt_id. Use that specific receipt's received quantity. If
receipt_id is null → received = 0 and the invoice is effectively NO_RECEIPT.
- For a PO/receiving-batch reconciliation, scope to the named batch/receipt
only (the memo will say "closeout for an already-posted receipt"). Do NOT roll
in later receipts on the same PO that fall after the as-of/review date.
- Always check whether a PO has other receipts: an extra receipt on the same PO
that is out of the date window or tied to a different invoice goes into an
excluded list (e.g.
excluded_same_po_receipt_ids), not the in-scope list.
3.3 Contract price consistency
contract_price_match (or ceiling_ok pricing context) = invoice/PO
unit_price equals the active contract's unit_price for that sku/supplier.
A mismatch → PRICE_MISMATCH / PRICE_VARIANCE. Find the contract via
/contracts?sku=...&supplier_id=...; if none exists the line has no commercial
basis (blocker missing_contract, commercial_basis_id = null).
3.4 Contract ceiling headroom
noncancelled_subtotal = sum of subtotal over all POs on the contract whose
status is not cancelled (exclude cancelled; list them in
excluded_cancelled_po_ids).
headroom_before_change = ceiling_amount - noncancelled_subtotal.
requested_subtotal = requested_quantity * contract_unit_price.
headroom_after_change = headroom_before_change - requested_subtotal.
ceiling_ok = requested_subtotal <= headroom_before_change
(equivalently headroom_after >= 0).
- Contract ceiling exposure is subtotal before tax and freight.
3.5 Program budget headroom
- Use the program's
budget_snapshot (preferred) or the program record:
remaining_budget = budget_cap - committed_amount.
- Budget exposure = line subtotal + estimated tax (freight only if the memo
supplies a freight figure).
requested_tax = requested_subtotal * tax_rate;
requested_total = requested_subtotal + requested_tax.
budget_after_change = remaining_budget - requested_total;
budget_ok = budget_after_change >= 0.
max_quantity_with_current_budget = floor( remaining_budget / (unit_price * (1 + tax_rate)) ) (per-unit cost grossed up for tax; floor it).
budget_headroom_usd for a program summary = budget_cap - committed_amount
(rounded to cents).
3.6 Approval gate
Find the requisition's events via /approval_events?object_id=<requisition_id>.
Take the latest by event_date (tie-break by event_id). approval_ok is true
only if the latest action is in the memo's "good actions" set — typically just
approved. submitted, escalated, returned do not clear the gate.
3.7 Supplier-risk policy
risk_rating of watch/medium/high is context only and does not by
itself block a release — unless the rule says so. A watch rating commonly
raises a supplier_watch blocker code or a SUPPLIER_WATCH_RISK exception
note in nomination/receiving contexts, but it does not force a HOLD on its own.
- Open risk events = events with status
open or monitoring (closed are
out). Filter by /vendor_risk_events?supplier_id=... then keep open/monitoring
as of the as-of date (event_date <= as_of).
- Severe open event = an open/monitoring event with severity
high (treat
critical likewise if present). A severe open event is a real blocker
(open_supplier_risk / hold_for_supplier_risk); supplier_risk_ok = false.
Medium/low open events are usually monitored, not blocking — unless the
template's logic counts any open event.
- Supplier
status = quality_hold (vs active) is itself a hard supplier block.
3.8 AP invoice release vs hold
Default to HOLD / keep_on_hold when ANY of:
- invoice
status is on_hold or pending_receipt, or any non-null hold_code;
hold_code = NO_RECEIPT or the invoice's receipt is missing (received = 0);
- billed_qty > received_qty (
INVOICE_QTY_EXCEEDS_RECEIPT / QTY_VARIANCE);
- price mismatch vs contract (PRICE_VARIANCE);
- a severe open supplier-risk event / supplier on quality_hold.
RELEASE only when the invoice is
approved with no hold_code, the three-way
match is clean (billed == received), and there is no blocking risk. A
scheduled payment in /payments?invoice_id=... (with scheduled_date within the
horizon) is evidence to release and adds SCHEDULED_PAYMENT_FOUND.
3.9 Duplicate-invoice handling
Two non-cancelled invoices on the same PO (especially similar totals, or a
duplicate_invoice_review risk event) signal a possible duplicate. Do not blindly
release both — keep the later/unconfirmed one on hold and flag it. When the task
scope names ONE invoice per PO, the other invoice/receipt on that PO is excluded
context, not in-scope.
3.10 Chargeback / credit netting (AP release with debits)
When a local chargeback register lists debits against an invoice:
approved chargeback → net it now:
net_release_amount = invoice_total - approved_chargeback_amount;
decision = release_net_after_approved_chargeback.
pending_* chargeback (e.g. pending_quality_review) → do NOT net, HOLD the
invoice: decision = hold_pending_quality_chargeback, net_release = 0,
pending_chargeback_amount = the pending debit.
- chargeback amount =
basis_quantity * unit_cost (round to cents).
- An invoice with no receipt on its PO → hold_missing_receipt, net_release = 0,
regardless of any release request note.
- A receipt in
inspection_hold with a pending chargeback → hold for quality.
4. Output conventions
- Build the JSON to mirror the template's nesting and key order exactly; emit only
the keys the template names. If the template is "shape doc" (with
type,
required_keys, allowed_values), produce the concrete object those describe,
not the meta-schema.
- Enums: copy the allowed string verbatim. Reason-code / exception-code lists:
include only codes that actually apply; if none apply and a
NO_EXCEPTION /
none sentinel exists, use it.
- Sort every list the template marks "ascending" / "sorted"; de-dup set lists.
- USD → 2 dp; ratios → stated precision; integers for quantities; real booleans.
- For "queue" outputs (hold queue / release queue), list invoice_ids and sort
ascending; an invoice appears in exactly one queue.
- Evidence/supporting-id lists: include the actual record IDs you read from the
API; keep authoritative vs supporting-only sources separate when the template
asks (API records = authoritative; local notes/aliases = supporting-only).
5. Common misjudgments (learned from train tasks)
- Including out-of-window receipts/invoices. A PO often has a later receipt
(e.g. dated 06-08 when as-of is 06-01) — exclude it. Use
end=<as_of> or
filter date <= as_of yourself. Boundary date == as-of is INCLUDED.
- Rolling all receipts on a PO into one invoice's received qty. Use the
invoice's own
receipt_id; treat the rest as excluded/duplicate context.
- Treating
watch/medium risk as an automatic HOLD. It is context only;
only a severe (high) open event, supplier quality_hold status, or an actual
match/qty/price failure blocks a release.
- Counting cancelled POs in contract usage. Always exclude
cancelled from
noncancelled_subtotal and list them separately.
- Approval false-positives. "submitted"/"escalated"/"returned" are NOT
approvals; only the configured good action (usually
approved) clears the gate,
and you must use the latest event.
- Variance % base. Variance percentage is of the PO quantity, not billed
quantity — read the field description.
- Tax in budget but not in ceiling. Contract ceiling exposure = subtotal
only; program budget exposure = subtotal + tax (+ freight only if memo gives it).
- task_id / required_value. Emit the literal value the template demands
(e.g.
"train_002"), not the API-style id.
- Local payload numbers vs API. If the memo and API disagree on a price/qty,
use the API — except for chargeback debits that live only in the local register.
6. Step-by-step SOP for a new task
- Read
prompt.txt, the local payload(s), and answer_template.json. Note: the
as-of/close/review date, the exact watch-set IDs, the tax rate, any
"good actions" / exclusion rules, and the literal task_id/required values.
- Map the template: list every key, its type, enum domain, rounding, and sort/set
rule. This is your output contract.
- Pull each named anchor by ID from the API. Then fan out by filter to get every
related receipt (per PO), invoice (per PO/supplier/program), payment (per
invoice), approval event (per requisition), risk event (per supplier), and
contract (per sku+supplier or contract_id).
- Apply the as-of date scope: drop records dated after the as-of date; keep the
exact watch-set; separate excluded/duplicate context records.
- Run the relevant rule set (§3): three-way match, price/contract check, ceiling
headroom, budget headroom, approval gate, supplier-risk, AP hold/release,
chargeback netting. Compute every numeric field with explicit rounding.
- Derive decisions/enums from the computed facts; assemble queues and reason/
exception code lists (apply, don't over-add).
- Emit JSON matching the template exactly — right keys, enum spellings, rounding,
sorted/de-duped lists, real booleans. No prose outside the JSON object.
- Self-check: keys present == template keys; every amount 2 dp; every "ascending"
list sorted; every set de-duped; literal task_id correct; decision consistent
with the blocker/reason codes you emitted (e.g. if any blocker, ready_to_release
is false and blocker_count matches the count of required actions).
1---2name: self-attempt-02-273description: SOP for ProcureOps ERP procure-to-pay tasks (sourcing nomination, receiving control, three-way match, AP hold/release, chargeback netting, contract/budget headroom) using the read-only API as source of truth.4---56# ProcureOps procure-to-pay solver skill78You answer procurement / receiving / accounts-payable questions against the9**ProcureOps** read-only ERP API and a per-task local payload. Output is always a10single JSON object that must match the task's `answer_template.json` shape exactly.1112## 0. Golden rules (read first)13141. **The API is the source of truth.** Local payloads (memos, packets,15 chargeback registers, export notes) only tell you the *business request* and16 the *watch set* (which POs/receipts/invoices/suppliers to look at). Whenever a17 number disagrees, trust the API record, not the payload. One documented18 exception: a **local chargeback / credit register** is authoritative for19 chargeback amounts and approval status because those debits do not live in the20 ERP yet (see Task-5 pattern).212. **Base URL is supplied by the runner / environment_access.md.** The prompt's22 `http://127.0.0.1:8006` or `localhost:8006` is a placeholder — replace it with23 the real base URL from `environment_access.md` (e.g.24 `<remote-env-url>`).253. **Match the template byte-for-byte.** Same top-level keys, same nested keys,26 same enum spellings (copy enum strings verbatim, including UPPER_SNAKE vs27 lower_snake). Do not add keys the template does not list. Read whether28 `task_id` is a literal `required_value` and emit exactly that string.294. **Rounding & types.** USD amounts → round to 2 decimals (cents). Ratios with a30 stated precision (e.g. `precision: 4`) → round to that many places. Quantities31 are integers. Percentages → the stated decimals (often 1). Booleans are real32 JSON booleans, not strings.335. **Lists are sets unless a sort rule is given.** If the template says34 "sorted ascending" / "ordering: X ascending", sort. If it says35 "set; evaluator sorts values", order does not matter but de-duplicate. When in36 doubt, sort ascending — it never hurts a set.376. **Respect the as-of / close / review date.** Records dated *after* the as-of38 date are out of scope. The date filter is **inclusive** of the as-of day39 (a record dated exactly on the as-of date is IN scope). This is the single40 most common scoping mistake — see §5.4142## 1. API usage4344Endpoints (GET only):45`GET /` (endpoint list), `GET /health`, `GET /manifest` (counts + anchor IDs),46`GET /<collection>` → `{"count": N, "results": [...]}`,47`GET /<collection>/<id>` → one object (404 if missing).4849Collections: `programs`, `suppliers`, `items`, `contracts`,50`purchase_requisitions`, `purchase_orders`, `receipts`, `ap_invoices`,51`payments`, `approval_events`, `budget_snapshots`, `vendor_risk_events`.52Aliases also work: `purchase-orders`, `ap-invoices` / `/ap/invoices`,53`/ap/payments`, `approvals`, `budgets`, `vendor-risks`, `purchase-requests`.5455Filtering: append any record field as a query param (case-insensitive), e.g.56`/receipts?po_id=PO-AX17-4481`, `/ap_invoices?supplier_id=SUP-LUMA`,57`/vendor_risk_events?supplier_id=SUP-LUMA`, `/approval_events?object_id=REQ-AX17-141`,58`/purchase_orders?contract_id=CR-LMP-228`, `/purchase_orders?sku=LMP-228`.59Date window via `start`/`end` on the collection's primary date field60(receipts→receipt_date, ap_invoices→invoice_date, payments→scheduled_date,61purchase_orders→order_date, approval_events→event_date,62budget_snapshots→snapshot_date, vendor_risk_events→event_date,63purchase_requisitions→need_by, contracts→effective_date). `end=<as_of>` is the64clean way to scope to the as-of date and it is inclusive.6566Workflow: pull the named anchors by ID, then **fan out by filter** (all receipts67for a PO, all invoices for a PO, all risk events for a supplier, all POs for a68contract). Never assume one PO has one receipt or one invoice — multiples are69common and scoping them correctly is where points are won/lost.7071## 2. Record schemas (fields you will use)7273- **programs**: program_id, name, owner, cost_center, budget_cap,74 committed_amount, priority, status, region.75- **suppliers**: supplier_id, name, risk_rating (`low|watch|medium|high`),76 status (`active|quality_hold`), payment_terms, region.77- **items**: sku, description, category, standard_cost, preferred_supplier_id,78 uom, active.79- **contracts**: contract_id, program_id, sku, supplier_id, status80 (`active|draft|...`), price_type (`fixed|...`), unit_price, ceiling_amount,81 effective_date, expiry_date, buyer.82- **purchase_requisitions**: requisition_id, program_id, sku, quantity, requester,83 need_by, priority, status (`approved|converted|...`).84- **purchase_orders**: po_id, program_id, supplier_id, contract_id (may be null),85 requisition_id, status (`open|confirmed|partial_receipt|received|closed|cancelled`),86 order_date, due_date, ship_to, currency, subtotal, tax, total,87 `lines:[{line_id, sku, description, quantity, unit_price}]`.88- **receipts**: receipt_id, po_id, supplier_id, warehouse_id, receipt_date,89 packing_slip, receiver, status (`accepted|accepted_with_note|inspection_hold`),90 `lines:[{po_line_id, sku, quantity_received, quantity_rejected, inspection_status (`passed|variance`)}]`.91- **ap_invoices**: invoice_id, po_id, supplier_id, receipt_id (may be null),92 invoice_date, status (`approved|paid|on_hold|pending_receipt|entered`),93 hold_code (`null|NO_RECEIPT|PRICE_VARIANCE|QTY_VARIANCE|SUPPLIER_REVIEW`),94 currency, subtotal, freight, tax, total,95 `lines:[{po_line_id, sku, quantity_billed, unit_price}]`.96- **payments**: payment_id, invoice_id, supplier_id, amount, scheduled_date,97 status (`scheduled|...`), currency.98- **approval_events**: event_id, object_id (e.g. a requisition_id), object_type,99 action (`submitted|approved|escalated|returned`), actor, event_date, note_code.100- **budget_snapshots**: snapshot_id, program_id, budget_cap, committed_amount,101 pending_invoice_amount, snapshot_date, currency.102- **vendor_risk_events**: event_id, supplier_id, event_type103 (`bank_change|invoice_variance|late_delivery|duplicate_invoice_review|quality_hold`),104 severity (`low|medium|high`), status (`open|monitoring|closed`),105 related_object_id, event_date.106107## 3. Core business rules (transferable)108109### 3.1 Three-way match (PO ↔ receipt ↔ invoice)110Match on the same `po_line_id`/`sku`. Compute per line:111- `short_qty_vs_po = ordered_qty - received_qty` (PO line qty minus receipt qty).112- `unreceived_billed_qty = billed_qty - received_qty` (positive = invoiced more113 than received → the dangerous case).114- `receipt_completion_ratio = received_qty / ordered_qty`.115- `quantity_variance = billed_qty - received_qty`;116 `quantity_variance_pct = variance / PO_line_quantity * 100` (variance is taken117 as a % **of the PO quantity**, not of billed — read the template wording).118A clean three-way match = invoice approved, no hold_code, billed == received ==119ordered (or billed <= received), and prices consistent.120121### 3.2 Quantity_received: which receipts count122- For an **invoice-level** reconciliation, the invoice usually carries a123 `receipt_id`. Use that specific receipt's received quantity. If124 `receipt_id` is null → received = 0 and the invoice is effectively NO_RECEIPT.125- For a **PO/receiving-batch** reconciliation, scope to the named batch/receipt126 only (the memo will say "closeout for an already-posted receipt"). Do NOT roll127 in later receipts on the same PO that fall after the as-of/review date.128- Always check whether a PO has *other* receipts: an extra receipt on the same PO129 that is out of the date window or tied to a different invoice goes into an130 **excluded** list (e.g. `excluded_same_po_receipt_ids`), not the in-scope list.131132### 3.3 Contract price consistency133`contract_price_match` (or `ceiling_ok` pricing context) = invoice/PO134`unit_price` equals the active contract's `unit_price` for that sku/supplier.135A mismatch → `PRICE_MISMATCH` / `PRICE_VARIANCE`. Find the contract via136`/contracts?sku=...&supplier_id=...`; if none exists the line has **no commercial137basis** (blocker `missing_contract`, commercial_basis_id = null).138139### 3.4 Contract ceiling headroom140- `noncancelled_subtotal` = sum of `subtotal` over all POs on the contract whose141 status is **not** `cancelled` (exclude cancelled; list them in142 `excluded_cancelled_po_ids`).143- `headroom_before_change = ceiling_amount - noncancelled_subtotal`.144- `requested_subtotal = requested_quantity * contract_unit_price`.145- `headroom_after_change = headroom_before_change - requested_subtotal`.146- `ceiling_ok = requested_subtotal <= headroom_before_change`147 (equivalently headroom_after >= 0).148- Contract ceiling exposure is **subtotal before tax and freight**.149150### 3.5 Program budget headroom151- Use the program's `budget_snapshot` (preferred) or the program record:152 `remaining_budget = budget_cap - committed_amount`.153- Budget exposure = **line subtotal + estimated tax** (freight only if the memo154 supplies a freight figure). `requested_tax = requested_subtotal * tax_rate`;155 `requested_total = requested_subtotal + requested_tax`.156- `budget_after_change = remaining_budget - requested_total`;157 `budget_ok = budget_after_change >= 0`.158- `max_quantity_with_current_budget = floor( remaining_budget /159 (unit_price * (1 + tax_rate)) )` (per-unit cost grossed up for tax; floor it).160- `budget_headroom_usd` for a program summary = `budget_cap - committed_amount`161 (rounded to cents).162163### 3.6 Approval gate164Find the requisition's events via `/approval_events?object_id=<requisition_id>`.165Take the **latest by event_date** (tie-break by event_id). `approval_ok` is true166only if the latest action is in the memo's "good actions" set — typically just167`approved`. `submitted`, `escalated`, `returned` do **not** clear the gate.168169### 3.7 Supplier-risk policy170- `risk_rating` of `watch`/`medium`/`high` is **context only** and does not by171 itself block a release — unless the rule says so. A `watch` rating commonly172 raises a `supplier_watch` blocker code or a `SUPPLIER_WATCH_RISK` exception173 note in nomination/receiving contexts, but it does not force a HOLD on its own.174- **Open risk events** = events with status `open` or `monitoring` (closed are175 out). Filter by `/vendor_risk_events?supplier_id=...` then keep open/monitoring176 *as of the as-of date* (event_date <= as_of).177- **Severe open event** = an open/monitoring event with severity `high` (treat178 `critical` likewise if present). A severe open event is a real blocker179 (`open_supplier_risk` / hold_for_supplier_risk); `supplier_risk_ok = false`.180 Medium/low open events are usually monitored, not blocking — unless the181 template's logic counts any open event.182- Supplier `status = quality_hold` (vs `active`) is itself a hard supplier block.183184### 3.8 AP invoice release vs hold185Default to **HOLD / keep_on_hold** when ANY of:186- invoice `status` is `on_hold` or `pending_receipt`, or any non-null `hold_code`;187- `hold_code = NO_RECEIPT` or the invoice's receipt is missing (received = 0);188- billed_qty > received_qty (`INVOICE_QTY_EXCEEDS_RECEIPT` / QTY_VARIANCE);189- price mismatch vs contract (PRICE_VARIANCE);190- a severe open supplier-risk event / supplier on quality_hold.191**RELEASE** only when the invoice is `approved` with no hold_code, the three-way192match is clean (billed == received), and there is no blocking risk. A193`scheduled` payment in `/payments?invoice_id=...` (with scheduled_date within the194horizon) is evidence to release and adds `SCHEDULED_PAYMENT_FOUND`.195196### 3.9 Duplicate-invoice handling197Two non-cancelled invoices on the same PO (especially similar totals, or a198`duplicate_invoice_review` risk event) signal a possible duplicate. Do not blindly199release both — keep the later/unconfirmed one on hold and flag it. When the task200scope names ONE invoice per PO, the *other* invoice/receipt on that PO is excluded201context, not in-scope.202203### 3.10 Chargeback / credit netting (AP release with debits)204When a local chargeback register lists debits against an invoice:205- `approved` chargeback → net it now:206 `net_release_amount = invoice_total - approved_chargeback_amount`;207 decision = release_net_after_approved_chargeback.208- `pending_*` chargeback (e.g. pending_quality_review) → do NOT net, HOLD the209 invoice: decision = hold_pending_quality_chargeback, net_release = 0,210 pending_chargeback_amount = the pending debit.211- chargeback amount = `basis_quantity * unit_cost` (round to cents).212- An invoice with no receipt on its PO → hold_missing_receipt, net_release = 0,213 regardless of any release request note.214- A receipt in `inspection_hold` with a pending chargeback → hold for quality.215216## 4. Output conventions217218- Build the JSON to mirror the template's nesting and key order exactly; emit only219 the keys the template names. If the template is "shape doc" (with `type`,220 `required_keys`, `allowed_values`), produce the *concrete* object those describe,221 not the meta-schema.222- Enums: copy the allowed string verbatim. Reason-code / exception-code lists:223 include only codes that actually apply; if none apply and a `NO_EXCEPTION` /224 `none` sentinel exists, use it.225- Sort every list the template marks "ascending" / "sorted"; de-dup set lists.226- USD → 2 dp; ratios → stated precision; integers for quantities; real booleans.227- For "queue" outputs (hold queue / release queue), list invoice_ids and sort228 ascending; an invoice appears in exactly one queue.229- Evidence/supporting-id lists: include the actual record IDs you read from the230 API; keep authoritative vs supporting-only sources separate when the template231 asks (API records = authoritative; local notes/aliases = supporting-only).232233## 5. Common misjudgments (learned from train tasks)2342351. **Including out-of-window receipts/invoices.** A PO often has a later receipt236 (e.g. dated 06-08 when as-of is 06-01) — exclude it. Use `end=<as_of>` or237 filter `date <= as_of` yourself. Boundary date == as-of is INCLUDED.2382. **Rolling all receipts on a PO into one invoice's received qty.** Use the239 invoice's own `receipt_id`; treat the rest as excluded/duplicate context.2403. **Treating `watch`/medium risk as an automatic HOLD.** It is context only;241 only a severe (high) open event, supplier `quality_hold` status, or an actual242 match/qty/price failure blocks a release.2434. **Counting cancelled POs in contract usage.** Always exclude `cancelled` from244 noncancelled_subtotal and list them separately.2455. **Approval false-positives.** "submitted"/"escalated"/"returned" are NOT246 approvals; only the configured good action (usually `approved`) clears the gate,247 and you must use the *latest* event.2486. **Variance % base.** Variance percentage is of the **PO quantity**, not billed249 quantity — read the field description.2507. **Tax in budget but not in ceiling.** Contract ceiling exposure = subtotal251 only; program budget exposure = subtotal + tax (+ freight only if memo gives it).2528. **task_id / required_value.** Emit the literal value the template demands253 (e.g. `"train_002"`), not the API-style id.2549. **Local payload numbers vs API.** If the memo and API disagree on a price/qty,255 use the API — except for chargeback debits that live only in the local register.256257## 6. Step-by-step SOP for a new task2582591. Read `prompt.txt`, the local payload(s), and `answer_template.json`. Note: the260 as-of/close/review date, the exact watch-set IDs, the tax rate, any261 "good actions" / exclusion rules, and the literal `task_id`/required values.2622. Map the template: list every key, its type, enum domain, rounding, and sort/set263 rule. This is your output contract.2643. Pull each named anchor by ID from the API. Then fan out by filter to get every265 related receipt (per PO), invoice (per PO/supplier/program), payment (per266 invoice), approval event (per requisition), risk event (per supplier), and267 contract (per sku+supplier or contract_id).2684. Apply the as-of date scope: drop records dated after the as-of date; keep the269 exact watch-set; separate excluded/duplicate context records.2705. Run the relevant rule set (§3): three-way match, price/contract check, ceiling271 headroom, budget headroom, approval gate, supplier-risk, AP hold/release,272 chargeback netting. Compute every numeric field with explicit rounding.2736. Derive decisions/enums from the computed facts; assemble queues and reason/274 exception code lists (apply, don't over-add).2757. Emit JSON matching the template exactly — right keys, enum spellings, rounding,276 sorted/de-duped lists, real booleans. No prose outside the JSON object.2778. Self-check: keys present == template keys; every amount 2 dp; every "ascending"278 list sorted; every set de-duped; literal task_id correct; decision consistent279 with the blocker/reason codes you emitted (e.g. if any blocker, ready_to_release280 is false and blocker_count matches the count of required actions).