SKILL — ProcureOps Procurement Supplier & Receiving Control (task_group_006)
Transferable, executable experience for solving ProcureOps procurement-control tasks against the
shared ProcureOps API. Covers four task families that appear in this scenario:
- Family A — Sourcing nomination readiness packet (program scope + as-of date, budget/readiness, per-SKU supplier commercial decisions, evidence/blocker sets, committee action).
- Family B — Receiving-control closeout (batch identity, receipt scope, ordered↔received↔billed reconciliation, contract/PO/invoice price match, AP hold/release, financial totals, supplier-risk overlay, chargeback release file).
- Family C — AP close / vendor-balance + hold/release settlement (invoice-level hold/release, qty-variance %, totals & scheduled payments & net balance, vendor balance rows, program close totals, payment hold/release queues).
- Family D — Change-control contract amendment (contract status/price/qty, usage & headroom/ceiling, program-budget incremental exposure, requisition approval state, supplier risk, hold actions, final decision).
This is experience, not a restatement of any answer. Follow the recipes; verify every value against the API.
0. Environment & API reference
Base URL
- Remote (use this):
<remote-env-url>
- Prompts say
http://127.0.0.1:8006 — that is the same service at the remote host. Always use the remote URL.
Collections (GET list + GET by id)
| Collection |
Path |
ID field |
Date field |
| programs |
/programs |
program_id |
— |
| suppliers |
/suppliers |
supplier_id |
— |
| items |
/items |
sku |
— |
| contracts |
/contracts |
contract_id |
effective_date |
| purchase_requisitions |
/purchase_requisitions (aliases /purchase-requests, /purchase-requisitions) |
requisition_id |
need_by |
| purchase_orders |
/purchase_orders (alias /purchase-orders) |
po_id |
order_date |
| receipts |
/receipts |
receipt_id |
receipt_date |
| ap_invoices |
/ap_invoices (also /ap/invoices) |
invoice_id |
invoice_date |
| payments |
/payments (also /ap/payments) |
payment_id |
scheduled_date |
| approval_events |
/approval_events (alias /approvals) |
event_id |
event_date |
| budget_snapshots |
/budget_snapshots (alias /budgets) |
snapshot_id |
snapshot_date |
| vendor_risk_events |
/vendor_risk_events (alias /vendor-risks) |
event_id |
event_date |
Query semantics (verified)
GET /<coll>/<id> → returns the single record object directly.
GET /<coll>?<field>=<value> → returns {"count": N, "results": [...]}.
- Field filters match substring, case-insensitive, including nested list values. Blank values are ignored.
- Date range:
GET /<coll>?start=YYYY-MM-DD&end=YYYY-MM-DD (inclusive) on that collection's date field.
- Useful filters by relationship:
?po_id=, ?supplier_id=, ?invoice_id=, ?program_id=, ?contract_id=, ?sku=, ?object_id= (approvals), ?object_type= (approvals).
GET /manifest → record counts + anchor ids (no task/answer keys). GET /health → ok/seed.
Determining record scope — as-of date filtering is the #1 source of error
Many tasks specify an as_of_date / close_date / review_as_of. Apply it consistently:
- Receipts: include only
receipt_date <= as_of.
- AP invoices: include only
invoice_date <= as_of.
- Budget snapshot: pick the snapshot with
snapshot_date <= as_of (latest ≤ as_of). All snapshots here are dated 2026-06-01.
- Vendor risk events:
event_date <= as_of AND status == "open" for "open risk". (Do not treat monitoring or closed as open.)
- Approval events:
event_date <= as_of; take the latest by date for "latest action".
- Payments (AP close): the close memo defines a payment window (e.g. "scheduled through 2026-06-30"). Include only
status == "scheduled" payments with scheduled_date <= window_end.
Rounding
- USD amounts → round to 2 decimals (cents) in the final answer. Keep full precision in intermediate steps to avoid cent drift.
1. Cross-cutting data model & linkage
program ──┬── budget_snapshot (snapshot_id=BUD-<program_id>, budget_cap, committed_amount, pending_invoice_amount)
├── contracts (program_id, sku, supplier_id, unit_price, ceiling_amount, price_type, status)
├── purchase_requisitions (program_id, sku, need_by, status, priority, requester)
└── purchase_orders (program_id, supplier_id, contract_id, requisition_id, lines[], subtotal/tax/total)
purchase_order ──┬── receipts (po_id, supplier_id, lines[].quantity_received/rejected, status, receipt_date)
└── ap_invoices (po_id, supplier_id, receipt_id [nullable], lines[].quantity_billed, hold_code, status, subtotal/freight/tax/total)
ap_invoice ─── payments (invoice_id, amount, scheduled_date, status)
supplier ─── vendor_risk_events (supplier_id, severity, status, event_type, related_object_id)
purchase_requisition ─── approval_events (object_id=requisition_id, object_type="requisition", action, actor, event_date)
Key field facts
purchase_orders.lines[]: {line_id, sku, description, quantity, unit_price}. quantity = ordered. PO also has subtotal, tax, total ( subtotal+tax; freight lives on the invoice, not the PO in this data).
purchase_orders.status enum: open, confirmed, partial_receipt, received, closed, cancelled. cancelled POs are excluded from contract-usage/nomination scope but still listed as "excluded".
receipts.lines[]: {po_line_id, sku, quantity_received, quantity_rejected, inspection_status}. Receipt-level: status = accepted | accepted_with_note | inspection_hold.
ap_invoices.lines[]: {po_line_id, sku, quantity_billed, unit_price}. Invoice-level: status = on_hold | pending_receipt | approved | paid | entered; hold_code = QTY_VARIANCE | NO_RECEIPT | PRICE_VARIANCE | SUPPLIER_REVIEW | null; money = subtotal + freight + tax = total.
ap_invoices.receipt_id is nullable — a pending_receipt invoice has receipt_id: null. To find the matching receipt, query /receipts?po_id=<po_id>.
contracts.price_type = fixed | indexed | not_to_exceed. unit_price is the contract price; ceiling_amount is the not-to-exceed spend cap.
suppliers.risk_rating = low | medium | watch | high. vendor_risk_events.severity = low | medium | high (treat high—and critical if ever present—as severe). vendor_risk_events.status = open | monitoring | closed.
approval_events.action = submitted | approved | returned | escalated. object_type = requisition, object_id = requisition_id.
payments.status = scheduled | released | blocked.
Authority of sources (for evidence/source-list fields)
- Authoritative: ProcureOps API records (PO, receipt, invoice, payment, contract, supplier, program, budget, approval, vendor-risk) + the task-local chargeback register (chargebacks are NOT in the API — they live in the input payload).
- Supporting-only (narrative, do not net against): release-request notes, "stale alias" notes (e.g. PO-73xx alias notes), dock-handoff memos. Use them for context/follow-up actions only.
2. Reconciliation primitives (used by every family)
Quantity
ordered_qty = purchase_orders.lines[].quantity for the PO line.
received_qty = Σ receipts.lines[].quantity_received over in-scope receipts (as-of filtered) for that PO line.
rejected_qty = Σ receipts.lines[].quantity_rejected.
billed_qty = ap_invoices.lines[].quantity_billed.
short_qty_vs_po = ordered_qty − received_qty (report only if > 0).
unreceived_billed_qty = billed_qty − received_qty (only if > 0; i.e. invoice bills more than received).
receipt_completion_ratio = received_qty / ordered_qty.
quantity_variance = billed_qty − received_qty.
quantity_variance_pct = quantity_variance / billed_qty × 100. Denominator is BILLED, not ordered. (e.g. billed 240, received 216 → 10.0%; billed 75, received 0 → 100.0%.)
Price
po_unit_price = purchase_orders.lines[].unit_price.
contract_unit_price = contracts.unit_price (look up via po.contract_id; if null, there is no contract).
invoice_unit_price = ap_invoices.lines[].unit_price.
contract_price_match:
price_type == "fixed" → contract_unit_price == po_unit_price == invoice_unit_price.
indexed → PO/invoice price should equal the contract unit_price at the indexed point (here they match exactly, e.g. 149.75); treat equality as match.
not_to_exceed → price is within ceiling; match if po_unit_price <= contract.unit_price (or equals). Inspect and apply conservatively.
- If
po.contract_id is null → no contract → contract_price_match is false / not applicable (and surfaces a missing_contract blocker in nomination).
Money
line_subtotal = qty × unit_price.
received_goods_value = received_qty × unit_price (use PO/invoice unit price; they match when price matched).
unreceived_goods_value = short_qty_vs_po × unit_price.
invoice_subtotal = ap_invoices.subtotal; invoice_freight = ap_invoices.freight; invoice_tax = ap_invoices.tax; invoice_total = ap_invoices.total (= subtotal + freight + tax).
chargeback_amount = basis_quantity × unit_cost (basis_quantity and unit_cost come from the task-local chargeback register, keyed by invoice_id/receipt_id).
3. Family A — Sourcing nomination readiness packet
Inputs: a program_id, an as_of_date, and a memo naming the package line anchors (SKU → requisition_id + PO_id per line). API is source of truth.
Step recipe
GET /programs/<program_id> → owner, budget_cap, committed_amount. budget_headroom_usd = budget_cap − committed_amount (equiv. latest budget_snapshot ≤ as_of).
- For each package SKU, take the memo-named PO(s) as
package_po_ids. GET /purchase_orders/<po_id> for each.
selected_supplier_id = po.supplier_id. primary_requisition_id = po.requisition_id. commercial_basis_id = po.contract_id (null if none).
- Receipt evidence:
GET /receipts?po_id=<po_id>, keep receipt_date <= as_of → receipt_evidence_ids (sorted set).
- Invoice exceptions:
GET /ap_invoices?po_id=<po_id>, keep invoice_date <= as_of AND status in {on_hold, pending_receipt} → invoice_exception_ids. (approved/paid/entered are NOT exceptions.)
- Risk events:
GET /vendor_risk_events?supplier_id=<supplier_id>, keep status == "open" and event_date <= as_of → risk_event_ids.
- Blocker codes (derive from the above; emit the set):
pending_receipt — receipt_evidence_ids is empty (no in-scope receipt on the PO).
missing_contract — po.contract_id is null.
late_due_date — po.due_date > requisition.need_by (GET /purchase_requisitions/<po.requisition_id> for need_by). I.e. the PO promised date slips past the requisition need-by date.
open_supplier_risk — risk_event_ids is non-empty (any open VRE).
supplier_watch — suppliers.risk_rating == "watch".
ap_hold — any invoice on the PO has status == "on_hold" (generic for any on_hold invoice, regardless of hold_code).
- Anticipated variants if the template expects finer granularity:
price_variance (invoice hold_code PRICE_VARIANCE), supplier_review (hold_code SUPPLIER_REVIEW). Default to the generic ap_hold unless the template's option set is finer.
- A
high/critical supplier risk_rating may warrant a stronger blocker — inspect the template's option set.
- Readiness → decision mapping:
ready (no blockers; has receipt + contract + no open risk + no on_hold invoice) → nomination_decision = "nominate"; readiness "ready".
at_risk (has receipt + contract but soft blockers: ap_hold, supplier_watch, open medium risk, late_due_date without missing receipt) → nomination_decision = "conditional_nomination"; readiness "at_risk".
not_ready (hard blockers: pending_receipt, missing_contract, or any severe open risk) → nomination_decision = "hold"; readiness "not_ready".
- overall_readiness =
not_ready if any line not_ready; else at_risk if any line at_risk; else ready.
- committee_action:
nominate_now_supplier_ids = suppliers of ready lines.
conditional_supplier_ids = suppliers of at_risk lines.
hold_supplier_ids = suppliers of not_ready lines.
next_owner = "ap_team" if any ap_hold blocker exists anywhere; else "procurement" (sourcing).
send_to_committee = "no" if any line is on hold (overall not_ready); else "yes". (In the observed case, a hold line ⇒ "no" / next_owner ap_team.)
Pitfalls (Family A)
- Forgetting the as-of filter → including a later same-PO receipt (e.g. RCV-00001 dated after as_of) as "evidence". It must be excluded.
invoice_exception_ids must exclude approved/paid invoices on the same PO.
commercial_basis_id is the PO's contract_id, not just any contract for that SKU. null PO.contract_id ⇒ missing_contract even if a contract exists elsewhere.
late_due_date compares PO.due_date to requisition.need_by, not to as_of. A PO due after as_of but before need_by is NOT late.
- Only
status == "open" VREs count for open_supplier_risk (not monitoring, not closed).
4. Family B — Receiving-control closeout
Two variants: (B1) single-batch receiving closeout (batch_id given); (B2) multi-invoice AP release + chargeback file (target invoice/receipt/PO id lists given). Both share receipt-scope and reconciliation logic.
Receipt scope categories (apply in both variants)
- Target receipt: the receipt tied to the invoice under review =
invoice.receipt_id if set; otherwise the receipt(s) found via GET /receipts?po_id=<po_id> (as-of filtered if a review_as_of is given).
- Same-PO-later / other same-PO receipt: another receipt on the same PO that belongs to a different invoice (e.g. a second receipt on the PO tied to a different AP invoice). → goes in
excluded_same_po_receipt_ids; do not net its quantities into this invoice's reconciliation.
- Same-supplier-other-PO: receipts for the same supplier on a different PO. Out of scope entirely.
B1 — Single-batch receiving closeout (batch_id = a receipt_id)
GET /receipts/<batch_id> → po_id, supplier_id, warehouse_id, receipt_date, packing_slip, receiver, status, lines[].
Then GET /purchase_orders/<po_id>, GET /ap_invoices?po_id=<po_id> (pick the invoice whose receipt_id == batch_id, or the relevant invoice), GET /contracts/<po.contract_id> (if any), GET /suppliers/<supplier_id>, GET /vendor_risk_events?supplier_id=<supplier_id>.
inspection_summary
po_id, program_id (from PO), supplier_id, supplier_name, warehouse_id, receipt_date, packing_slip, receiver — all literal from the records.
line_reconciliation (one entry per receipt line)
po_line_id, sku (from receipt line).
ordered_qty = PO line quantity for that po_line_id.
received_qty = receipt line quantity_received.
rejected_qty = receipt line quantity_rejected.
billed_qty = invoice line quantity_billed (the invoice tied to this batch).
short_qty_vs_po, unreceived_billed_qty, receipt_completion_ratio per primitives.
po_unit_price, contract_unit_price, invoice_unit_price, contract_price_match per primitives.
invoice_review
invoice_id, invoice_status (invoice.status), hold_code (invoice.hold_code).
receipt_status = receipt.status.
po_status = po.status.
exception_codes (snake_case vocabulary, emit the set):
INVOICE_QTY_EXCEEDS_RECEIPT — billed_qty > received_qty.
PARTIAL_RECEIPT — po.status == "partial_receipt" (equiv. received < ordered).
SUPPLIER_WATCH_RISK — supplier.risk_rating == "watch".
- Anticipated:
RECEIPT_INSPECTION_HOLD (receipt.status == inspection_hold), PRICE_VARIANCE (invoice hold_code PRICE_VARIANCE), SUPPLIER_REVIEW (hold_code SUPPLIER_REVIEW), NO_RECEIPT (invoice pending_receipt / no receipt).
financials
received_goods_value = received_qty × unit_price.
unreceived_goods_value = short_qty_vs_po × unit_price.
invoice_subtotal, invoice_freight, invoice_tax, invoice_total — literal from the invoice.
decision (controlled enums)
batch_disposition: accept_partial_hold_variance (partial receipt + qty-variance hold). Anticipate: accept_full_match (full receipt, no variance), reject_shortage (severe shortfall), hold_inspection (inspection_hold receipt).
ap_action: keep_invoice_on_hold (when invoice on_hold / pending_receipt). Anticipate release_invoice.
receiving_action: record_shortage_follow_up (shortage vs PO). Anticipate close_receipt (full match), hold_for_inspection (inspection_hold).
supplier_action: request_credit_or_remaining_delivery (shortage). Anticipate none (clean match).
supplier_risk_context
supplier_risk_rating = supplier.risk_rating.
has_open_supplier_risk = any VRE for supplier with status == "open" (as-of).
open_supplier_risk_event_ids = those event ids.
evidence
endpoint_record_ids = sorted set of all API record ids used (invoice, contract, item/sku, PO, receipt, supplier, VRE).
task_payloads_reviewed = list of the input payload file paths you read (e.g. input/payloads/receiving_memo.md).
B2 — AP release + chargeback file (target id lists given)
The target id lists (po_ids, receipt_ids, invoice_ids) come from the input packet (e.g. ap_release_packet.json). The chargeback register is in the task-local payload (NOT in the API) — read chargeback_register_excerpt from the packet. Each chargeback entry: {chargeback_id, invoice_id, po_id, receipt_id, reason_code, basis_quantity, unit_cost, status} where status = approved | pending_quality_review.
For each target invoice — release_decisions[]
GET /ap_invoices/<invoice_id> → po_id, total, receipt_id.
- receipt_ids_in_scope:
- If
invoice.receipt_id is set → [invoice.receipt_id].
- Else
GET /receipts?po_id=<po_id>; if exactly one (or the one matching this invoice's line) → [that receipt_id].
- If no receipts on the PO →
[] (missing receipt).
- excluded_same_po_receipt_ids: all OTHER receipts on the same PO not in scope (e.g. a second receipt belonging to a different invoice).
GET /receipts?po_id=<po_id> minus in-scope.
- Find chargeback entries for this
invoice_id (and/or its receipt_id/po_id) in the local register.
approved_chargeback_amount = Σ basis_quantity × unit_cost over entries with status == "approved".
pending_chargeback_amount = Σ over entries with status == "pending_quality_review".
- decision / primary_reason / net_release_amount:
- No receipt on PO (
receipt_ids_in_scope == []) → decision = "hold_missing_receipt", primary_reason = "no_receipt_on_po", net_release_amount = 0.
- Receipt exists, chargeback
approved (or no pending) → decision = "release_net_after_approved_chargeback", net_release_amount = invoice_total − approved_chargeback_amount.
primary_reason: "approved_qty_chargeback" when register reason_code is "Underage Quantity"; "approved_ap_quantity_variance" when reason_code is "AP Quantity Variance". (Map the register reason_code to the lower-case enum.)
- Receipt exists, chargeback
pending_quality_review (typically with receipt.status == inspection_hold) → decision = "hold_pending_quality_chargeback", primary_reason = "inspection_hold_pending_chargeback", net_release_amount = 0.
invoice_total = ap_invoices.total for that invoice.
Per receipt — receiving_exceptions[] (one row per in-scope receipt, plus a MISSING row for POs with no receipt)
receipt_id, po_id.
exception_codes (Title-Case vocabulary, emit as a set):
- If
receipt.status == "inspection_hold" → "Inspection Hold".
- If
received_qty < ordered_qty (receipt short vs PO) → "Severe Unmatched Quantity".
- Add the chargeback register
reason_code verbatim for this receipt (e.g. "Underage Quantity", "AP Quantity Variance").
accepted / accepted_with_note → no status-derived label.
- MISSING receipt row (
receipt_id = "MISSING:<po_id>") → exception_codes = [].
chargeback_status: "approved" if the register entry for this receipt is approved; "pending_quality_review" if pending; "not_applicable" if no receipt / no chargeback.
resolution_status: "net_release_ready" (chargeback approved) | "hold_for_quality_review" (pending/inspection_hold) | "missing_receipt" (no receipt).
summary
release_invoice_ids = invoices with release_net_after_approved_chargeback.
hold_invoice_ids = invoices with hold_* decision.
approved_chargeback_total = Σ approved_chargeback_amount across all invoices.
pending_chargeback_total = Σ pending_chargeback_amount.
net_release_total = Σ net_release_amount across all invoices.
authoritative_sources = ["local_chargeback_register", "procureops_ap_records", "procureops_po_records", "procureops_receipt_records"] (add others you actually used, e.g. procureops_supplier_records).
supporting_only_sources = the narrative notes from the packet (e.g. ap_release_request_note, stale_po73xx_alias_note).
followup_actions: derive — e.g. ask_receiving_for_vantix_receipt (missing receipt), hold_luma_duplicate_receipt_for_separate_invoice (excluded same-PO receipt belongs to another invoice), post_approved_chargeback_netting, route_po00031_quality_review (pending quality review on a specific PO).
Pitfalls (Family B)
- Do not net a same-PO-later receipt that belongs to a different invoice. It goes in
excluded_same_po_receipt_ids, not into received_qty.
billed_qty for the batch reconciliation is the invoice tied to that batch (match on invoice.receipt_id == batch_id), not any invoice on the PO.
- The two exception vocabularies are different: B1 uses snake_case (
INVOICE_QTY_EXCEEDS_RECEIPT...), B2 uses Title Case (Inspection Hold, Severe Unmatched Quantity...). Do not mix them.
- Chargebacks are not in the API — only in the task-local register. If a register entry is missing for an invoice, approved/pending chargeback = 0.
invoice_total includes freight+tax; chargeback unit_cost is a unit price (multiply by basis_quantity) — don't confuse with invoice total.
- A receipt can be
accepted but still short vs PO (quantity_received < ordered) → still emits Severe Unmatched Quantity / PARTIAL_RECEIPT.
5. Family C — AP close / vendor-balance + hold/release settlement
Inputs: a close-slice memo naming specific invoice_ids (the slice), a close_date, and a rule that opening balance for the slice suppliers is 0, plus a payment-credit window (e.g. "payments scheduled through 2026-06-30 reduce the close balance").
Step recipe
For each target invoice:
GET /ap_invoices/<invoice_id> → status, hold_code, po_id, supplier_id, total, lines[].quantity_billed.
GET /purchase_orders/<po_id> → program_id, lines[].quantity (ordered), supplier_id.
GET /receipts?po_id=<po_id> → quantity_received = Σ quantity_received over receipts (slice scope; apply as-of if review date given).
GET /suppliers/<supplier_id> → supplier_name.
GET /payments?invoice_id=<invoice_id> → find scheduled payments within the close window.
invoice_decisions[]
invoice_id, program_id, po_id, supplier_id, supplier_name, invoice_status.
quantity_billed, quantity_received, quantity_variance = billed − received, quantity_variance_pct = variance / billed × 100.
invoice_total = ap_invoices.total.
hold_decision:
- invoice.status ==
approved AND hold_code == null → "RELEASE", release_to_payment = true.
- invoice.status in {
on_hold, pending_receipt} → "HOLD", release_to_payment = false.
- (Anticipate:
paid/entered → handle per memo; usually not in a close slice.)
hold_code = invoice.hold_code (null if RELEASE).
scheduled_payment_amount = Σ payment.amount for this invoice where status == "scheduled" AND scheduled_date <= <window_end> (window from memo, e.g. 2026-06-30). If none → 0.0.
net_balance_impact = invoice_total − scheduled_payment_amount.
reason_codes (emit set):
- approved + no hold →
APPROVED_THREE_WAY_MATCH.
- on_hold → the hold_code itself (
QTY_VARIANCE / PRICE_VARIANCE / SUPPLIER_REVIEW).
- pending_receipt →
NO_RECEIPT.
- if
scheduled_payment_amount > 0 → also add SCHEDULED_PAYMENT_FOUND.
vendor_balances[] (one row per supplier in the slice)
opening_balance = 0.0 (per memo assumption; do not invent otherwise).
invoice_total = Σ invoice_total over this supplier's slice invoices.
scheduled_payments = Σ scheduled_payment_amount over this supplier's slice invoices.
held_invoice_total = Σ invoice_total over this supplier's slice invoices with hold_decision == HOLD.
releasable_invoice_total = Σ invoice_total over this supplier's slice invoices with hold_decision == RELEASE.
close_balance = invoice_total − scheduled_payments.
balance_status:
FULLY_SCHEDULED — close_balance == 0 and scheduled_payments == invoice_total (released + fully scheduled).
OPEN_HELD — held invoices and close_balance > 0.
- Anticipated:
OPEN_UNHELD — released but not yet scheduled (close_balance > 0, held == 0).
program_summary[] (one row per program in the slice)
program_id, invoice_count, invoice_total = Σ over slice invoices for that program, held_total = Σ invoice_total of HOLD invoices, released_total = Σ invoice_total of RELEASE invoices, net_close_balance = invoice_total − released_total (= held_total, i.e. what remains open). Verify against the totals.
Queues & total
payment_hold_queue = list of invoice_ids with hold_decision == HOLD.
payment_release_queue = list of invoice_ids with hold_decision == RELEASE.
total_close_balance = Σ close_balance across vendors (= Σ held invoice totals when opening is 0).
Pitfalls (Family C)
quantity_variance_pct denominator is billed (e.g. 24/240 = 10.0%, not 24/216).
scheduled_payment_amount must respect the memo's window (date ≤ window_end) and status == "scheduled". released/blocked payments and out-of-window scheduled payments don't count.
- Only the slice invoices count toward vendor/program totals — not all the supplier's invoices. AP-0027 (a scheduled LUMA payment) is irrelevant if AP-LUMA-7714 is the slice invoice.
net_balance_impact for a RELEASE invoice with a scheduled payment = 0 (it's covered); for a HOLD invoice = invoice_total (full exposure remains).
6. Family D — Change-control contract amendment
Inputs: a change memo (change_memo.json) with: change_request_id, program_id, contract_id, supplier_id, sku, variant_code, requested_incremental_quantity, source_requisition_id, and business_controls (tax_rate_percent, currency, budget_exposure rule, contract_ceiling_exposure rule, existing_contract_usage = "exclude cancelled", approval_good_actions = ["approved"], supplier_watch_rating = "context only unless an open severe event is found").
Step recipe
GET /contracts/<contract_id> → status, price_type, unit_price, ceiling_amount.
- Contract usage (existing):
GET /purchase_orders?contract_id=<contract_id>. Per existing_contract_usage, exclude status == "cancelled".
included_po_ids = non-cancelled POs.
excluded_cancelled_po_ids = cancelled POs.
noncancelled_subtotal = Σ po.subtotal over included POs.
GET /budget_snapshots?program_id=<program_id> → pick snapshot (latest snapshot_date ≤ memo_date); snapshot_id, budget_cap, committed_amount. (Program record carries the same budget_cap/committed_amount; either is acceptable but prefer the snapshot for as-of correctness.)
GET /purchase_requisitions/<source_requisition_id> and GET /approval_events?object_id=<source_requisition_id> (object_type=requisition) → pick latest by event_date ≤ memo_date.
GET /suppliers/<supplier_id> and GET /vendor_risk_events?supplier_id=<supplier_id>.
contract_check
contract_status, price_type, unit_price, ceiling_amount — from contract.
headroom_before_change = ceiling_amount − noncancelled_subtotal.
requested_quantity = memo.requested_incremental_quantity.
requested_subtotal = requested_quantity × unit_price (ceiling exposure = subtotal before tax & freight per memo rule).
headroom_after_change = headroom_before_change − requested_subtotal.
ceiling_ok = headroom_after_change >= 0.
program_budget_check
remaining_budget = budget_cap − committed_amount.
requested_tax = requested_subtotal × tax_rate_percent / 100.
requested_total = requested_subtotal + requested_tax (+ freight only if the memo provides a freight figure; normally it does not).
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_percent/100))) when no freight. (If freight is given, subtract it first; solve q×unit_price×(1+tax) + freight ≤ remaining_budget.)
approval_check
source_requisition_id, latest_event_id, latest_action, latest_actor, latest_event_date — from the latest approval_event (≤ memo_date).
approval_ok = latest_action in business_controls.approval_good_actions (i.e. "approved"). submitted/returned/escalated ⇒ not ok.
supplier_risk_check
supplier_status = supplier.status, supplier_risk_rating = supplier.risk_rating.
open_event_ids = VREs with status == "open" (≤ memo_date).
severe_open_event_ids = open VREs with severity in {high, critical}.
supplier_risk_ok = severe_open_event_ids is empty (per memo: watch/medium is context only; a severe open event blocks).
supporting_ids
included_po_ids, excluded_cancelled_po_ids (from step 2), approval_event_ids (approval events for the requisition).
required_actions (derive from failing checks)
approval_ok == false → "obtain_final_requisition_approval".
budget_ok == false → "raise_budget_exception_or_reduce_quantity".
ceiling_ok == false → "reduce_quantity_or_raise_ceiling".
supplier_risk_ok == false → "escalate_supplier_risk".
summary & decision
blocker_count = number of failing checks among {ceiling_ok, budget_ok, approval_ok, supplier_risk_ok}.
currency = memo currency, ready_to_release = (blocker_count == 0).
decision:
- all ok →
"release_as_amendment".
- budget_ok false AND approval_ok false →
"hold_for_budget_and_approval".
- only budget →
"hold_for_budget"; only approval → "hold_for_approval"; ceiling false → "hold_for_ceiling"; severe supplier risk → "hold_for_supplier_risk". Combine multiple failing checks into a compound hold label matching the template's style.
Pitfalls (Family D)
- Exclude cancelled POs from contract usage. Including them inflates
noncancelled_subtotal and breaks headroom.
- Ceiling exposure = subtotal only (no tax, no freight). Budget exposure = subtotal + tax (+ freight only if memo gives it). Don't swap these.
approval_ok uses the latest approval action by date; an earlier "approved" doesn't count if a later "submitted"/"returned" exists.
supplier_risk_ok: watch rating / medium severity is context only (does not block) per memo. Only a severe open event blocks. Do not block on watch.
max_quantity_with_current_budget uses the after-tax unit cost (unit_price × (1+tax)) as the divisor; floor the result.
budget_after_change can be negative — report it as the negative number (don't clamp to 0); budget_ok = (>= 0).
7. Cross-family pitfalls & common misjudgments
- As-of leakage: the single most common error. Re-derive every "current" set (receipts, invoices, risk events, approvals, budget snapshot) with the task's as-of/close/review date. A record dated one day after the cutoff changes the answer.
- Quantity denominators:
quantity_variance_pct divides by billed. receipt_completion_ratio divides received by ordered. Don't interchange.
- PO total vs invoice total: PO
total = subtotal + tax (no freight in this data). Invoice total = subtotal + freight + tax. For invoice-facing money fields use the invoice; for PO-facing use the PO.
invoice.receipt_id nullable: a pending_receipt invoice has receipt_id: null. Find the receipt via ?po_id=. Do not treat null as "missing entirely" if a receipt exists on the PO.
- Duplicate / parallel receipts on one PO: a PO can have multiple receipts, each belonging to a different invoice. Scope to the invoice's own receipt; put the others in
excluded_same_po_receipt_ids and never net them.
- VRE "open" vs "monitoring": only
status == "open" is an open risk. monitoring and closed are not. Severe = severity high/critical AND open.
- Chargebacks are local, not API: read
chargeback_register_excerpt from the input payload. No /chargebacks endpoint exists.
- Contract price_type matters:
fixed/indexed are exact-price matches; not_to_exceed is a ceiling check. Don't assert contract_price_match equality blindly for not_to_exceed.
- Cancelled POs: excluded from contract usage and from nomination scope, but listed explicitly in
excluded_cancelled_po_ids for traceability.
- Source authority: API records + local chargeback register are authoritative. Narrative notes (release requests, alias notes, dock memos) are supporting-only — use for context/follow-ups, never as the basis for a number.
- Option sets are family-specific: B1 exception codes are snake_case (
PARTIAL_RECEIPT); B2 exception codes are Title Case (Severe Unmatched Quantity). AP-close reason codes are upper-snake (APPROVED_THREE_WAY_MATCH). Use the right vocabulary for each template.
- Rounding: compute at full precision, round only the emitted USD values to 2 decimals. Chargeback netting (
invoice_total − approved_chargeback) must be done before rounding each term.
- Verify totals:
total_close_balance = Σ vendor close_balance = Σ held invoice totals (when opening=0). net_release_total = Σ net_release. approved_chargeback_total = Σ approved. Re-sum as a self-check before emitting.
8. Final output discipline
- Return only the JSON object matching
input/payloads/answer_template.json — no prose, no markdown fence (unless the template/runner requires it).
- Read
answer_template.json first: it defines the exact field names, nesting, and which fields are lists/sets vs scalars. Mirror its structure exactly; do not add or omit keys.
- Treat list fields as sets (dedupe; sort for determinism unless the template specifies an order).
- Every record id you reference must exist in the API (or the local chargeback register for chargeback ids). If a target id from the packet isn't in the API, surface it as
MISSING:<id> per the template pattern (e.g. MISSING:PO-AX17-4519).
- Always re-derive numbers from the API; do not trust narrative memos for figures.
1---2name: fewshot-attempt-01-73description: SKILL — ProcureOps Procurement Supplier & Receiving Control (task_group_006)4---5# SKILL — ProcureOps Procurement Supplier & Receiving Control (task_group_006)67Transferable, executable experience for solving ProcureOps procurement-control tasks against the8shared ProcureOps API. Covers four task families that appear in this scenario:910- **Family A — Sourcing nomination readiness packet** (program scope + as-of date, budget/readiness, per-SKU supplier commercial decisions, evidence/blocker sets, committee action).11- **Family B — Receiving-control closeout** (batch identity, receipt scope, ordered↔received↔billed reconciliation, contract/PO/invoice price match, AP hold/release, financial totals, supplier-risk overlay, **chargeback release file**).12- **Family C — AP close / vendor-balance + hold/release settlement** (invoice-level hold/release, qty-variance %, totals & scheduled payments & net balance, vendor balance rows, program close totals, payment hold/release queues).13- **Family D — Change-control contract amendment** (contract status/price/qty, usage & headroom/ceiling, program-budget incremental exposure, requisition approval state, supplier risk, hold actions, final decision).1415This is experience, not a restatement of any answer. Follow the recipes; verify every value against the API.1617---1819## 0. Environment & API reference2021### Base URL22- **Remote (use this):** `<remote-env-url>`23- Prompts say `http://127.0.0.1:8006` — that is the **same service** at the remote host. Always use the remote URL.2425### Collections (GET list + GET by id)26| Collection | Path | ID field | Date field |27| --- | --- | --- | --- |28| programs | `/programs` | program_id | — |29| suppliers | `/suppliers` | supplier_id | — |30| items | `/items` | sku | — |31| contracts | `/contracts` | contract_id | effective_date |32| purchase_requisitions | `/purchase_requisitions` (aliases `/purchase-requests`, `/purchase-requisitions`) | requisition_id | need_by |33| purchase_orders | `/purchase_orders` (alias `/purchase-orders`) | po_id | order_date |34| receipts | `/receipts` | receipt_id | receipt_date |35| ap_invoices | `/ap_invoices` (also `/ap/invoices`) | invoice_id | invoice_date |36| payments | `/payments` (also `/ap/payments`) | payment_id | scheduled_date |37| approval_events | `/approval_events` (alias `/approvals`) | event_id | event_date |38| budget_snapshots | `/budget_snapshots` (alias `/budgets`) | snapshot_id | snapshot_date |39| vendor_risk_events | `/vendor_risk_events` (alias `/vendor-risks`) | event_id | event_date |4041### Query semantics (verified)42- `GET /<coll>/<id>` → returns the single record object directly.43- `GET /<coll>?<field>=<value>` → returns `{"count": N, "results": [...]}`.44- Field filters match **substring, case-insensitive**, including nested list values. Blank values are ignored.45- Date range: `GET /<coll>?start=YYYY-MM-DD&end=YYYY-MM-DD` (inclusive) on that collection's date field.46- Useful filters by relationship: `?po_id=`, `?supplier_id=`, `?invoice_id=`, `?program_id=`, `?contract_id=`, `?sku=`, `?object_id=` (approvals), `?object_type=` (approvals).47- `GET /manifest` → record counts + anchor ids (no task/answer keys). `GET /health` → ok/seed.4849### Determining record scope — **as-of date filtering is the #1 source of error**50Many tasks specify an `as_of_date` / `close_date` / `review_as_of`. Apply it consistently:51- **Receipts:** include only `receipt_date <= as_of`.52- **AP invoices:** include only `invoice_date <= as_of`.53- **Budget snapshot:** pick the snapshot with `snapshot_date <= as_of` (latest ≤ as_of). All snapshots here are dated `2026-06-01`.54- **Vendor risk events:** `event_date <= as_of` **AND** `status == "open"` for "open risk". (Do **not** treat `monitoring` or `closed` as open.)55- **Approval events:** `event_date <= as_of`; take the latest by date for "latest action".56- **Payments (AP close):** the close memo defines a payment window (e.g. "scheduled through 2026-06-30"). Include only `status == "scheduled"` payments with `scheduled_date <= window_end`.5758### Rounding59- USD amounts → round to 2 decimals (cents) in the final answer. Keep full precision in intermediate steps to avoid cent drift.6061---6263## 1. Cross-cutting data model & linkage6465```66program ──┬── budget_snapshot (snapshot_id=BUD-<program_id>, budget_cap, committed_amount, pending_invoice_amount)67 ├── contracts (program_id, sku, supplier_id, unit_price, ceiling_amount, price_type, status)68 ├── purchase_requisitions (program_id, sku, need_by, status, priority, requester)69 └── purchase_orders (program_id, supplier_id, contract_id, requisition_id, lines[], subtotal/tax/total)7071purchase_order ──┬── receipts (po_id, supplier_id, lines[].quantity_received/rejected, status, receipt_date)72 └── ap_invoices (po_id, supplier_id, receipt_id [nullable], lines[].quantity_billed, hold_code, status, subtotal/freight/tax/total)7374ap_invoice ─── payments (invoice_id, amount, scheduled_date, status)7576supplier ─── vendor_risk_events (supplier_id, severity, status, event_type, related_object_id)7778purchase_requisition ─── approval_events (object_id=requisition_id, object_type="requisition", action, actor, event_date)79```8081### Key field facts82- `purchase_orders.lines[]`: `{line_id, sku, description, quantity, unit_price}`. `quantity` = **ordered**. PO also has `subtotal`, `tax`, `total` ( subtotal+tax; **freight lives on the invoice, not the PO** in this data).83- `purchase_orders.status` enum: `open, confirmed, partial_receipt, received, closed, cancelled`. **`cancelled` POs are excluded from contract-usage/nomination scope** but still listed as "excluded".84- `receipts.lines[]`: `{po_line_id, sku, quantity_received, quantity_rejected, inspection_status}`. Receipt-level: `status` = `accepted | accepted_with_note | inspection_hold`.85- `ap_invoices.lines[]`: `{po_line_id, sku, quantity_billed, unit_price}`. Invoice-level: `status` = `on_hold | pending_receipt | approved | paid | entered`; `hold_code` = `QTY_VARIANCE | NO_RECEIPT | PRICE_VARIANCE | SUPPLIER_REVIEW | null`; money = `subtotal` + `freight` + `tax` = `total`.86- `ap_invoices.receipt_id` is **nullable** — a pending_receipt invoice has `receipt_id: null`. To find the matching receipt, query `/receipts?po_id=<po_id>`.87- `contracts.price_type` = `fixed | indexed | not_to_exceed`. `unit_price` is the contract price; `ceiling_amount` is the not-to-exceed spend cap.88- `suppliers.risk_rating` = `low | medium | watch | high`. `vendor_risk_events.severity` = `low | medium | high` (treat `high`—and `critical` if ever present—as **severe**). `vendor_risk_events.status` = `open | monitoring | closed`.89- `approval_events.action` = `submitted | approved | returned | escalated`. `object_type` = `requisition`, `object_id` = requisition_id.90- `payments.status` = `scheduled | released | blocked`.9192### Authority of sources (for evidence/source-list fields)93- **Authoritative:** ProcureOps API records (PO, receipt, invoice, payment, contract, supplier, program, budget, approval, vendor-risk) + the **task-local chargeback register** (chargebacks are NOT in the API — they live in the input payload).94- **Supporting-only (narrative, do not net against):** release-request notes, "stale alias" notes (e.g. PO-73xx alias notes), dock-handoff memos. Use them for context/follow-up actions only.9596---9798## 2. Reconciliation primitives (used by every family)99100### Quantity101- `ordered_qty` = `purchase_orders.lines[].quantity` for the PO line.102- `received_qty` = Σ `receipts.lines[].quantity_received` over **in-scope** receipts (as-of filtered) for that PO line.103- `rejected_qty` = Σ `receipts.lines[].quantity_rejected`.104- `billed_qty` = `ap_invoices.lines[].quantity_billed`.105- `short_qty_vs_po` = `ordered_qty − received_qty` (report only if > 0).106- `unreceived_billed_qty` = `billed_qty − received_qty` (only if > 0; i.e. invoice bills more than received).107- `receipt_completion_ratio` = `received_qty / ordered_qty`.108- `quantity_variance` = `billed_qty − received_qty`.109- `quantity_variance_pct` = `quantity_variance / billed_qty × 100`. **Denominator is BILLED, not ordered.** (e.g. billed 240, received 216 → 10.0%; billed 75, received 0 → 100.0%.)110111### Price112- `po_unit_price` = `purchase_orders.lines[].unit_price`.113- `contract_unit_price` = `contracts.unit_price` (look up via `po.contract_id`; if `null`, there is no contract).114- `invoice_unit_price` = `ap_invoices.lines[].unit_price`.115- `contract_price_match`:116 - `price_type == "fixed"` → `contract_unit_price == po_unit_price == invoice_unit_price`.117 - `indexed` → PO/invoice price should equal the contract `unit_price` at the indexed point (here they match exactly, e.g. 149.75); treat equality as match.118 - `not_to_exceed` → price is within ceiling; match if `po_unit_price <= contract.unit_price` (or equals). Inspect and apply conservatively.119 - If `po.contract_id` is null → no contract → `contract_price_match` is false / not applicable (and surfaces a `missing_contract` blocker in nomination).120121### Money122- `line_subtotal` = `qty × unit_price`.123- `received_goods_value` = `received_qty × unit_price` (use PO/invoice unit price; they match when price matched).124- `unreceived_goods_value` = `short_qty_vs_po × unit_price`.125- `invoice_subtotal` = `ap_invoices.subtotal`; `invoice_freight` = `ap_invoices.freight`; `invoice_tax` = `ap_invoices.tax`; `invoice_total` = `ap_invoices.total` (= subtotal + freight + tax).126- `chargeback_amount` = `basis_quantity × unit_cost` (basis_quantity and unit_cost come from the task-local chargeback register, keyed by invoice_id/receipt_id).127128---129130## 3. Family A — Sourcing nomination readiness packet131132**Inputs:** a program_id, an `as_of_date`, and a memo naming the package line anchors (SKU → requisition_id + PO_id per line). API is source of truth.133134### Step recipe1351. `GET /programs/<program_id>` → owner, budget_cap, committed_amount. `budget_headroom_usd = budget_cap − committed_amount` (equiv. latest budget_snapshot ≤ as_of).1362. For each package SKU, take the memo-named PO(s) as `package_po_ids`. `GET /purchase_orders/<po_id>` for each.1373. `selected_supplier_id` = `po.supplier_id`. `primary_requisition_id` = `po.requisition_id`. `commercial_basis_id` = `po.contract_id` (null if none).1384. **Receipt evidence:** `GET /receipts?po_id=<po_id>`, keep `receipt_date <= as_of` → `receipt_evidence_ids` (sorted set).1395. **Invoice exceptions:** `GET /ap_invoices?po_id=<po_id>`, keep `invoice_date <= as_of` **AND** `status in {on_hold, pending_receipt}` → `invoice_exception_ids`. (approved/paid/entered are NOT exceptions.)1406. **Risk events:** `GET /vendor_risk_events?supplier_id=<supplier_id>`, keep `status == "open"` and `event_date <= as_of` → `risk_event_ids`.1417. **Blocker codes** (derive from the above; emit the set):142 - `pending_receipt` — `receipt_evidence_ids` is empty (no in-scope receipt on the PO).143 - `missing_contract` — `po.contract_id` is null.144 - `late_due_date` — `po.due_date > requisition.need_by` (`GET /purchase_requisitions/<po.requisition_id>` for need_by). I.e. the PO promised date slips past the requisition need-by date.145 - `open_supplier_risk` — `risk_event_ids` is non-empty (any open VRE).146 - `supplier_watch` — `suppliers.risk_rating == "watch"`.147 - `ap_hold` — any invoice on the PO has `status == "on_hold"` (generic for any `on_hold` invoice, regardless of hold_code).148 - Anticipated variants if the template expects finer granularity: `price_variance` (invoice hold_code PRICE_VARIANCE), `supplier_review` (hold_code SUPPLIER_REVIEW). Default to the generic `ap_hold` unless the template's option set is finer.149 - A `high`/critical supplier risk_rating may warrant a stronger blocker — inspect the template's option set.1508. **Readiness → decision mapping:**151 - **`ready`** (no blockers; has receipt + contract + no open risk + no on_hold invoice) → `nomination_decision = "nominate"`; readiness `"ready"`.152 - **`at_risk`** (has receipt + contract but soft blockers: `ap_hold`, `supplier_watch`, open **medium** risk, late_due_date without missing receipt) → `nomination_decision = "conditional_nomination"`; readiness `"at_risk"`.153 - **`not_ready`** (hard blockers: `pending_receipt`, `missing_contract`, or any **severe** open risk) → `nomination_decision = "hold"`; readiness `"not_ready"`.1549. **overall_readiness** = `not_ready` if any line not_ready; else `at_risk` if any line at_risk; else `ready`.15510. **committee_action:**156 - `nominate_now_supplier_ids` = suppliers of `ready` lines.157 - `conditional_supplier_ids` = suppliers of `at_risk` lines.158 - `hold_supplier_ids` = suppliers of `not_ready` lines.159 - `next_owner` = `"ap_team"` if any `ap_hold` blocker exists anywhere; else `"procurement"` (sourcing).160 - `send_to_committee` = `"no"` if any line is on `hold` (overall not_ready); else `"yes"`. (In the observed case, a hold line ⇒ "no" / next_owner ap_team.)161162### Pitfalls (Family A)163- Forgetting the as-of filter → including a later same-PO receipt (e.g. RCV-00001 dated after as_of) as "evidence". It must be excluded.164- `invoice_exception_ids` must exclude `approved`/`paid` invoices on the same PO.165- `commercial_basis_id` is the **PO's** contract_id, not just any contract for that SKU. null PO.contract_id ⇒ `missing_contract` even if a contract exists elsewhere.166- `late_due_date` compares PO.due_date to **requisition.need_by**, not to as_of. A PO due *after as_of but before need_by* is NOT late.167- Only `status == "open"` VREs count for `open_supplier_risk` (not `monitoring`, not `closed`).168169---170171## 4. Family B — Receiving-control closeout172173Two variants: (B1) single-batch receiving closeout (batch_id given); (B2) multi-invoice AP release + chargeback file (target invoice/receipt/PO id lists given). Both share receipt-scope and reconciliation logic.174175### Receipt scope categories (apply in both variants)176- **Target receipt:** the receipt tied to the invoice under review = `invoice.receipt_id` if set; otherwise the receipt(s) found via `GET /receipts?po_id=<po_id>` (as-of filtered if a review_as_of is given).177- **Same-PO-later / other same-PO receipt:** another receipt on the **same PO** that belongs to a **different invoice** (e.g. a second receipt on the PO tied to a different AP invoice). → goes in `excluded_same_po_receipt_ids`; **do not net its quantities** into this invoice's reconciliation.178- **Same-supplier-other-PO:** receipts for the same supplier on a different PO. Out of scope entirely.179180### B1 — Single-batch receiving closeout (batch_id = a receipt_id)181182`GET /receipts/<batch_id>` → po_id, supplier_id, warehouse_id, receipt_date, packing_slip, receiver, status, lines[].183Then `GET /purchase_orders/<po_id>`, `GET /ap_invoices?po_id=<po_id>` (pick the invoice whose `receipt_id == batch_id`, or the relevant invoice), `GET /contracts/<po.contract_id>` (if any), `GET /suppliers/<supplier_id>`, `GET /vendor_risk_events?supplier_id=<supplier_id>`.184185#### `inspection_summary`186po_id, program_id (from PO), supplier_id, supplier_name, warehouse_id, receipt_date, packing_slip, receiver — all literal from the records.187188#### `line_reconciliation` (one entry per receipt line)189- `po_line_id`, `sku` (from receipt line).190- `ordered_qty` = PO line quantity for that po_line_id.191- `received_qty` = receipt line quantity_received.192- `rejected_qty` = receipt line quantity_rejected.193- `billed_qty` = invoice line quantity_billed (the invoice tied to this batch).194- `short_qty_vs_po`, `unreceived_billed_qty`, `receipt_completion_ratio` per primitives.195- `po_unit_price`, `contract_unit_price`, `invoice_unit_price`, `contract_price_match` per primitives.196197#### `invoice_review`198- `invoice_id`, `invoice_status` (invoice.status), `hold_code` (invoice.hold_code).199- `receipt_status` = receipt.status.200- `po_status` = po.status.201- `exception_codes` (snake_case vocabulary, emit the set):202 - `INVOICE_QTY_EXCEEDS_RECEIPT` — `billed_qty > received_qty`.203 - `PARTIAL_RECEIPT` — `po.status == "partial_receipt"` (equiv. received < ordered).204 - `SUPPLIER_WATCH_RISK` — `supplier.risk_rating == "watch"`.205 - Anticipated: `RECEIPT_INSPECTION_HOLD` (receipt.status == inspection_hold), `PRICE_VARIANCE` (invoice hold_code PRICE_VARIANCE), `SUPPLIER_REVIEW` (hold_code SUPPLIER_REVIEW), `NO_RECEIPT` (invoice pending_receipt / no receipt).206207#### `financials`208- `received_goods_value` = received_qty × unit_price.209- `unreceived_goods_value` = short_qty_vs_po × unit_price.210- `invoice_subtotal`, `invoice_freight`, `invoice_tax`, `invoice_total` — literal from the invoice.211212#### `decision` (controlled enums)213- `batch_disposition`: `accept_partial_hold_variance` (partial receipt + qty-variance hold). Anticipate: `accept_full_match` (full receipt, no variance), `reject_shortage` (severe shortfall), `hold_inspection` (inspection_hold receipt).214- `ap_action`: `keep_invoice_on_hold` (when invoice on_hold / pending_receipt). Anticipate `release_invoice`.215- `receiving_action`: `record_shortage_follow_up` (shortage vs PO). Anticipate `close_receipt` (full match), `hold_for_inspection` (inspection_hold).216- `supplier_action`: `request_credit_or_remaining_delivery` (shortage). Anticipate `none` (clean match).217218#### `supplier_risk_context`219- `supplier_risk_rating` = supplier.risk_rating.220- `has_open_supplier_risk` = any VRE for supplier with `status == "open"` (as-of).221- `open_supplier_risk_event_ids` = those event ids.222223#### `evidence`224- `endpoint_record_ids` = sorted set of all API record ids used (invoice, contract, item/sku, PO, receipt, supplier, VRE).225- `task_payloads_reviewed` = list of the input payload file paths you read (e.g. `input/payloads/receiving_memo.md`).226227### B2 — AP release + chargeback file (target id lists given)228229The target id lists (`po_ids`, `receipt_ids`, `invoice_ids`) come from the input packet (e.g. `ap_release_packet.json`). The **chargeback register** is in the **task-local payload** (NOT in the API) — read `chargeback_register_excerpt` from the packet. Each chargeback entry: `{chargeback_id, invoice_id, po_id, receipt_id, reason_code, basis_quantity, unit_cost, status}` where `status` = `approved | pending_quality_review`.230231#### For each target invoice — `release_decisions[]`2321. `GET /ap_invoices/<invoice_id>` → po_id, total, receipt_id.2332. **receipt_ids_in_scope:**234 - If `invoice.receipt_id` is set → `[invoice.receipt_id]`.235 - Else `GET /receipts?po_id=<po_id>`; if exactly one (or the one matching this invoice's line) → `[that receipt_id]`.236 - If no receipts on the PO → `[]` (missing receipt).2373. **excluded_same_po_receipt_ids:** all OTHER receipts on the same PO not in scope (e.g. a second receipt belonging to a different invoice). `GET /receipts?po_id=<po_id>` minus in-scope.2384. Find chargeback entries for this `invoice_id` (and/or its `receipt_id`/`po_id`) in the local register.239 - `approved_chargeback_amount` = Σ `basis_quantity × unit_cost` over entries with `status == "approved"`.240 - `pending_chargeback_amount` = Σ over entries with `status == "pending_quality_review"`.2415. **decision / primary_reason / net_release_amount:**242 - No receipt on PO (`receipt_ids_in_scope == []`) → `decision = "hold_missing_receipt"`, `primary_reason = "no_receipt_on_po"`, `net_release_amount = 0`.243 - Receipt exists, chargeback `approved` (or no pending) → `decision = "release_net_after_approved_chargeback"`, `net_release_amount = invoice_total − approved_chargeback_amount`.244 - `primary_reason`: `"approved_qty_chargeback"` when register reason_code is "Underage Quantity"; `"approved_ap_quantity_variance"` when reason_code is "AP Quantity Variance". (Map the register reason_code to the lower-case enum.)245 - Receipt exists, chargeback `pending_quality_review` (typically with receipt.status == `inspection_hold`) → `decision = "hold_pending_quality_chargeback"`, `primary_reason = "inspection_hold_pending_chargeback"`, `net_release_amount = 0`.246247`invoice_total` = ap_invoices.total for that invoice.248249#### Per receipt — `receiving_exceptions[]` (one row per in-scope receipt, plus a MISSING row for POs with no receipt)250- `receipt_id`, `po_id`.251- `exception_codes` (Title-Case vocabulary, emit as a set):252 - If `receipt.status == "inspection_hold"` → `"Inspection Hold"`.253 - If `received_qty < ordered_qty` (receipt short vs PO) → `"Severe Unmatched Quantity"`.254 - Add the chargeback register `reason_code` verbatim for this receipt (e.g. `"Underage Quantity"`, `"AP Quantity Variance"`).255 - `accepted` / `accepted_with_note` → no status-derived label.256 - MISSING receipt row (`receipt_id = "MISSING:<po_id>"`) → `exception_codes = []`.257- `chargeback_status`: `"approved"` if the register entry for this receipt is approved; `"pending_quality_review"` if pending; `"not_applicable"` if no receipt / no chargeback.258- `resolution_status`: `"net_release_ready"` (chargeback approved) | `"hold_for_quality_review"` (pending/inspection_hold) | `"missing_receipt"` (no receipt).259260#### `summary`261- `release_invoice_ids` = invoices with `release_net_after_approved_chargeback`.262- `hold_invoice_ids` = invoices with `hold_*` decision.263- `approved_chargeback_total` = Σ approved_chargeback_amount across all invoices.264- `pending_chargeback_total` = Σ pending_chargeback_amount.265- `net_release_total` = Σ net_release_amount across all invoices.266- `authoritative_sources` = `["local_chargeback_register", "procureops_ap_records", "procureops_po_records", "procureops_receipt_records"]` (add others you actually used, e.g. procureops_supplier_records).267- `supporting_only_sources` = the narrative notes from the packet (e.g. `ap_release_request_note`, `stale_po73xx_alias_note`).268- `followup_actions`: derive — e.g. `ask_receiving_for_vantix_receipt` (missing receipt), `hold_luma_duplicate_receipt_for_separate_invoice` (excluded same-PO receipt belongs to another invoice), `post_approved_chargeback_netting`, `route_po00031_quality_review` (pending quality review on a specific PO).269270### Pitfalls (Family B)271- **Do not net a same-PO-later receipt** that belongs to a different invoice. It goes in `excluded_same_po_receipt_ids`, not into received_qty.272- `billed_qty` for the batch reconciliation is the **invoice tied to that batch** (match on `invoice.receipt_id == batch_id`), not any invoice on the PO.273- The two exception vocabularies are different: B1 uses snake_case (`INVOICE_QTY_EXCEEDS_RECEIPT`...), B2 uses Title Case (`Inspection Hold`, `Severe Unmatched Quantity`...). Do not mix them.274- Chargebacks are **not** in the API — only in the task-local register. If a register entry is missing for an invoice, approved/pending chargeback = 0.275- `invoice_total` includes freight+tax; chargeback `unit_cost` is a unit price (multiply by basis_quantity) — don't confuse with invoice total.276- A receipt can be `accepted` but still short vs PO (`quantity_received < ordered`) → still emits `Severe Unmatched Quantity` / `PARTIAL_RECEIPT`.277278---279280## 5. Family C — AP close / vendor-balance + hold/release settlement281282**Inputs:** a close-slice memo naming **specific invoice_ids** (the slice), a close_date, and a rule that opening balance for the slice suppliers is 0, plus a payment-credit window (e.g. "payments scheduled through 2026-06-30 reduce the close balance").283284### Step recipe285For each target invoice:2861. `GET /ap_invoices/<invoice_id>` → status, hold_code, po_id, supplier_id, total, lines[].quantity_billed.2872. `GET /purchase_orders/<po_id>` → program_id, lines[].quantity (ordered), supplier_id.2883. `GET /receipts?po_id=<po_id>` → `quantity_received` = Σ quantity_received over receipts (slice scope; apply as-of if review date given).2894. `GET /suppliers/<supplier_id>` → supplier_name.2905. `GET /payments?invoice_id=<invoice_id>` → find scheduled payments within the close window.291292#### `invoice_decisions[]`293- `invoice_id`, `program_id`, `po_id`, `supplier_id`, `supplier_name`, `invoice_status`.294- `quantity_billed`, `quantity_received`, `quantity_variance = billed − received`, `quantity_variance_pct = variance / billed × 100`.295- `invoice_total` = ap_invoices.total.296- `hold_decision`:297 - invoice.status == `approved` AND hold_code == null → `"RELEASE"`, `release_to_payment = true`.298 - invoice.status in {`on_hold`, `pending_receipt`} → `"HOLD"`, `release_to_payment = false`.299 - (Anticipate: `paid`/`entered` → handle per memo; usually not in a close slice.)300- `hold_code` = invoice.hold_code (null if RELEASE).301- `scheduled_payment_amount` = Σ `payment.amount` for this invoice where `status == "scheduled"` AND `scheduled_date <= <window_end>` (window from memo, e.g. 2026-06-30). If none → 0.0.302- `net_balance_impact` = `invoice_total − scheduled_payment_amount`.303- `reason_codes` (emit set):304 - approved + no hold → `APPROVED_THREE_WAY_MATCH`.305 - on_hold → the hold_code itself (`QTY_VARIANCE` / `PRICE_VARIANCE` / `SUPPLIER_REVIEW`).306 - pending_receipt → `NO_RECEIPT`.307 - if `scheduled_payment_amount > 0` → also add `SCHEDULED_PAYMENT_FOUND`.308309#### `vendor_balances[]` (one row per supplier in the slice)310- `opening_balance` = 0.0 (per memo assumption; do not invent otherwise).311- `invoice_total` = Σ invoice_total over this supplier's slice invoices.312- `scheduled_payments` = Σ scheduled_payment_amount over this supplier's slice invoices.313- `held_invoice_total` = Σ invoice_total over this supplier's slice invoices with `hold_decision == HOLD`.314- `releasable_invoice_total` = Σ invoice_total over this supplier's slice invoices with `hold_decision == RELEASE`.315- `close_balance` = `invoice_total − scheduled_payments`.316- `balance_status`:317 - `FULLY_SCHEDULED` — close_balance == 0 and scheduled_payments == invoice_total (released + fully scheduled).318 - `OPEN_HELD` — held invoices and close_balance > 0.319 - Anticipated: `OPEN_UNHELD` — released but not yet scheduled (close_balance > 0, held == 0).320321#### `program_summary[]` (one row per program in the slice)322- `program_id`, `invoice_count`, `invoice_total` = Σ over slice invoices for that program, `held_total` = Σ invoice_total of HOLD invoices, `released_total` = Σ invoice_total of RELEASE invoices, `net_close_balance` = `invoice_total − released_total` (= held_total, i.e. what remains open). Verify against the totals.323324#### Queues & total325- `payment_hold_queue` = list of invoice_ids with `hold_decision == HOLD`.326- `payment_release_queue` = list of invoice_ids with `hold_decision == RELEASE`.327- `total_close_balance` = Σ `close_balance` across vendors (= Σ held invoice totals when opening is 0).328329### Pitfalls (Family C)330- `quantity_variance_pct` denominator is **billed** (e.g. 24/240 = 10.0%, not 24/216).331- `scheduled_payment_amount` must respect the memo's window (date ≤ window_end) and `status == "scheduled"`. `released`/`blocked` payments and out-of-window scheduled payments don't count.332- Only the **slice invoices** count toward vendor/program totals — not all the supplier's invoices. AP-0027 (a scheduled LUMA payment) is irrelevant if AP-LUMA-7714 is the slice invoice.333- `net_balance_impact` for a RELEASE invoice with a scheduled payment = 0 (it's covered); for a HOLD invoice = invoice_total (full exposure remains).334335---336337## 6. Family D — Change-control contract amendment338339**Inputs:** a change memo (`change_memo.json`) with: `change_request_id`, `program_id`, `contract_id`, `supplier_id`, `sku`, `variant_code`, `requested_incremental_quantity`, `source_requisition_id`, and `business_controls` (tax_rate_percent, currency, budget_exposure rule, contract_ceiling_exposure rule, `existing_contract_usage = "exclude cancelled"`, `approval_good_actions = ["approved"]`, `supplier_watch_rating = "context only unless an open severe event is found"`).340341### Step recipe3421. `GET /contracts/<contract_id>` → status, price_type, unit_price, ceiling_amount.3432. **Contract usage (existing):** `GET /purchase_orders?contract_id=<contract_id>`. Per `existing_contract_usage`, **exclude `status == "cancelled"`**.344 - `included_po_ids` = non-cancelled POs.345 - `excluded_cancelled_po_ids` = cancelled POs.346 - `noncancelled_subtotal` = Σ `po.subtotal` over included POs.3473. `GET /budget_snapshots?program_id=<program_id>` → pick snapshot (latest `snapshot_date ≤ memo_date`); `snapshot_id`, `budget_cap`, `committed_amount`. (Program record carries the same budget_cap/committed_amount; either is acceptable but prefer the snapshot for as-of correctness.)3484. `GET /purchase_requisitions/<source_requisition_id>` and `GET /approval_events?object_id=<source_requisition_id>` (object_type=requisition) → pick latest by event_date ≤ memo_date.3495. `GET /suppliers/<supplier_id>` and `GET /vendor_risk_events?supplier_id=<supplier_id>`.350351#### `contract_check`352- `contract_status`, `price_type`, `unit_price`, `ceiling_amount` — from contract.353- `headroom_before_change` = `ceiling_amount − noncancelled_subtotal`.354- `requested_quantity` = memo.requested_incremental_quantity.355- `requested_subtotal` = `requested_quantity × unit_price` (ceiling exposure = **subtotal before tax & freight** per memo rule).356- `headroom_after_change` = `headroom_before_change − requested_subtotal`.357- `ceiling_ok` = `headroom_after_change >= 0`.358359#### `program_budget_check`360- `remaining_budget` = `budget_cap − committed_amount`.361- `requested_tax` = `requested_subtotal × tax_rate_percent / 100`.362- `requested_total` = `requested_subtotal + requested_tax` (+ freight **only if** the memo provides a freight figure; normally it does not).363- `budget_after_change` = `remaining_budget − requested_total`.364- `budget_ok` = `budget_after_change >= 0`.365- `max_quantity_with_current_budget` = `floor(remaining_budget / (unit_price × (1 + tax_rate_percent/100)))` when no freight. (If freight is given, subtract it first; solve `q×unit_price×(1+tax) + freight ≤ remaining_budget`.)366367#### `approval_check`368- `source_requisition_id`, `latest_event_id`, `latest_action`, `latest_actor`, `latest_event_date` — from the latest approval_event (≤ memo_date).369- `approval_ok` = `latest_action in business_controls.approval_good_actions` (i.e. `"approved"`). `submitted/returned/escalated` ⇒ not ok.370371#### `supplier_risk_check`372- `supplier_status` = supplier.status, `supplier_risk_rating` = supplier.risk_rating.373- `open_event_ids` = VREs with `status == "open"` (≤ memo_date).374- `severe_open_event_ids` = open VREs with `severity in {high, critical}`.375- `supplier_risk_ok` = `severe_open_event_ids` is empty (per memo: watch/medium is context only; a severe open event blocks).376377#### `supporting_ids`378- `included_po_ids`, `excluded_cancelled_po_ids` (from step 2), `approval_event_ids` (approval events for the requisition).379380#### `required_actions` (derive from failing checks)381- `approval_ok == false` → `"obtain_final_requisition_approval"`.382- `budget_ok == false` → `"raise_budget_exception_or_reduce_quantity"`.383- `ceiling_ok == false` → `"reduce_quantity_or_raise_ceiling"`.384- `supplier_risk_ok == false` → `"escalate_supplier_risk"`.385386#### `summary` & `decision`387- `blocker_count` = number of failing checks among {ceiling_ok, budget_ok, approval_ok, supplier_risk_ok}.388- `currency` = memo currency, `ready_to_release` = (blocker_count == 0).389- `decision`:390 - all ok → `"release_as_amendment"`.391 - budget_ok false AND approval_ok false → `"hold_for_budget_and_approval"`.392 - only budget → `"hold_for_budget"`; only approval → `"hold_for_approval"`; ceiling false → `"hold_for_ceiling"`; severe supplier risk → `"hold_for_supplier_risk"`. Combine multiple failing checks into a compound hold label matching the template's style.393394### Pitfalls (Family D)395- **Exclude cancelled POs** from contract usage. Including them inflates `noncancelled_subtotal` and breaks headroom.396- Ceiling exposure = **subtotal only** (no tax, no freight). Budget exposure = **subtotal + tax** (+ freight only if memo gives it). Don't swap these.397- `approval_ok` uses the **latest** approval action by date; an earlier "approved" doesn't count if a later "submitted"/"returned" exists.398- `supplier_risk_ok`: watch rating / medium severity is **context only** (does not block) per memo. Only a **severe open** event blocks. Do not block on `watch`.399- `max_quantity_with_current_budget` uses the **after-tax unit cost** (`unit_price × (1+tax)`) as the divisor; floor the result.400- `budget_after_change` can be negative — report it as the negative number (don't clamp to 0); `budget_ok = (>= 0)`.401402---403404## 7. Cross-family pitfalls & common misjudgments405406- **As-of leakage:** the single most common error. Re-derive every "current" set (receipts, invoices, risk events, approvals, budget snapshot) with the task's as-of/close/review date. A record dated one day after the cutoff changes the answer.407- **Quantity denominators:** `quantity_variance_pct` divides by **billed**. `receipt_completion_ratio` divides received by **ordered**. Don't interchange.408- **PO total vs invoice total:** PO `total` = subtotal + tax (no freight in this data). Invoice `total` = subtotal + freight + tax. For invoice-facing money fields use the invoice; for PO-facing use the PO.409- **`invoice.receipt_id` nullable:** a `pending_receipt` invoice has `receipt_id: null`. Find the receipt via `?po_id=`. Do not treat null as "missing entirely" if a receipt exists on the PO.410- **Duplicate / parallel receipts on one PO:** a PO can have multiple receipts, each belonging to a different invoice. Scope to the invoice's own receipt; put the others in `excluded_same_po_receipt_ids` and never net them.411- **VRE "open" vs "monitoring":** only `status == "open"` is an open risk. `monitoring` and `closed` are not. Severe = severity `high`/`critical` AND open.412- **Chargebacks are local, not API:** read `chargeback_register_excerpt` from the input payload. No `/chargebacks` endpoint exists.413- **Contract price_type matters:** `fixed`/`indexed` are exact-price matches; `not_to_exceed` is a ceiling check. Don't assert `contract_price_match` equality blindly for not_to_exceed.414- **Cancelled POs:** excluded from contract usage and from nomination scope, but listed explicitly in `excluded_cancelled_po_ids` for traceability.415- **Source authority:** API records + local chargeback register are authoritative. Narrative notes (release requests, alias notes, dock memos) are supporting-only — use for context/follow-ups, never as the basis for a number.416- **Option sets are family-specific:** B1 exception codes are snake_case (`PARTIAL_RECEIPT`); B2 exception codes are Title Case (`Severe Unmatched Quantity`). AP-close reason codes are upper-snake (`APPROVED_THREE_WAY_MATCH`). Use the right vocabulary for each template.417- **Rounding:** compute at full precision, round only the emitted USD values to 2 decimals. Chargeback netting (`invoice_total − approved_chargeback`) must be done before rounding each term.418- **Verify totals:** `total_close_balance` = Σ vendor `close_balance` = Σ held invoice totals (when opening=0). `net_release_total` = Σ net_release. `approved_chargeback_total` = Σ approved. Re-sum as a self-check before emitting.419420---421422## 8. Final output discipline423424- Return **only** the JSON object matching `input/payloads/answer_template.json` — no prose, no markdown fence (unless the template/runner requires it).425- Read `answer_template.json` first: it defines the exact field names, nesting, and which fields are lists/sets vs scalars. Mirror its structure exactly; do not add or omit keys.426- Treat list fields as **sets** (dedupe; sort for determinism unless the template specifies an order).427- Every record id you reference must exist in the API (or the local chargeback register for chargeback ids). If a target id from the packet isn't in the API, surface it as `MISSING:<id>` per the template pattern (e.g. `MISSING:PO-AX17-4519`).428- Always re-derive numbers from the API; do not trust narrative memos for figures.