ERP Finance Expense-Control Skill (task_group_005)
A self-contained playbook for solving ERP finance expense-control tasks against the shared
remote API. The solver only needs the remote API base URL (provided per task in
environment_access.md or the prompt). Discover everything else from the API itself.
NEVER call any /api/judge endpoint. NEVER read local env/ sources, evaluator files, or
test/train answer keys. The remote API is the only system of record. Staged local payloads
(answer templates, batch/scope files, stale CSVs) are inputs only.
1. API access & discovery contract
- Base URL: the runner-provided remote URL (e.g.
<remote-env-url>). Prompts may
also mention http://127.0.0.1:8005; always use the remote URL from environment_access.md.
- Discover endpoints:
GET {base}/endpoints returns the list of paths and the filtering
contract. Health: GET /health, GET /api/health.
- Filtering: list endpoints accept exact-match query parameters by field name, plus
limit and offset for pagination. Default limit is 100. There is no fuzzy/partial matching.
- Namespaced vs bare paths: most resources exist under BOTH a namespaced
/api/... path and
a bare root path (aliases). Prefer the /api/... form. Known pairs:
/api/claims == /claims
/api/ap/bills == /bills
/api/ap/payments == /payments
/api/ap/aging (aging view, no bare alias documented)
/api/vendors == /vendors
/api/compliance/objects == /compliance/objects
/api/prepaids/invoices == /prepaids/invoices
/api/prepaids/gl-balances == /gl/balances
/api/close/logs == /close/logs
- Response shape (every list endpoint):
{ "count", "data": [...], "endpoint", "limit", "offset", "total" }. Read data; use total/offset to paginate when a set may exceed 100.
Endpoint catalog (key fields)
| Resource |
Filterable keys (used) |
Key fields returned |
/api/claims |
claim_id |
claim_id, status, amount, currency, vendor_id, department, category, receipt_status, policy_flags, submitted_date, approved_date, notes |
/api/ap/bills |
bill_id, claim_id, status, vendor_id |
bill_id, claim_id, vendor_id, account, amount, currency, status, bill_date, due_date, invoice_number, memo |
/api/ap/payments |
payment_id, bill_id (NOT claim_id) |
payment_id, bill_id, vendor_id, amount, status, method, payment_date, bank_reference |
/api/ap/aging |
bill_id, status, vendor_id, claim_id |
bill_id, claim_id, vendor_id, amount, paid_amount, balance, status, bill_date, due_date, as_of |
/api/vendors |
vendor_id (NOT business_id) |
vendor_id, vendor_name, legal_name, status, tax_id, bank_account_last4, payment_terms, default_account, industry, updated_at |
/api/compliance/objects |
business_id, vendor_id |
business_id, business_name, vendor_id, review_status, risk_score, bank_account_status, tax_id, license_expiry, pep_status, sanctions_check_status, shell_company_suspected, ownership_layer_count, missing_fields, ubo_list[{name,ownership_pct}] |
/api/prepaids/invoices |
prepaid_invoice_id, account, vendor_id |
prepaid_invoice_id, account, vendor_id, original_amount, monthly_amortization, recognition_method, service_start, service_end, invoice_date, invoice_number, data_quality_flags, description, source_document |
/api/prepaids/gl-balances |
account, entity, period |
account, account_name, entity, period, ending_balance, loaded_at, source |
/api/close/logs |
log_id, area, period, status |
log_id, area, period, status, message, owner, related_account, created_at |
Linking keys (critical — most mistakes come from wrong joins)
- Claim → AP bill: join on
claim_id (bills carry claim_id; some bills have claim_id: null).
- AP bill → payment: join on
bill_id. Payments do NOT carry claim_id (filtering
payments by claim_id returns 0). To get all payments for a claim: claim → bills(claim_id)
→ payments(bill_id for each bill).
- Compliance → vendor: compliance object carries BOTH
business_id and vendor_id. Fetch
the vendor with /api/vendors?vendor_id=<compliance.vendor_id>. Vendors are NOT filterable by
business_id.
- Account-change batch → compliance/vendor: the batch payload lists
business_id per ticket
(and vendor_id and requested_bank_last4). Use business_id to pull compliance, then
vendor_id to pull the vendor master.
- Prepaid invoice → GL: both share
account (+ entity + period on GL). GL balances are
keyed by (account, entity, period).
2. Task archetypes & SOPs
Five recurring archetypes appear in this group. Identify which one a prompt asks for by its
answer template, then follow the matching SOP. Always return a single JSON object matching the
provided answer_template.json (key names, ordering, types, enums, and precision matter).
Archetype A — Reimbursement-to-AP close (claim triage)
Template fields: payable_claim_ids, blocked_claim_ids, paid_claim_ids, ap_open_balance_total, crm_required_claim_ids, batch_status, reviewed_claim_count.
SOP — for each candidate claim_id (in the prompt's batch):
GET /api/claims?claim_id=<id> → claim record (status, amount, vendor_id).
GET /api/ap/bills?claim_id=<id> → all bills linked to the claim.
- For each bill,
GET /api/ap/payments?bill_id=<bill_id> → payments.
- Classify the claim:
- Paid — there is a matching AP bill where
bill.amount == claim.amount AND
bill.vendor_id == claim.vendor_id AND (bill.status == "paid" OR a payment with
status == "cleared" exists for the full amount). (Claim status == "paid" corroborates.)
- Payable — claim is approved (
status == "approved") AND there is a valid OPEN AP bill:
bill.status in {scheduled, approved} (not void/paid), and bill.amount/bill.vendor_id
match the claim. The bill has an outstanding (not fully cleared) balance.
- Blocked — everything else: claim not approved, OR no AP bill, OR bill is
void, OR
bill.amount != claim.amount, OR bill.vendor_id != claim.vendor_id.
ap_open_balance_total = sum over payable claims of bill.amount − cleared_payment_total
(only payments with status == "cleared" reduce the balance; processing/scheduled
payments do NOT). Blocked and paid claims contribute 0. Use 2 decimals.
crm_required_claim_ids = blocked claims (they need expense-case owner cleanup or AP-link
remediation). Sorted ascending.
batch_status: "blocked" if any claim is blocked; else "open_payables" if any payable
claim remains; else "ready_to_close".
reviewed_claim_count = number of claim IDs in the requested batch. All ID lists ascending.
Archetype B — Stale-snapshot AP batch (eligibility + corrections)
Template fields: eligible_claim_ids, not_ready_claim_ids, ap_balance_by_claim, stale_snapshot_corrections, close_log_required, batch_status.
The staged CSV (stale_ap_snapshot.csv) is a CIRCULATED EXPORT, not the system of record. Treat
it as context; reconcile every candidate against the live API.
SOP — for each candidate claim:
- Pull live claim + bills + payments (same as Archetype A). Also read the CSV row for that claim.
- Decide eligibility to STAY in the batch:
- Eligible — claim is approved AND has a valid matching AP bill (non-void, amount & vendor
match). This includes a fully paid/matched claim (balance 0) and an open/in-flight payable.
- Not ready — claim not approved, or bill void, or amount/vendor mismatch, or no valid bill.
ap_balance_by_claim (one entry per candidate claim): for eligible claims, open balance =
valid matched bill amount − cleared payments (in-flight/processing payments do NOT reduce it);
for not-ready claims, 0.0.
stale_snapshot_corrections — pick ONE enum per candidate by comparing CSV vs live API, in
this precedence:
block_unapproved_claim — claim status != "approved" (e.g. needs_receipt,
rejected, submitted; approved_date null).
ignore_void_bill — the linked bill status == "void".
exclude_amount_or_vendor_mismatch — bill exists & non-void but bill.amount != claim.amount OR bill.vendor_id != claim.vendor_id.
replace_with_matched_paid_bill — CSV shows a scheduled/old bill but the live API has a
DIFFERENT bill that matches the claim and is paid with a cleared payment.
mark_in_flight_payment — CSV shows no payment (or none/0) but live API has a payment in
processing/scheduled (in-flight, not yet cleared).
current_snapshot_ok — CSV and live API agree (no correction needed).
close_log_required: {required: bool, ids: [...]}.
required = true when the batch needs correction/refresh (any correction that is not
current_snapshot_ok, equivalently when batch_status != "ready_to_send").
ids = AP-area close logs that document a manual AP correction superseding the stale export:
GET /api/close/logs?area=AP (optionally scoped to the period(s) of the batch's AP bills)
and select logs whose message == "Manual journal entry posted". Sorted ascending by
log_id. Empty list if none.
batch_status: "ready_to_send" if all candidates eligible and every correction is
current_snapshot_ok; "blocked" if zero candidates are eligible (all not-ready); otherwise
"needs_ap_refresh" (mixed eligible + corrections needed).
Archetype C — Vendor onboarding / KYC release
Template fields: per_business[{business_id, decision}], reportable_ubo_counts, hard_stop_flags, follow_up_business_ids, overall_release_ready.
SOP — for each business_id in the onboarding batch:
GET /api/compliance/objects?business_id=<id> → compliance record; get vendor_id.
GET /api/vendors?vendor_id=<vendor_id> → vendor master.
- Determine
as_of_date from the batch payload.
- Build
hard_stop_flags (alphabetical, empty list if none) per the table in §4.
- Compute
reportable_ubo_counts[business_id] = number of DISTINCT ubo_list names whose
aggregated (summed across all entries for that name) ownership_pct >= 25. Deduplicate by
name. Whole number ≥ 0.
decision:
approve — no hard stop flags at all.
awaiting_information — hard stops present but EVERY one is a remediable/info flag
(missing_required_documents, screening_not_run).
escalate — at least one SEVERE hard stop (confirmed_pep, sanctions_confirmed,
bank_closed, bank_name_mismatch, expired_license, shell_company_suspected,
vendor_on_hold).
follow_up_business_ids = businesses with decision != "approve" (escalate or
awaiting_information), ascending.
overall_release_ready = true only if EVERY business is approve.
Archetype D — Prepaid amortization & GL reconciliation
Template fields: period, entity, selected_invoice_ids, account_rollup{...}, invoice_results[{...}], default_missing_term_invoice_ids, exception_invoice_ids.
Inputs come from a prepaid_close_scope.json: entity, close_period (YYYY-MM), accounts, selected_prepaid_invoice_ids, variance_threshold_abs.
SOP:
period = scope close_period; entity = scope entity; selected_invoice_ids = scope
list in the same order (do NOT re-sort these two arrays; the scope order is required).
- For each scoped invoice:
GET /api/prepaids/invoices?prepaid_invoice_id=<id>.
- Compute, for the close period
P (e.g. 2025-03, treat as year=2025, month=3):
months_through_P = clamp( (Py12+Pm) − (start_y12+start_m) + 1, 0, total_term_months )
where total_term_months = (end_y12+end_m) − (start_y12+start_m) + 1.
(Counting is by calendar month inclusive of start and end months; mid-month start dates
still count the start month as a full month — NOT prorated by day.)
<period>_amortization = monthly_amortization if P's month is within
[start month … end month] inclusive, else 0.
cumulative_amortization_through_<P> = monthly_amortization × months_through_P, 2 decimals.
ending_balance = original_amount − cumulative_amortization_through_P, 2 decimals
(may be a small rounding residual like 0.01; do NOT clamp to 0).
default_missing_term_flag = ("missing_contract_dates" in data_quality_flags).
exception_flag = (data_quality_flags is non-empty) — ANY data-quality flag is an exception.
account_rollup[account] (one entry per scope account) — aggregate the invoices whose
account matches:
account_name, selected_invoice_count, original_amount_total, <period>_amortization_total,
cumulative_amortization_through_<P>, schedule_ending_balance (sums of the per-invoice
values; 2 decimals).
gl_ending_balance = GET /api/prepaids/gl-balances?account=<acct>&entity=<entity>&period=<P>
→ ending_balance.
variance_amount = schedule_ending_balance − gl_ending_balance (SIGNED; positive = schedule
over GL, negative = GL over schedule). This is the "signed close-impact direction."
variance_flag = abs(variance_amount) > variance_threshold_abs (from scope).
has_default_missing_term_flag = any invoice in the account has default_missing_term_flag == true.
account_status: "reconciled" if variance_flag == false AND
has_default_missing_term_flag == false; otherwise "requires_reconciliation".
("variance_review" is a reserved intermediate value; when variance_flag is true the
demonstrated, safe choice is requires_reconciliation.)
default_missing_term_invoice_ids = invoices with default_missing_term_flag == true,
ascending. exception_invoice_ids = invoices with exception_flag == true, ascending.
Archetype E — Account-change payment release (compliance-gated)
Template fields: task_id, batch_id, as_of_date, target_business_ids, decisions, bank_mismatch_ids, invalid_tax_ids, expired_license_ids, review_queue_ids, risk_score_override_flags (these identifiers/task_id/batch_id/as_of_date come from the
account-change batch payload).
SOP — for each business_id (use the batch review_date/as_of_date, e.g. 2025-06-01):
GET /api/compliance/objects?business_id=<id>; then GET /api/vendors?vendor_id=<vendor_id>.
- Compute per-business booleans:
bank_mismatch = compliance.bank_account_status == "name_mismatch".
bank_closed = compliance.bank_account_status == "closed".
invalid_tax = compliance.tax_id != vendor.tax_id (compliance-vs-vendor mismatch).
expired_license = license expired vs as_of_date per the month rule in §5 (AND "license"
not in missing_fields; a missing license is an info gap, not an expiry).
risk_override = compliance.risk_score >= 70.
- Aggregate lists (ascending):
bank_mismatch_ids = business_ids with bank_mismatch (name
mismatch ONLY; closed does NOT go in this list). invalid_tax_ids, expired_license_ids =
their respective booleans. risk_score_override_flags = risk_override business_ids.
decisions[business_id] (precedence — first match wins):
escalate — if invalid_tax is true (tax mismatch = highest-severity fraud signal).
hold — if any of bank_mismatch, bank_closed, expired_license, risk_override is true.
release — otherwise (no compliance issues at all).
review_queue_ids = all business_ids with decision != "release" (i.e. hold or escalate),
ascending.
Archetype F — Payment release board / payment-run priority
(When a prompt asks to sequence open AP bills into a payment run, rank by due date.)
- Pull open bills:
GET /api/ap/aging?status=scheduled and ?status=approved (or pull all and
filter to those with balance > 0 and status in {approved, scheduled} — i.e. NOT paid,
void, or draft-only).
- Rank ascending by
due_date, then ascending by bill_id as the tiebreaker. The earliest
due date is highest payment priority.
- Optionally enrich with vendor (
/api/vendors?vendor_id=) for payment_terms/bank info, and
payment evidence (/api/ap/payments?bill_id=) to confirm no cleared payment exists. Payments
that are cleared already should have balance == 0 and be excluded; processing payments
mean the bill is in-flight (treat balance as still open unless the task says otherwise).
3. Output field definitions & required enums
Use EXACTLY these enum values (matching the answer templates). Lists must be sorted as the
template specifies (usually ascending by ID). Currency: 2 decimals USD unless the template says
otherwise (one Reimbursement-to-AP template asks for "USD cents" — follow that template's unit
field literally).
Archetype A — batch_status: ready_to_close | open_payables | blocked.
Archetype B — stale_snapshot_corrections: current_snapshot_ok | mark_in_flight_payment |
replace_with_matched_paid_bill | exclude_amount_or_vendor_mismatch | ignore_void_bill |
block_unapproved_claim. batch_status: ready_to_send | needs_ap_refresh | blocked.
close_log_required: {required: bool, ids: [log_id, ...]}.
Archetype C — decision: approve | awaiting_information | escalate. hard_stop_flags
enum: bank_closed | bank_name_mismatch | confirmed_pep | expired_license |
missing_required_documents | sanctions_confirmed | screening_not_run |
shell_company_suspected | vendor_on_hold (alphabetical within each list; empty list if none).
Archetype D — account_status: reconciled | variance_review | requires_reconciliation.
Prepaid data_quality_flags observed: duplicate_invoice_number | manual_override |
missing_contract_dates | rounded_amount.
Archetype E — decision: release | hold | escalate.
Domain enum values (from the API) for reference: claim status ∈
{approved, needs_receipt, paid, rejected, submitted}; claim receipt_status ∈
{attached, missing, partial}; bill status ∈ {approved, draft, paid, scheduled, void};
payment status ∈ {cleared, processing, scheduled}; payment method ∈
{ACH, Check, Virtual card, Wire}; vendor status ∈ {active, inactive, on_hold};
compliance bank_account_status ∈ {closed, name_mismatch, not_verified, verified};
pep_status ∈ {confirmed_pep, none, not_run, possible_pep}; sanctions_check_status ∈
{clear, confirmed_match, not_run, possible_match}; review_status ∈
{approved, awaiting_information, escalated, in_review, not_started}; close-log area ∈
{AP, Compliance, Expense, GL, Prepaids, Treasury}; close-log status ∈
{blocked, closed, open, ready_for_review}; close-log message ∈
{Legacy import created duplicate line, Manual journal entry posted, Reviewer cleared variance, Support uploaded, Variance review pending, Waiting on AP export refresh}.
4. Compliance → hard_stop_flags mapping (Archetype C)
Build the alphabetical list from these field checks (all use the COMPLIANCE object unless noted;
each maps to one hard_stop enum value):
| hard_stop enum |
Trigger condition |
bank_closed |
compliance.bank_account_status == "closed" |
bank_name_mismatch |
compliance.bank_account_status == "name_mismatch" |
confirmed_pep |
compliance.pep_status == "confirmed_pep" |
expired_license |
license expired vs as_of (§5 month rule) AND "license" NOT in compliance.missing_fields |
missing_required_documents |
compliance.missing_fields is non-empty |
sanctions_confirmed |
compliance.sanctions_check_status == "confirmed_match" |
screening_not_run |
compliance.pep_status == "not_run" OR compliance.sanctions_check_status == "not_run" |
shell_company_suspected |
compliance.shell_company_suspected == true |
vendor_on_hold |
vendor.status == "on_hold" (VENDOR master) |
Notes:
possible_pep / possible_match do NOT set a hard stop (they are inconclusive, not confirmed).
- A license that is
missing (in missing_fields) sets missing_required_documents, NOT
expired_license (suppress the expiry flag in that case).
bank_account_status == "not_verified" is not a hard_stop in the enum; treat as a soft flag
(does not by itself block — but if other evidence is missing it may surface via
missing_required_document-type logic). When in doubt, only set the enumerated hard stops above.
5. Computation rules that are easy to get wrong
- License expiry — MONTH granularity: a license is expired when the calendar MONTH of
license_expiry is STRICTLY BEFORE the calendar month of as_of_date. Equivalently, a license
stays valid through the LAST DAY of its expiry month. Compare YYYY-MM(license_expiry) < YYYY-MM(as_of_date). (A license expiring 2025-05-17 is NOT expired as of 2025-05-31 — same
month — but IS expired as of 2025-06-01 — next month.) Suppress when "license" ∈
missing_fields (then it is missing_required_documents, not expired).
- Reportable UBO count: aggregate (SUM)
ownership_pct across ALL entries that share the
SAME name (the same person can appear at multiple ownership layers); count DISTINCT names
whose aggregated ownership is ≥ 25%. Do not double-count names; do not count names below 25%.
- Tax validity (Archetype E):
invalid_tax = compliance.tax_id != vendor.tax_id
(compliance record vs vendor master). A placeholder/malformed compliance tax_id will typically
also mismatch the vendor tax_id; the robust test is the cross-record mismatch.
- Open AP balance: only
cleared payments reduce the open balance. processing and
scheduled payments are in-flight and do NOT reduce it. void bills contribute 0. For
not-ready/blocked claims (no valid matched bill), the balance is 0.0.
- Prepaid amortization: always use the stored
monthly_amortization field × full calendar
months (no day-proration). ending_balance = original_amount − cumulative; keep rounding
residuals (e.g. 0.01) — do NOT force to 0. A bill whose service period ends exactly at the
close month fully amortizes (cumulative may equal original minus a 0.01 residual).
- Variance sign:
variance_amount = schedule_ending_balance − gl_ending_balance (signed,
per template definition). Negative means GL exceeds the schedule; positive means schedule
exceeds GL. variance_flag uses abs(variance_amount) > variance_threshold_abs.
- Batch status precedence (A): blocked (any blocked claim) > open_payables (any payable) >
ready_to_close. (E) decision precedence: invalid_tax→escalate > (mismatch/closed/expired/
risk)→hold > release.
- ID list ordering: sort claim/bill/business/log/invoice ID lists ascending as strings
unless the template says otherwise (Archetype D's
selected_invoice_ids/invoice_results
keep the SCOPE order, not sorted).
6. Common misjudgments & exclusion rules
- Stale-snapshot conflict (Archetype B): the circulated CSV is NOT the system of record. A
CSV row showing
payment_status=none while the live API shows a processing payment means
mark_in_flight_payment (the balance is still open). A CSV showing a scheduled bill while
the live API shows a different, matched, paid+cleared bill means replace_with_matched_paid_bill
(balance 0). Never trust the CSV amounts/statuses over the API.
- Paid vs payable (Archetypes A/B): a claim with a matching
paid bill + cleared payment
is PAID (balance 0), not payable. A scheduled/processing payment does NOT make a claim "paid."
- Claim-vs-AP alignment: the link is
claim_id (claim→bill) then bill_id (bill→payment).
Do not join payments to claims directly (no shared key). A bill is "valid" for a claim only if
amount AND vendor match AND status is not void/draft. Mismatched amount or vendor → block/exclude.
- Bill status
void: a void bill is ignored entirely (open balance 0; correction
ignore_void_bill). draft bills are not valid open AP either.
- Bank "closed" vs "name_mismatch" (Archetype E):
bank_mismatch_ids contains ONLY
name_mismatch businesses. A closed bank still drives a hold decision but is NOT listed in
bank_mismatch_ids.
- License in missing_fields: do not double-flag as expired; it is a missing-document issue.
- PEP/sanctions "possible_*": inconclusive — not a hard stop. Only
confirmed_pep and
confirmed_match block.
- UBO duplicates: the same name at multiple layers must be SUMMED then tested against 25%,
not counted per entry.
- Exception vs default-missing-term (Archetype D):
default_missing_term_flag is specifically
missing_contract_dates; exception_flag is ANY data-quality flag. An invoice can be an
exception (e.g. rounded_amount) without being a default-missing-term invoice.
- Exception priority / list ordering:
default_missing_term_invoice_ids ⊆
exception_invoice_ids (a missing-term invoice is always also an exception). Both lists sorted
ascending; do not dedupe across the two fields (they are separate outputs).
- Signed close-impact direction: report
variance_amount with its sign (schedule − GL).
Flipping the sign is a common error.
- reviewed_claim_count (Archetype A): count the CLAIMS IN THE REQUESTED BATCH (from the
prompt), not the number of bills/payments returned.
7. Recommended query order (general)
GET /endpoints once to confirm available paths (cache it).
- Read the task's staged payload (
answer_template.json is the contract; batch/scope/CSV files
are inputs). Identify the archetype from the template fields.
- Fetch the candidate entities (claims/businesses/invoices) and ALL their linked records BEFORE
classifying — a claim can have multiple bills, a bill can have multiple payments, a business
has both a compliance object and a vendor record.
- Classify per the archetype SOP, applying the precedence rules in §5.
- Round currency to 2 decimals; sort ID lists per template; emit exactly the template's keys
with the exact enum strings. Return one JSON object only (no narrative).
1---2name: fewshot-attempt-03-23description: ERP Finance Expense-Control Skill (task_group_005)4---5# ERP Finance Expense-Control Skill (task_group_005)67A self-contained playbook for solving ERP finance expense-control tasks against the shared8remote API. The solver only needs the remote API base URL (provided per task in9`environment_access.md` or the prompt). Discover everything else from the API itself.1011> NEVER call any `/api/judge` endpoint. NEVER read local `env/` sources, evaluator files, or12> test/train answer keys. The remote API is the only system of record. Staged local payloads13> (answer templates, batch/scope files, stale CSVs) are inputs only.1415---1617## 1. API access & discovery contract1819- **Base URL:** the runner-provided remote URL (e.g. `<remote-env-url>`). Prompts may20 also mention `http://127.0.0.1:8005`; always use the remote URL from `environment_access.md`.21- **Discover endpoints:** `GET {base}/endpoints` returns the list of paths and the filtering22 contract. Health: `GET /health`, `GET /api/health`.23- **Filtering:** list endpoints accept **exact-match query parameters by field name**, plus24 `limit` and `offset` for pagination. Default limit is 100. There is no fuzzy/partial matching.25- **Namespaced vs bare paths:** most resources exist under BOTH a namespaced `/api/...` path and26 a bare root path (aliases). Prefer the `/api/...` form. Known pairs:27 - `/api/claims` == `/claims`28 - `/api/ap/bills` == `/bills`29 - `/api/ap/payments` == `/payments`30 - `/api/ap/aging` (aging view, no bare alias documented)31 - `/api/vendors` == `/vendors`32 - `/api/compliance/objects` == `/compliance/objects`33 - `/api/prepaids/invoices` == `/prepaids/invoices`34 - `/api/prepaids/gl-balances` == `/gl/balances`35 - `/api/close/logs` == `/close/logs`36- **Response shape (every list endpoint):** `{ "count", "data": [...], "endpoint", "limit",37 "offset", "total" }`. Read `data`; use `total`/`offset` to paginate when a set may exceed 100.3839### Endpoint catalog (key fields)4041| Resource | Filterable keys (used) | Key fields returned |42|---|---|---|43| `/api/claims` | `claim_id` | `claim_id, status, amount, currency, vendor_id, department, category, receipt_status, policy_flags, submitted_date, approved_date, notes` |44| `/api/ap/bills` | `bill_id`, `claim_id`, `status`, `vendor_id` | `bill_id, claim_id, vendor_id, account, amount, currency, status, bill_date, due_date, invoice_number, memo` |45| `/api/ap/payments` | `payment_id`, **`bill_id`** (NOT `claim_id`) | `payment_id, bill_id, vendor_id, amount, status, method, payment_date, bank_reference` |46| `/api/ap/aging` | `bill_id`, `status`, `vendor_id`, `claim_id` | `bill_id, claim_id, vendor_id, amount, paid_amount, balance, status, bill_date, due_date, as_of` |47| `/api/vendors` | `vendor_id` (NOT `business_id`) | `vendor_id, vendor_name, legal_name, status, tax_id, bank_account_last4, payment_terms, default_account, industry, updated_at` |48| `/api/compliance/objects` | `business_id`, `vendor_id` | `business_id, business_name, vendor_id, review_status, risk_score, bank_account_status, tax_id, license_expiry, pep_status, sanctions_check_status, shell_company_suspected, ownership_layer_count, missing_fields, ubo_list[{name,ownership_pct}]` |49| `/api/prepaids/invoices` | `prepaid_invoice_id`, `account`, `vendor_id` | `prepaid_invoice_id, account, vendor_id, original_amount, monthly_amortization, recognition_method, service_start, service_end, invoice_date, invoice_number, data_quality_flags, description, source_document` |50| `/api/prepaids/gl-balances` | `account`, `entity`, `period` | `account, account_name, entity, period, ending_balance, loaded_at, source` |51| `/api/close/logs` | `log_id`, `area`, `period`, `status` | `log_id, area, period, status, message, owner, related_account, created_at` |5253### Linking keys (critical — most mistakes come from wrong joins)5455- **Claim → AP bill:** join on `claim_id` (bills carry `claim_id`; some bills have `claim_id: null`).56- **AP bill → payment:** join on **`bill_id`**. Payments do NOT carry `claim_id` (filtering57 payments by `claim_id` returns 0). To get all payments for a claim: claim → bills(`claim_id`)58 → payments(`bill_id` for each bill).59- **Compliance → vendor:** compliance object carries BOTH `business_id` and `vendor_id`. Fetch60 the vendor with `/api/vendors?vendor_id=<compliance.vendor_id>`. Vendors are NOT filterable by61 `business_id`.62- **Account-change batch → compliance/vendor:** the batch payload lists `business_id` per ticket63 (and `vendor_id` and `requested_bank_last4`). Use `business_id` to pull compliance, then64 `vendor_id` to pull the vendor master.65- **Prepaid invoice → GL:** both share `account` (+ `entity` + `period` on GL). GL balances are66 keyed by (account, entity, period).6768---6970## 2. Task archetypes & SOPs7172Five recurring archetypes appear in this group. Identify which one a prompt asks for by its73answer template, then follow the matching SOP. Always return a single JSON object matching the74provided `answer_template.json` (key names, ordering, types, enums, and precision matter).7576### Archetype A — Reimbursement-to-AP close (claim triage)77Template fields: `payable_claim_ids, blocked_claim_ids, paid_claim_ids, ap_open_balance_total,78crm_required_claim_ids, batch_status, reviewed_claim_count`.7980**SOP — for each candidate `claim_id` (in the prompt's batch):**811. `GET /api/claims?claim_id=<id>` → claim record (`status, amount, vendor_id`).822. `GET /api/ap/bills?claim_id=<id>` → all bills linked to the claim.833. For each bill, `GET /api/ap/payments?bill_id=<bill_id>` → payments.844. Classify the claim:85 - **Paid** — there is a *matching* AP bill where `bill.amount == claim.amount` AND86 `bill.vendor_id == claim.vendor_id` AND (`bill.status == "paid"` OR a payment with87 `status == "cleared"` exists for the full amount). (Claim `status == "paid"` corroborates.)88 - **Payable** — claim is approved (`status == "approved"`) AND there is a valid OPEN AP bill:89 `bill.status` in `{scheduled, approved}` (not void/paid), and `bill.amount`/`bill.vendor_id`90 match the claim. The bill has an outstanding (not fully cleared) balance.91 - **Blocked** — everything else: claim not approved, OR no AP bill, OR bill is `void`, OR92 `bill.amount != claim.amount`, OR `bill.vendor_id != claim.vendor_id`.935. `ap_open_balance_total` = sum over **payable** claims of `bill.amount − cleared_payment_total`94 (only payments with `status == "cleared"` reduce the balance; `processing`/`scheduled`95 payments do NOT). Blocked and paid claims contribute 0. Use 2 decimals.966. `crm_required_claim_ids` = blocked claims (they need expense-case owner cleanup or AP-link97 remediation). Sorted ascending.987. `batch_status`: `"blocked"` if any claim is blocked; else `"open_payables"` if any payable99 claim remains; else `"ready_to_close"`.1008. `reviewed_claim_count` = number of claim IDs in the requested batch. All ID lists ascending.101102### Archetype B — Stale-snapshot AP batch (eligibility + corrections)103Template fields: `eligible_claim_ids, not_ready_claim_ids, ap_balance_by_claim,104stale_snapshot_corrections, close_log_required, batch_status`.105106The staged CSV (`stale_ap_snapshot.csv`) is a CIRCULATED EXPORT, not the system of record. Treat107it as context; reconcile every candidate against the live API.108109**SOP — for each candidate claim:**1101. Pull live claim + bills + payments (same as Archetype A). Also read the CSV row for that claim.1112. Decide eligibility to STAY in the batch:112 - **Eligible** — claim is approved AND has a valid matching AP bill (non-void, amount & vendor113 match). This includes a fully paid/matched claim (balance 0) and an open/in-flight payable.114 - **Not ready** — claim not approved, or bill void, or amount/vendor mismatch, or no valid bill.1153. `ap_balance_by_claim` (one entry per candidate claim): for eligible claims, open balance =116 valid matched bill amount − cleared payments (in-flight/processing payments do NOT reduce it);117 for not-ready claims, `0.0`.1184. `stale_snapshot_corrections` — pick ONE enum per candidate by comparing CSV vs live API, in119 this precedence:120 1. `block_unapproved_claim` — claim `status != "approved"` (e.g. `needs_receipt`,121 `rejected`, `submitted`; `approved_date` null).122 2. `ignore_void_bill` — the linked bill `status == "void"`.123 3. `exclude_amount_or_vendor_mismatch` — bill exists & non-void but `bill.amount !=124 claim.amount` OR `bill.vendor_id != claim.vendor_id`.125 4. `replace_with_matched_paid_bill` — CSV shows a scheduled/old bill but the live API has a126 DIFFERENT bill that matches the claim and is `paid` with a cleared payment.127 5. `mark_in_flight_payment` — CSV shows no payment (or `none`/0) but live API has a payment in128 `processing`/`scheduled` (in-flight, not yet cleared).129 6. `current_snapshot_ok` — CSV and live API agree (no correction needed).1305. `close_log_required`: `{required: bool, ids: [...]}`.131 - `required = true` when the batch needs correction/refresh (any correction that is not132 `current_snapshot_ok`, equivalently when `batch_status != "ready_to_send"`).133 - `ids` = AP-area close logs that document a manual AP correction superseding the stale export:134 `GET /api/close/logs?area=AP` (optionally scoped to the period(s) of the batch's AP bills)135 and select logs whose `message == "Manual journal entry posted"`. Sorted ascending by136 `log_id`. Empty list if none.1376. `batch_status`: `"ready_to_send"` if all candidates eligible and every correction is138 `current_snapshot_ok`; `"blocked"` if zero candidates are eligible (all not-ready); otherwise139 `"needs_ap_refresh"` (mixed eligible + corrections needed).140141### Archetype C — Vendor onboarding / KYC release142Template fields: `per_business[{business_id, decision}], reportable_ubo_counts,143hard_stop_flags, follow_up_business_ids, overall_release_ready`.144145**SOP — for each `business_id` in the onboarding batch:**1461. `GET /api/compliance/objects?business_id=<id>` → compliance record; get `vendor_id`.1472. `GET /api/vendors?vendor_id=<vendor_id>` → vendor master.1483. Determine `as_of_date` from the batch payload.1494. Build `hard_stop_flags` (alphabetical, empty list if none) per the table in §4.1505. Compute `reportable_ubo_counts[business_id]` = number of DISTINCT `ubo_list` names whose151 **aggregated (summed across all entries for that name) `ownership_pct >= 25`**. Deduplicate by152 name. Whole number ≥ 0.1536. `decision`:154 - `approve` — no hard stop flags at all.155 - `awaiting_information` — hard stops present but EVERY one is a remediable/info flag156 (`missing_required_documents`, `screening_not_run`).157 - `escalate` — at least one SEVERE hard stop (`confirmed_pep`, `sanctions_confirmed`,158 `bank_closed`, `bank_name_mismatch`, `expired_license`, `shell_company_suspected`,159 `vendor_on_hold`).1607. `follow_up_business_ids` = businesses with `decision != "approve"` (escalate or161 awaiting_information), ascending.1628. `overall_release_ready` = `true` only if EVERY business is `approve`.163164### Archetype D — Prepaid amortization & GL reconciliation165Template fields: `period, entity, selected_invoice_ids, account_rollup{...},166invoice_results[{...}], default_missing_term_invoice_ids, exception_invoice_ids`.167168Inputs come from a `prepaid_close_scope.json`: `entity, close_period (YYYY-MM), accounts,169selected_prepaid_invoice_ids, variance_threshold_abs`.170171**SOP:**1721. `period` = scope `close_period`; `entity` = scope `entity`; `selected_invoice_ids` = scope173 list **in the same order** (do NOT re-sort these two arrays; the scope order is required).1742. For each scoped invoice: `GET /api/prepaids/invoices?prepaid_invoice_id=<id>`.1753. Compute, for the close period `P` (e.g. 2025-03, treat as year=2025, month=3):176 - `months_through_P` = clamp( (Py*12+Pm) − (start_y*12+start_m) + 1, 0, total_term_months )177 where `total_term_months` = (end_y*12+end_m) − (start_y*12+start_m) + 1.178 (Counting is by calendar month inclusive of start and end months; mid-month start dates179 still count the start month as a full month — NOT prorated by day.)180 - `<period>_amortization` = `monthly_amortization` if P's month is within181 [start month … end month] inclusive, else `0`.182 - `cumulative_amortization_through_<P>` = `monthly_amortization × months_through_P`, 2 decimals.183 - `ending_balance` = `original_amount − cumulative_amortization_through_P`, 2 decimals184 (may be a small rounding residual like `0.01`; do NOT clamp to 0).185 - `default_missing_term_flag` = `("missing_contract_dates" in data_quality_flags)`.186 - `exception_flag` = `(data_quality_flags is non-empty)` — ANY data-quality flag is an exception.1874. `account_rollup[account]` (one entry per scope account) — aggregate the invoices whose188 `account` matches:189 - `account_name`, `selected_invoice_count`, `original_amount_total`, `<period>_amortization_total`,190 `cumulative_amortization_through_<P>`, `schedule_ending_balance` (sums of the per-invoice191 values; 2 decimals).192 - `gl_ending_balance` = `GET /api/prepaids/gl-balances?account=<acct>&entity=<entity>&period=<P>`193 → `ending_balance`.194 - `variance_amount` = `schedule_ending_balance − gl_ending_balance` (SIGNED; positive = schedule195 over GL, negative = GL over schedule). This is the "signed close-impact direction."196 - `variance_flag` = `abs(variance_amount) > variance_threshold_abs` (from scope).197 - `has_default_missing_term_flag` = any invoice in the account has `default_missing_term_flag == true`.198 - `account_status`: `"reconciled"` if `variance_flag == false` AND199 `has_default_missing_term_flag == false`; otherwise `"requires_reconciliation"`.200 (`"variance_review"` is a reserved intermediate value; when variance_flag is true the201 demonstrated, safe choice is `requires_reconciliation`.)2025. `default_missing_term_invoice_ids` = invoices with `default_missing_term_flag == true`,203 ascending. `exception_invoice_ids` = invoices with `exception_flag == true`, ascending.204205### Archetype E — Account-change payment release (compliance-gated)206Template fields: `task_id, batch_id, as_of_date, target_business_ids, decisions,207bank_mismatch_ids, invalid_tax_ids, expired_license_ids, review_queue_ids,208risk_score_override_flags` (these identifiers/`task_id`/`batch_id`/`as_of_date` come from the209account-change batch payload).210211**SOP — for each `business_id` (use the batch `review_date`/`as_of_date`, e.g. 2025-06-01):**2121. `GET /api/compliance/objects?business_id=<id>`; then `GET /api/vendors?vendor_id=<vendor_id>`.2132. Compute per-business booleans:214 - `bank_mismatch` = `compliance.bank_account_status == "name_mismatch"`.215 - `bank_closed` = `compliance.bank_account_status == "closed"`.216 - `invalid_tax` = `compliance.tax_id != vendor.tax_id` (compliance-vs-vendor mismatch).217 - `expired_license` = license expired vs `as_of_date` per the month rule in §5 (AND `"license"`218 not in `missing_fields`; a missing license is an info gap, not an expiry).219 - `risk_override` = `compliance.risk_score >= 70`.2203. Aggregate lists (ascending): `bank_mismatch_ids` = business_ids with `bank_mismatch` (name221 mismatch ONLY; `closed` does NOT go in this list). `invalid_tax_ids`, `expired_license_ids` =222 their respective booleans. `risk_score_override_flags` = `risk_override` business_ids.2234. `decisions[business_id]` (precedence — first match wins):224 1. `escalate` — if `invalid_tax` is true (tax mismatch = highest-severity fraud signal).225 2. `hold` — if any of `bank_mismatch`, `bank_closed`, `expired_license`, `risk_override` is true.226 3. `release` — otherwise (no compliance issues at all).2275. `review_queue_ids` = all business_ids with `decision != "release"` (i.e. hold or escalate),228 ascending.229230### Archetype F — Payment release board / payment-run priority231(When a prompt asks to sequence open AP bills into a payment run, rank by due date.)2321. Pull open bills: `GET /api/ap/aging?status=scheduled` and `?status=approved` (or pull all and233 filter to those with `balance > 0` and `status` in `{approved, scheduled}` — i.e. NOT `paid`,234 `void`, or `draft`-only).2352. Rank **ascending by `due_date`, then ascending by `bill_id`** as the tiebreaker. The earliest236 due date is highest payment priority.2373. Optionally enrich with vendor (`/api/vendors?vendor_id=`) for payment_terms/bank info, and238 payment evidence (`/api/ap/payments?bill_id=`) to confirm no cleared payment exists. Payments239 that are `cleared` already should have `balance == 0` and be excluded; `processing` payments240 mean the bill is in-flight (treat balance as still open unless the task says otherwise).241242---243244## 3. Output field definitions & required enums245246Use EXACTLY these enum values (matching the answer templates). Lists must be sorted as the247template specifies (usually ascending by ID). Currency: 2 decimals USD unless the template says248otherwise (one Reimbursement-to-AP template asks for "USD cents" — follow that template's unit249field literally).250251**Archetype A — batch_status:** `ready_to_close` | `open_payables` | `blocked`.252**Archetype B — stale_snapshot_corrections:** `current_snapshot_ok` | `mark_in_flight_payment` |253`replace_with_matched_paid_bill` | `exclude_amount_or_vendor_mismatch` | `ignore_void_bill` |254`block_unapproved_claim`. **batch_status:** `ready_to_send` | `needs_ap_refresh` | `blocked`.255**close_log_required:** `{required: bool, ids: [log_id, ...]}`.256**Archetype C — decision:** `approve` | `awaiting_information` | `escalate`. **hard_stop_flags257enum:** `bank_closed` | `bank_name_mismatch` | `confirmed_pep` | `expired_license` |258`missing_required_documents` | `sanctions_confirmed` | `screening_not_run` |259`shell_company_suspected` | `vendor_on_hold` (alphabetical within each list; empty list if none).260**Archetype D — account_status:** `reconciled` | `variance_review` | `requires_reconciliation`.261**Prepaid data_quality_flags observed:** `duplicate_invoice_number` | `manual_override` |262`missing_contract_dates` | `rounded_amount`.263**Archetype E — decision:** `release` | `hold` | `escalate`.264**Domain enum values (from the API) for reference:** claim `status` ∈265`{approved, needs_receipt, paid, rejected, submitted}`; claim `receipt_status` ∈266`{attached, missing, partial}`; bill `status` ∈ `{approved, draft, paid, scheduled, void}`;267payment `status` ∈ `{cleared, processing, scheduled}`; payment `method` ∈268`{ACH, Check, Virtual card, Wire}`; vendor `status` ∈ `{active, inactive, on_hold}`;269compliance `bank_account_status` ∈ `{closed, name_mismatch, not_verified, verified}`;270`pep_status` ∈ `{confirmed_pep, none, not_run, possible_pep}`; `sanctions_check_status` ∈271`{clear, confirmed_match, not_run, possible_match}`; `review_status` ∈272`{approved, awaiting_information, escalated, in_review, not_started}`; close-log `area` ∈273`{AP, Compliance, Expense, GL, Prepaids, Treasury}`; close-log `status` ∈274`{blocked, closed, open, ready_for_review}`; close-log `message` ∈275`{Legacy import created duplicate line, Manual journal entry posted, Reviewer cleared variance,276Support uploaded, Variance review pending, Waiting on AP export refresh}`.277278---279280## 4. Compliance → hard_stop_flags mapping (Archetype C)281282Build the alphabetical list from these field checks (all use the COMPLIANCE object unless noted;283each maps to one hard_stop enum value):284285| hard_stop enum | Trigger condition |286|---|---|287| `bank_closed` | `compliance.bank_account_status == "closed"` |288| `bank_name_mismatch` | `compliance.bank_account_status == "name_mismatch"` |289| `confirmed_pep` | `compliance.pep_status == "confirmed_pep"` |290| `expired_license` | license expired vs as_of (§5 month rule) AND `"license"` NOT in `compliance.missing_fields` |291| `missing_required_documents` | `compliance.missing_fields` is non-empty |292| `sanctions_confirmed` | `compliance.sanctions_check_status == "confirmed_match"` |293| `screening_not_run` | `compliance.pep_status == "not_run"` OR `compliance.sanctions_check_status == "not_run"` |294| `shell_company_suspected` | `compliance.shell_company_suspected == true` |295| `vendor_on_hold` | `vendor.status == "on_hold"` (VENDOR master) |296297Notes:298- `possible_pep` / `possible_match` do NOT set a hard stop (they are inconclusive, not confirmed).299- A license that is `missing` (in `missing_fields`) sets `missing_required_documents`, NOT300 `expired_license` (suppress the expiry flag in that case).301- `bank_account_status == "not_verified"` is not a hard_stop in the enum; treat as a soft flag302 (does not by itself block — but if other evidence is missing it may surface via303 `missing_required_document`-type logic). When in doubt, only set the enumerated hard stops above.304305---306307## 5. Computation rules that are easy to get wrong308309- **License expiry — MONTH granularity:** a license is expired when the calendar MONTH of310 `license_expiry` is STRICTLY BEFORE the calendar month of `as_of_date`. Equivalently, a license311 stays valid through the LAST DAY of its expiry month. Compare `YYYY-MM(license_expiry) <312 YYYY-MM(as_of_date)`. (A license expiring 2025-05-17 is NOT expired as of 2025-05-31 — same313 month — but IS expired as of 2025-06-01 — next month.) Suppress when `"license"` ∈314 `missing_fields` (then it is `missing_required_documents`, not expired).315- **Reportable UBO count:** aggregate (SUM) `ownership_pct` across ALL entries that share the316 SAME `name` (the same person can appear at multiple ownership layers); count DISTINCT names317 whose aggregated ownership is **≥ 25%**. Do not double-count names; do not count names below 25%.318- **Tax validity (Archetype E):** `invalid_tax` = `compliance.tax_id != vendor.tax_id`319 (compliance record vs vendor master). A placeholder/malformed compliance tax_id will typically320 also mismatch the vendor tax_id; the robust test is the cross-record mismatch.321- **Open AP balance:** only `cleared` payments reduce the open balance. `processing` and322 `scheduled` payments are in-flight and do NOT reduce it. `void` bills contribute 0. For323 not-ready/blocked claims (no valid matched bill), the balance is `0.0`.324- **Prepaid amortization:** always use the stored `monthly_amortization` field × full calendar325 months (no day-proration). `ending_balance = original_amount − cumulative`; keep rounding326 residuals (e.g. `0.01`) — do NOT force to 0. A bill whose service period ends exactly at the327 close month fully amortizes (cumulative may equal original minus a 0.01 residual).328- **Variance sign:** `variance_amount = schedule_ending_balance − gl_ending_balance` (signed,329 per template definition). Negative means GL exceeds the schedule; positive means schedule330 exceeds GL. `variance_flag` uses `abs(variance_amount) > variance_threshold_abs`.331- **Batch status precedence (A):** blocked (any blocked claim) > open_payables (any payable) >332 ready_to_close. **(E) decision precedence:** invalid_tax→escalate > (mismatch/closed/expired/333 risk)→hold > release.334- **ID list ordering:** sort claim/bill/business/log/invoice ID lists ascending as strings335 unless the template says otherwise (Archetype D's `selected_invoice_ids`/`invoice_results`336 keep the SCOPE order, not sorted).337338---339340## 6. Common misjudgments & exclusion rules341342- **Stale-snapshot conflict (Archetype B):** the circulated CSV is NOT the system of record. A343 CSV row showing `payment_status=none` while the live API shows a `processing` payment means344 `mark_in_flight_payment` (the balance is still open). A CSV showing a `scheduled` bill while345 the live API shows a different, matched, `paid`+cleared bill means `replace_with_matched_paid_bill`346 (balance 0). Never trust the CSV amounts/statuses over the API.347- **Paid vs payable (Archetypes A/B):** a claim with a matching `paid` bill + `cleared` payment348 is PAID (balance 0), not payable. A scheduled/processing payment does NOT make a claim "paid."349- **Claim-vs-AP alignment:** the link is `claim_id` (claim→bill) then `bill_id` (bill→payment).350 Do not join payments to claims directly (no shared key). A bill is "valid" for a claim only if351 amount AND vendor match AND status is not void/draft. Mismatched amount or vendor → block/exclude.352- **Bill status `void`:** a void bill is ignored entirely (open balance 0; correction353 `ignore_void_bill`). `draft` bills are not valid open AP either.354- **Bank "closed" vs "name_mismatch" (Archetype E):** `bank_mismatch_ids` contains ONLY355 `name_mismatch` businesses. A `closed` bank still drives a `hold` decision but is NOT listed in356 `bank_mismatch_ids`.357- **License in missing_fields:** do not double-flag as expired; it is a missing-document issue.358- **PEP/sanctions "possible_*":** inconclusive — not a hard stop. Only `confirmed_pep` and359 `confirmed_match` block.360- **UBO duplicates:** the same name at multiple layers must be SUMMED then tested against 25%,361 not counted per entry.362- **Exception vs default-missing-term (Archetype D):** `default_missing_term_flag` is specifically363 `missing_contract_dates`; `exception_flag` is ANY data-quality flag. An invoice can be an364 exception (e.g. `rounded_amount`) without being a default-missing-term invoice.365- **Exception priority / list ordering:** `default_missing_term_invoice_ids` ⊆366 `exception_invoice_ids` (a missing-term invoice is always also an exception). Both lists sorted367 ascending; do not dedupe across the two fields (they are separate outputs).368- **Signed close-impact direction:** report `variance_amount` with its sign (schedule − GL).369 Flipping the sign is a common error.370- **reviewed_claim_count (Archetype A):** count the CLAIMS IN THE REQUESTED BATCH (from the371 prompt), not the number of bills/payments returned.372373---374375## 7. Recommended query order (general)3763771. `GET /endpoints` once to confirm available paths (cache it).3782. Read the task's staged payload (`answer_template.json` is the contract; batch/scope/CSV files379 are inputs). Identify the archetype from the template fields.3803. Fetch the candidate entities (claims/businesses/invoices) and ALL their linked records BEFORE381 classifying — a claim can have multiple bills, a bill can have multiple payments, a business382 has both a compliance object and a vendor record.3834. Classify per the archetype SOP, applying the precedence rules in §5.3845. Round currency to 2 decimals; sort ID lists per template; emit exactly the template's keys385 with the exact enum strings. Return one JSON object only (no narrative).