SKILL — task_group_005 ERP Finance Expense-Control Close
Transferable, executable experience for solving ERP finance expense-control tasks in the
task_group_005 shared API environment. Covers four task archetypes that recur across
train/test pairs:
- A. Reimbursement-to-AP close review (claim → AP bill → payment reconciliation; payable/blocked/paid partition; stale AP-snapshot correction).
- B. Vendor onboarding / KYC release board (compliance hard-stop flags, UBO reporting, release decisions).
- C. Prepaid amortization & GL reconciliation close (straight-line monthly schedule vs. GL ending balance, variance + data-quality flags).
- D. Payment-release risk review after vendor account-change events (compliance + vendor + bank/tax/license/risk gating, release/hold/escalate).
The rules below are derived from working the 5 train tasks against the live API. They are
rules and decision criteria, not a lookup of any answer set.
1. API contract
Base URL & discovery
- Remote API base:
<remote-env-url> (a prompt may print http://127.0.0.1:8005; the
remote URL above is the shared system of record — use it).
GET /endpoints → lists all paths and the filtering contract.
GET /health and GET /api/health → {status, task_group}.
- Currency convention: report all amounts to 2 decimals in USD unless a task says
otherwise (see §9 for the "USD cents" trap).
Endpoints (canonical + alias)
Every resource has a namespaced /api/... path and, for most, a bare alias. Both return
identical data. Use the /api/... form.
| Resource |
Canonical path |
Alias |
| Claims |
/api/claims |
/claims |
| AP bills |
/api/ap/bills |
/bills |
| AP payments |
/api/ap/payments |
/payments |
| AP aging |
/api/ap/aging |
— |
| Vendors |
/api/vendors |
/vendors |
| Compliance (KYC) objects |
/api/compliance/objects |
/compliance/objects |
| Prepaid invoices |
/api/prepaids/invoices |
/prepaids/invoices |
| GL balances (general) |
/api/prepaids/gl-balances |
/gl/balances |
| Close logs |
/api/close/logs |
/close/logs |
Filtering contract
- Exact-match query parameters by field name. Examples:
?claim_id=CLM-2025-0038,
?business_id=BUS-2025-0009, ?vendor_id=VEN-0064, ?account=1250, ?period=2025-03,
?status=blocked.
- Paginate with
limit (default 100) and offset. Always pass limit when you need the
full set; default page may be smaller than total.
- A list by a unique field returns
{count, data:[...], total, limit, offset}. Filter by a
join key that can repeat (e.g. ?claim_id=) returns all matching rows — use this to
pull every bill/payment for one claim.
- Path-style single fetch (
/api/claims/<id>) works for claims but not for bills
(/api/ap/bills/<id> returns not_found). Prefer the query-param form.
2. Data model — fields that matter
Claims /api/claims
claim_id, status, amount, currency, submitted_date, approved_date, category, department, employee_name, receipt_status, policy_flags[], notes, vendor_id.
status values seen: needs_receipt, approved, paid (also rejected/draft possible).
policy_flags values seen: manual_rate, duplicate_amount, over_limit, late_receipt.
Policy flags do not by themselves block AP release once the claim is approved.
receipt_status: attached vs partial.
vendor_id may be null (employee reimbursement) — do not treat null vendor as a mismatch
against a bill vendor; use amount alignment instead.
AP bills /api/ap/bills
bill_id, claim_id, vendor_id, account, amount, currency, bill_date, due_date, invoice_number, memo, status.
status values: draft, approved, scheduled, paid, void.
account is the GL account hit (e.g. 6200 expense, 2100 AP clearing, 1250/1251 prepaid,
6500 T&E). A bill linked to a reimbursement claim but on a prepaid account with a
non-matching amount/vendor is not that claim's reimbursement bill.
- A single
claim_id can map to multiple bills (e.g. a prepaid bill + the real paid
reimbursement bill). Always fetch all bills for the claim and pick the one matching claim
amount (+ vendor).
AP payments /api/ap/payments
payment_id, bill_id, vendor_id, amount, status, method, payment_date, bank_reference.
status values: none(implicit), processing, scheduled, cleared, failed.
- Paid = bill
status=paid AND a payment with status=cleared for the bill/claim amount.
processing / scheduled payments are in-flight, not cleared → the bill is still an
open payable.
AP aging /api/ap/aging
bill_id, claim_id, vendor_id, amount, paid_amount, balance, bill_date, due_date, status, as_of.
- Used for payment-priority ranking. Note: aging rows can overlap/duplicate canonical
bills with the same
bill_id but different status — do not age-rank from here unless the
task explicitly asks for aging; rank from /api/ap/bills + /api/ap/payments instead.
Vendors /api/vendors
vendor_id, vendor_name, legal_name, status, tax_id, bank_account_last4, default_account, industry, payment_terms, updated_at.
status: active, on_hold, (others possible). on_hold is a release hard-stop.
tax_id here is the vendor-master tax id — compare against compliance.tax_id to find
corrupted/placeholder tax ids.
bank_account_last4 — compare against the account-change ticket's requested_bank_last4.
Compliance objects /api/compliance/objects
business_id, business_name, vendor_id, jurisdiction, registration_number, tax_id, bank_account_status, license_expiry, missing_fields[], ownership_layer_count, ubo_list[], pep_status, sanctions_check_status, shell_company_suspected, review_status, risk_score.
bank_account_status: verified, name_mismatch, closed.
pep_status: none, possible_pep, confirmed_pep, not_run.
sanctions_check_status: clear, not_run, (confirmed).
review_status: not_started, in_review, awaiting_information, approved, escalated.
risk_score: integer 0–100.
ubo_list[]: {name, ownership_pct}.
missing_fields[]: document names e.g. license, beneficial_owner_id.
Prepaid invoices /api/prepaids/invoices
prepaid_invoice_id, account, vendor_id, invoice_number, invoice_date, service_start, service_end, original_amount, monthly_amortization, recognition_method, description, source_document, data_quality_flags[].
recognition_method: straight_line.
data_quality_flags values seen: rounded_amount, missing_contract_dates.
GL balances /api/prepaids/gl-balances (= /gl/balances)
account, account_name, entity, period (YYYY-MM), ending_balance, source, loaded_at.
- Multiple periods per account; filter by
account and period. Entity = legal entity string.
Close logs /api/close/logs
log_id, area, period, related_account, message, owner, status, created_at.
area: AP, Expense, Prepaids, Treasury, GL, Compliance.
status: closed, ready_for_review, open, blocked.
message patterns: Waiting on AP export refresh, Variance review pending,
Reviewer cleared variance, Manual journal entry posted, Support uploaded,
Legacy import created duplicate line.
3. Task A — Reimbursement-to-AP close review
Goal: partition a list of candidate claim IDs into payable / blocked / paid, compute the
open AP balance, and set an overall batch status. Variant with a stale CSV snapshot also
classifies a per-claim snapshot correction.
SOP — query order
GET /api/claims?claim_id=<each> → claim status, amount, vendor_id, policy_flags.
GET /api/ap/bills?claim_id=<each> → every bill linked to the claim.
- For each bill,
GET /api/ap/payments?bill_id=<each bill_id> → payment evidence.
- Reconcile per claim (rules below). Sum open AP for payable claims only.
Claim classification rules
- paid: claim has an AP bill whose
status=paid AND whose amount equals the claim
amount AND a payment with status=cleared for that amount. (Claim status=paid is a hint;
the paid bill + cleared payment is the proof.)
- payable (eligible): claim
status=approved AND there is an open AP bill
(status in approved/scheduled — i.e. not paid, not void, not draft-blocking) whose
amount equals the claim amount and (when claim has a vendor) vendor matches. In-flight
payments (processing/scheduled) do not make it paid — the bill is still an open
payable.
- blocked: not paid AND not payable, because the AP link is wrong/missing. Reasons:
- no bill at all for the claim;
- the only linked bill is
void;
- the linked bill's
amount ≠ claim amount, or (when both have a vendor) vendor mismatch
(these are AP-link problems); or
- the claim itself is unapproved (
needs_receipt/rejected) — blocked until the expense
case is fixed.
- Policy flags (
over_limit, late_receipt, manual_rate, duplicate_amount) and
partial receipts do not auto-block an approved claim; they are case-side metadata.
They only matter when the claim is not approved, or to explain a separate case-cleanup need.
crm_required vs blocked
crm_required_claim_ids = blocked claims that need expense-case owner cleanup OR AP-link
remediation. In practice this equals the blocked set (every blocked claim needs one or the
other). Do not put payable or paid claims here.
ap_open_balance_total
Sum of open AP balances for payable claims only. Open AP balance per claim =
bill.amount − (sum of cleared payments on that bill). In-flight payments are not
subtracted. Void/mismatched bills contribute 0 (excluded).
batch_status (A, simple template)
blocked if any claim is blocked; else open_payables if any payable (valid unpaid AP
remains); else ready_to_close.
Stale-snapshot variant (separate template) — stale_snapshot_corrections
Compare the circulated CSV snapshot to the current API. Per candidate claim pick one enum:
current_snapshot_ok — snapshot matches current ERP (bill id/amount/status, payment) and
claim is an open payable → keep as-is.
mark_in_flight_payment — snapshot shows payment none/0.00 but current ERP has a
processing/scheduled (uncleared) payment on the bill.
replace_with_matched_paid_bill — snapshot points to a wrong/scheduled bill (often a
non-matching prepaid bill) but the real matched bill is paid + cleared for the claim
amount; replace the snapshot row with that matched paid bill (claim is settled, not in the
open batch).
exclude_amount_or_vendor_mismatch — the snapshot/current bill amount ≠ claim amount or
vendor mismatches; exclude that AP row from the claim's balance.
ignore_void_bill — snapshot shows the bill as approved but current ERP status=void;
ignore it (contributes 0 to AP balance).
block_unapproved_claim — the claim is not approved (needs_receipt/rejected); block it
regardless of bill state.
ap_balance_by_claim (stale variant)
Keys = all candidate claim IDs. Value = open AP balance after applying cleared payments
and ignoring stale/voided/mismatched rows. Paid claims and blocked/mismatched/void claims
= 0.00.
not_ready_claim_ids (stale variant)
Claims that should not remain in the batch: paid (settled), unapproved, void-bill-only, or
amount/vendor-mismatched. (A paid claim is "not ready" for the open batch because it is
already settled.)
close_log_required (stale variant)
required: true when non-closed close logs whose message is Waiting on AP export refresh
exist for accounts touched by the batch's bills.
ids = those non-closed (ready_for_review/open/blocked) Waiting on AP export refresh
log IDs, ascending. Filter to related_account ∈ the set of bill accounts in the candidate
batch. (Closed Waiting on AP export refresh logs do not count.)
required: false, ids: [] only when no such pending AP-refresh log touches the batch.
batch_status (stale variant)
ready_to_send | needs_ap_refresh | blocked.
needs_ap_refresh when the batch was built from a stale AP export and non-closed
AP-refresh logs are pending (typical). blocked only when a candidate is hard-blocked
(e.g. unapproved claim) and nothing is eligible; otherwise prefer needs_ap_refresh if at
least one claim is eligible.
4. Task B — Vendor onboarding / KYC release board
Goal: for a list of business_ids, produce per-business decision
(approve/awaiting_information/escalate), reportable_ubo_counts,
hard_stop_flags, follow_up_business_ids, overall_release_ready.
SOP
GET /api/compliance/objects?business_id=<each> → KYC evidence.
GET /api/vendors?vendor_id=<compliance.vendor_id> → master status, tax_id, bank last4.
- Derive hard-stop flags; then decision; then summary fields.
hard_stop_flags (alphabetical within each business; [] if none)
Map each condition to its enum value:
bank_closed ← bank_account_status == "closed"
bank_name_mismatch ← bank_account_status == "name_mismatch"
confirmed_pep ← pep_status == "confirmed_pep" (possible_pep is NOT this flag)
expired_license ← license_expiry < as_of_date
missing_required_documents ← missing_fields non-empty
sanctions_confirmed ← sanctions_check_status == "confirmed" (sanctioned)
screening_not_run ← sanctions_check_status == "not_run" OR pep_status == "not_run"
shell_company_suspected ← shell_company_suspected == true
vendor_on_hold ← vendor status == "on_hold"
If none apply → [].
Decision (approve / awaiting_information / escalate)
- escalate if any
hard_stop_flag is set.
- approve if all evidence is clean: no hard stops,
bank verified, pep none,
sanctions clear, shell false, license valid, missing_fields empty, vendor active,
tax id matches master, risk below threshold. review_status=in_review alone does not
block approval — decisions are evidence-based ("suitable for release control, not just a
copy of the current review status"), so clean evidence with an in-progress review still
approves.
- awaiting_information otherwise (moderate issues without a hard stop):
bank_name_mismatch (already a hard stop, so escalates), possible_pep, pending screening,
missing docs, or review_status in not_started/awaiting_information.
(If a business has bank_name_mismatch it is in hard_stop_flags and therefore escalates;
bank_name_mismatch is treated as a hard stop in onboarding.)
reportable_ubo_counts
- Reporting threshold = 25% ownership. Count unique UBO
names with
ownership_pct >= 25. Duplicate name entries count once. (Design cue: 24% entries sit just
below the threshold and must be excluded.)
- Whole-number integer, 0 if none qualify.
follow_up_business_ids / overall_release_ready
follow_up_business_ids = all businesses whose decision ≠ approve, ascending by
business_id.
overall_release_ready = true only if every business is approve.
5. Task C — Prepaid amortization & GL reconciliation close
Goal: for a scoped set of prepaid invoice IDs, accounts, entity, and close period
(YYYY-MM), build the straight-line amortization schedule, reconcile to GL ending balances,
flag variances and data-quality exceptions, and set per-account status.
SOP
- Read scope:
entity, close_period, accounts[], selected_prepaid_invoice_ids[]
(preserve their order — outputs use "same order as scope"), variance_threshold_abs.
GET /api/prepaids/invoices?prepaid_invoice_id=<each> → schedule inputs.
- For each account,
GET /api/prepaids/gl-balances?account=<acct>&period=<close_period> →
gl_ending_balance (the period's ending_balance).
- Compute per-invoice amortization and roll up per account.
Amortization math (straight-line monthly)
- Use the stored
monthly_amortization as the per-month charge (do not recompute from
original_amount/months; the stored value is what the schedule represents).
- A month counts as a full month if it falls within
[service_start month … service_end month]; the start month counts in full even when service_start is mid-month.
march_amortization (period amortization) = monthly_amortization if the close period
month is within the service window, else 0.
cumulative_amortization_through_<period> = monthly_amortization × (number of service
months from service_start through the end of the close period).
ending_balance = original_amount − cumulative_amortization_through_period (≥ 0;
small rounding residuals such as 0.01 from monthly×n ≠ original are kept as-is — do not
true-up the final month unless a task says so).
- All currency to 2 decimals.
Account rollup (per account)
selected_invoice_count = invoices in scope for that account.
original_amount_total, march_amortization_total,
cumulative_amortization_through_march, schedule_ending_balance = sums of the per-invoice
values. (Check: schedule_ending_balance == original_amount_total − cumulative_total.)
gl_ending_balance = GL ending_balance for that account+period.
variance_amount = schedule_ending_balance − gl_ending_balance (signed:
schedule minus GL).
variance_flag = abs(variance_amount) >= variance_threshold_abs.
has_default_missing_term_flag = any invoice on the account has a default/missing-term flag.
Flags & lists
default_missing_term_flag (per invoice) = invoice data_quality_flags contains
missing_contract_dates. (rounded_amount is not a missing-term flag.)
default_missing_term_invoice_ids = invoices with that flag, ascending by invoice id.
exception_flag (per invoice) = invoice data_quality_flags is non-empty (any data
quality issue: rounded_amount, missing_contract_dates, …). "Invoice-level data quality
exceptions from the source records."
exception_invoice_ids = invoices with any data-quality flag, ascending by invoice id.
selected_invoice_ids and invoice_results keep the scope file's order (not sorted).
account_status enum mapping (priority order)
requires_reconciliation — account has_default_missing_term_flag is true (schedule is
unreliable without source remediation), regardless of variance.
variance_review — else if variance_flag is true (variance exceeds threshold, but no
missing-term issues).
reconciled — else (variance within threshold, no missing-term issues).
Ordering gotcha
Invoices prefixed PPD-2025-NNNN sort before PPD-AUR-… / PPD-xxx-… because digit
2 (0x32) < letter A (0x41). When a list says "ascending by invoice id," apply plain string
sort (so PPD-2025-0001 precedes PPD-AUR-1251-GOOD-001).
6. Task D — Payment-release risk review after account-change events
Goal: for a batch of account-change tickets (business_id, vendor_id,
requested_bank_last4, change_type, requested_release_amount_usd, priority), decide
release / hold / escalate per business and surface bank_mismatch_ids,
invalid_tax_ids, expired_license_ids, review_queue_ids, risk_score_override_flags.
as_of_date from the batch (e.g. 2025-06-01).
SOP
GET /api/compliance/objects?business_id=<each> and GET /api/vendors?vendor_id=<each>.
- Per business compute the derived lists, then the decision.
Derived lists (ascending by business_id)
bank_mismatch_ids ← compliance.bank_account_status == "name_mismatch".
invalid_tax_ids ← compliance.tax_id != vendors.tax_id (mismatch) or malformed
placeholder (e.g. TIN999999, TIN12X899 with a letter). Both a mismatch and a placeholder
qualify; a clean master-matching tax id does not.
expired_license_ids ← license_expiry < as_of_date (pure date compare; a license whose
doc is missing_fields but whose license_expiry is in the future is not expired by
this list — it is a separate missing-docs concern).
risk_score_override_flags ← risk_score >= 70 (the threshold is ">= 70"; 70 qualifies).
review_queue_ids ← businesses not released (i.e. hold or escalate) — those that
require review before release.
Decision (release / hold / escalate) — evidence-based, not a copy of review_status
- escalate if any hard block:
confirmed_pep, sanctions_check_status not_run/confirmed,
bank_account_status == "closed", shell_company_suspected, tax invalid, license
expired, vendor on_hold, or review_status == "escalated".
- hold if moderate issues without a hard block:
bank_account_status == "name_mismatch",
possible_pep, missing_fields non-empty, risk_score >= 70, risky change_type
(new_account_after_remittance_failure, reactivation_after_closed_bank_notice), or
review_status in not_started/awaiting_information.
- release if clean:
bank_account_status == "verified", requested_bank_last4 ==
vendors.bank_account_last4, license valid, sanctions clear, pep none, shell false,
tax matches master, vendor active, risk_score < 70. review_status == "in_review"
alone does not block release when all evidence is clean — decisions are evidence-based.
Bank account-change sanity check
Always compare requested_bank_last4 to vendors.bank_account_last4. A mismatch means the
requested payout account is not the on-file account → cannot release (hold/escalate). In this
environment the tickets generally match the master last4; a mismatch is a red flag.
7. Common misjudgments & exclusion rules (apply across all task types)
- Stale snapshot vs. current ERP: a circulated CSV/export is context only, never the
system of record. Always re-pull claims/bills/payments from the API and compare.
- Paid vs. payable:
processing/scheduled payments are not cleared → the bill is
still an open payable. Only status=cleared payment (and paid bill) means settled.
- Claim-vs-AP alignment: a bill
claim_id link is not enough — require amount match
(and vendor match when both exist). A prepaid-account bill with a different amount/vendor
that happens to share claim_id is not the claim's reimbursement bill.
- Void bills: a
void bill is ignored for AP balance and cannot make a claim payable or
paid.
- Multiple bills per claim: fetch all; the matching bill is the one whose amount equals
the claim amount (and matching vendor). Non-matching bills are excluded.
possible_pep is not confirmed_pep: only confirmed_pep is a hard stop. possible_pep
is a moderate/hold signal.
screening_not_run: triggers on sanctions_check_status == "not_run" or
pep_status == "not_run" (any screening channel not run).
- License expiry vs. license-document-missing:
expired_license is a pure
license_expiry < as_of_date date compare. A license listed in missing_fields but with a
future license_expiry is a missing-docs issue, not an expired-license issue. Both can
apply simultaneously when the date is also in the past.
- Tax id: compare
compliance.tax_id to vendors.tax_id. Placeholders
(TIN999999) and malformed (TIN12X899) or simple mismatches all → invalid.
- UBO threshold: 25%. 24% entries are deliberately below — exclude them. Count unique
names, not entries.
- risk_score threshold:
>= 70 for the override flag (70 inclusive). 64/57 etc. do
not qualify.
- review_status is not the decision:
review_status=approved with hard stops must
escalate; review_status=in_review with clean evidence can release/approve. Drive the
decision from evidence, not the status string. (This is the explicit "not just a copy of
the current review status" instruction.)
- Policy flags ≠ block:
over_limit/late_receipt/manual_rate/duplicate_amount on
an approved claim do not block AP release. They are case metadata.
- Variance direction:
variance_amount = schedule_ending_balance − gl_ending_balance
(schedule minus GL). Keep the sign; variance_flag uses the absolute value.
- Missing-term ≠ rounded-amount:
missing_contract_dates drives
default_missing_term_flag/requires_reconciliation; rounded_amount drives only the
generic exception_flag.
- ap/aging duplicates: aging rows can share
bill_id with the canonical bills endpoint
under a different status. Do not age-rank from /api/ap/aging unless explicitly asked; use
/api/ap/bills + /api/ap/payments.
- Ordering strings: "ascending by claim_id/business_id/invoice id" = plain string sort.
CLM-2025-00… precedes CLM-2025-FIN-…; PPD-2025-… precedes PPD-AUR-…. Numbers
inside are zero-padded so lexical sort works.
- List all keys: when a template says "keys: candidate claim/business IDs," include
every candidate in the object even if the value is 0/empty — don't omit settled/
blocked ones.
8. Output field & controlled-vocabulary reference
Task A (reimbursement-to-AP, simple)
Top-level: payable_claim_ids[], blocked_claim_ids[], paid_claim_ids[],
ap_open_balance_total (USD 2dp), crm_required_claim_ids[], batch_status,
reviewed_claim_count.
batch_status ∈ {ready_to_close, open_payables, blocked}.
- All claim-id lists ascending by claim_id.
Task A stale-snapshot variant
Top-level: eligible_claim_ids[], not_ready_claim_ids[], ap_balance_by_claim{},
stale_snapshot_corrections{}, close_log_required{required, ids[]}, batch_status.
stale_snapshot_corrections value ∈ {current_snapshot_ok, mark_in_flight_payment,
replace_with_matched_paid_bill, exclude_amount_or_vendor_mismatch, ignore_void_bill,
block_unapproved_claim}.
close_log_required.ids ascending by log id.
batch_status ∈ {ready_to_send, needs_ap_refresh, blocked}.
- All claim-id lists ascending by claim_id;
ap_balance_by_claim values USD 2dp.
Task B (onboarding/KYC)
Top-level order: per_business[], reportable_ubo_counts{}, hard_stop_flags{},
follow_up_business_ids[], overall_release_ready.
per_business[].decision ∈ {approve, awaiting_information, escalate}; list ascending
by business_id.
hard_stop_flags value ∈ {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; [] when none.
reportable_ubo_counts integer ≥ 0.
follow_up_business_ids ascending; overall_release_ready boolean.
Task C (prepaid close)
Top-level: period (YYYY-MM), entity, selected_invoice_ids[] (scope order),
account_rollup{}, invoice_results[] (scope order), default_missing_term_invoice_ids[],
exception_invoice_ids[].
- Per invoice object keys:
prepaid_invoice_id, account, march_amortization, cumulative_amortization_through_march, ending_balance, default_missing_term_flag, exception_flag (2dp numbers).
account_rollup.<acct> value fields: account_name, selected_invoice_count, original_amount_total, march_amortization_total, cumulative_amortization_through_march, schedule_ending_balance, gl_ending_balance, variance_amount, variance_flag, has_default_missing_term_flag, account_status.
account_status ∈ {reconciled, variance_review, requires_reconciliation}.
variance_amount = schedule_ending_balance − gl_ending_balance.
default_missing_term_invoice_ids / exception_invoice_ids ascending by invoice id.
Task D (payment-release risk)
Top-level: task_id, batch_id, as_of_date (YYYY-MM-DD), target_business_ids[],
decisions{}, bank_mismatch_ids[], invalid_tax_ids[], expired_license_ids[],
review_queue_ids[], risk_score_override_flags[].
decisions value ∈ {release, hold, escalate}.
task_id, batch_id, as_of_date, target_business_ids are fixed by the batch payload.
- All id lists ascending by business_id.
9. Currency / precision conventions & the "USD cents" trap
- Default: USD, 2 decimals (dollars), per
environment_access.md and the templates'
precision:2, unit:USD. This applies to AP balances, prepaid totals, variance, etc.
- The trap: some prompts say "Use USD cents for currency totals" while the matching
template field still declares
precision:2, unit:USD. The template field schema is the
machine-validated output contract, so emit dollars with 2 decimals (e.g. 1842.36) into
a precision:2, unit:USD field — do not emit bare integer cents. Treat "USD cents" as
"cent-level precision (2 decimal dollars)." Always confirm against the template's
precision/unit for each numeric field rather than the prompt wording alone.
- Variance/amortization math: compute in full precision, round only the final reported values
to 2 decimals.
10. Concrete SOPs (quick reference)
Reconciliation close (A)
- Pull claims (status, amount, vendor).
- Pull bills by
claim_id; for each pull payments by bill_id.
- Classify: paid (paid bill + cleared payment = amount) / payable (approved + open matching
bill) / blocked (no valid matching bill or unapproved).
ap_open_balance_total = sum over payable of (bill.amount − cleared payments).
batch_status = blocked if any blocked else open_payables if any payable else ready_to_close.
Stale-snapshot reconciliation (A variant)
- Same pulls as above; diff against the CSV snapshot.
- Assign one correction enum per candidate (see §3). Payable+clean snapshot →
current_snapshot_ok.
ap_balance_by_claim for every candidate (0 for paid/blocked/mismatched/void).
close_log_required: collect non-closed Waiting on AP export refresh close logs whose
related_account is among the batch's bill accounts; required=true if any.
batch_status: needs_ap_refresh if a stale refresh is pending and ≥1 eligible;
blocked if hard-blocked with nothing eligible; ready_to_send if all eligible and
current (rare).
Onboarding / payment release (B / D)
- Pull compliance + vendor per business.
- Build hard-stop/derived lists (§4/§6).
- Decision = escalate (any hard block) → hold (moderate) → release/approve (clean). Evidence
drives it;
review_status=in_review does not override clean evidence; review_status= approved does not override hard blocks.
- Summary lists ascending;
overall_release_ready/review_queue per rules.
Prepaid close (C)
- Pull scoped invoices + GL balances for each account@period.
- Per invoice:
march_amortization (monthly if period in window else 0),
cumulative = monthly × months service_start→end-of-period,
ending_balance = original − cumulative.
- Roll up per account (sums), then
variance_amount = schedule − gl,
variance_flag = |variance| >= threshold.
- Flags:
default_missing_term_flag ← missing_contract_dates; exception_flag ← any
data_quality_flag.
account_status: missing-term → requires_reconciliation; else variance_flag →
variance_review; else reconciled.
- Preserve scope order for
selected_invoice_ids/invoice_results; sort the two id lists
ascending.
Payment-priority ranking (general, when a task asks to rank payable bills)
- Candidates: open bills (
balance > 0, payable status approved/scheduled) from
/api/ap/bills, excluding void/draft and fully-paid.
- Rank by
due_date ascending, then bill_id ascending. Apply cleared payments first to
derive each bill's open balance.
1---2name: self-attempt-03-93description: SKILL — task_group_005 ERP Finance Expense-Control Close4---5# SKILL — task_group_005 ERP Finance Expense-Control Close67Transferable, executable experience for solving ERP finance expense-control tasks in the8`task_group_005` shared API environment. Covers four task archetypes that recur across9train/test pairs:1011- **A. Reimbursement-to-AP close review** (claim → AP bill → payment reconciliation; payable/blocked/paid partition; stale AP-snapshot correction).12- **B. Vendor onboarding / KYC release board** (compliance hard-stop flags, UBO reporting, release decisions).13- **C. Prepaid amortization & GL reconciliation close** (straight-line monthly schedule vs. GL ending balance, variance + data-quality flags).14- **D. Payment-release risk review after vendor account-change events** (compliance + vendor + bank/tax/license/risk gating, release/hold/escalate).1516The rules below are derived from working the 5 train tasks against the live API. They are17**rules and decision criteria**, not a lookup of any answer set.1819---2021## 1. API contract2223### Base URL & discovery24- Remote API base: `<remote-env-url>` (a prompt may print `http://127.0.0.1:8005`; the25 remote URL above is the shared system of record — use it).26- `GET /endpoints` → lists all paths and the filtering contract.27- `GET /health` and `GET /api/health` → `{status, task_group}`.28- **Currency convention:** report all amounts to **2 decimals in USD** unless a task says29 otherwise (see §9 for the "USD cents" trap).3031### Endpoints (canonical + alias)32Every resource has a namespaced `/api/...` path and, for most, a bare alias. Both return33identical data. Use the `/api/...` form.3435| Resource | Canonical path | Alias |36|---|---|---|37| Claims | `/api/claims` | `/claims` |38| AP bills | `/api/ap/bills` | `/bills` |39| AP payments | `/api/ap/payments` | `/payments` |40| AP aging | `/api/ap/aging` | — |41| Vendors | `/api/vendors` | `/vendors` |42| Compliance (KYC) objects | `/api/compliance/objects` | `/compliance/objects` |43| Prepaid invoices | `/api/prepaids/invoices` | `/prepaids/invoices` |44| GL balances (general) | `/api/prepaids/gl-balances` | `/gl/balances` |45| Close logs | `/api/close/logs` | `/close/logs` |4647### Filtering contract48- **Exact-match query parameters by field name.** Examples: `?claim_id=CLM-2025-0038`,49 `?business_id=BUS-2025-0009`, `?vendor_id=VEN-0064`, `?account=1250`, `?period=2025-03`,50 `?status=blocked`.51- Paginate with `limit` (default 100) and `offset`. **Always pass `limit`** when you need the52 full set; default page may be smaller than `total`.53- A list by a unique field returns `{count, data:[...], total, limit, offset}`. Filter by a54 join key that can repeat (e.g. `?claim_id=`) returns **all** matching rows — use this to55 pull every bill/payment for one claim.56- Path-style single fetch (`/api/claims/<id>`) works for claims but **not** for bills57 (`/api/ap/bills/<id>` returns `not_found`). Prefer the query-param form.5859---6061## 2. Data model — fields that matter6263### Claims `/api/claims`64`claim_id, status, amount, currency, submitted_date, approved_date, category, department,65employee_name, receipt_status, policy_flags[], notes, vendor_id`.66- `status` values seen: `needs_receipt`, `approved`, `paid` (also `rejected`/`draft` possible).67- `policy_flags` values seen: `manual_rate`, `duplicate_amount`, `over_limit`, `late_receipt`.68 **Policy flags do not by themselves block AP release** once the claim is `approved`.69- `receipt_status`: `attached` vs `partial`.70- `vendor_id` may be `null` (employee reimbursement) — do not treat null vendor as a mismatch71 against a bill vendor; use amount alignment instead.7273### AP bills `/api/ap/bills`74`bill_id, claim_id, vendor_id, account, amount, currency, bill_date, due_date, invoice_number,75memo, status`.76- `status` values: `draft`, `approved`, `scheduled`, `paid`, `void`.77- `account` is the GL account hit (e.g. `6200` expense, `2100` AP clearing, `1250/1251` prepaid,78 `6500` T&E). A bill linked to a reimbursement claim but on a **prepaid account** with a79 **non-matching amount/vendor** is not that claim's reimbursement bill.80- A single `claim_id` can map to **multiple** bills (e.g. a prepaid bill + the real paid81 reimbursement bill). Always fetch all bills for the claim and pick the one matching claim82 amount (+ vendor).8384### AP payments `/api/ap/payments`85`payment_id, bill_id, vendor_id, amount, status, method, payment_date, bank_reference`.86- `status` values: `none`(implicit), `processing`, `scheduled`, `cleared`, `failed`.87- **Paid = bill `status=paid` AND a payment with `status=cleared` for the bill/claim amount.**88- `processing` / `scheduled` payments are **in-flight, not cleared** → the bill is still an89 open payable.9091### AP aging `/api/ap/aging`92`bill_id, claim_id, vendor_id, amount, paid_amount, balance, bill_date, due_date, status, as_of`.93- Used for payment-priority ranking. **Note:** aging rows can overlap/duplicate canonical94 bills with the same `bill_id` but different status — do not age-rank from here unless the95 task explicitly asks for aging; rank from `/api/ap/bills` + `/api/ap/payments` instead.9697### Vendors `/api/vendors`98`vendor_id, vendor_name, legal_name, status, tax_id, bank_account_last4, default_account,99industry, payment_terms, updated_at`.100- `status`: `active`, `on_hold`, (others possible). `on_hold` is a release hard-stop.101- `tax_id` here is the **vendor-master** tax id — compare against `compliance.tax_id` to find102 corrupted/placeholder tax ids.103- `bank_account_last4` — compare against the account-change ticket's `requested_bank_last4`.104105### Compliance objects `/api/compliance/objects`106`business_id, business_name, vendor_id, jurisdiction, registration_number, tax_id,107bank_account_status, license_expiry, missing_fields[], ownership_layer_count, ubo_list[],108pep_status, sanctions_check_status, shell_company_suspected, review_status, risk_score`.109- `bank_account_status`: `verified`, `name_mismatch`, `closed`.110- `pep_status`: `none`, `possible_pep`, `confirmed_pep`, `not_run`.111- `sanctions_check_status`: `clear`, `not_run`, (`confirmed`).112- `review_status`: `not_started`, `in_review`, `awaiting_information`, `approved`, `escalated`.113- `risk_score`: integer 0–100.114- `ubo_list[]`: `{name, ownership_pct}`.115- `missing_fields[]`: document names e.g. `license`, `beneficial_owner_id`.116117### Prepaid invoices `/api/prepaids/invoices`118`prepaid_invoice_id, account, vendor_id, invoice_number, invoice_date, service_start,119service_end, original_amount, monthly_amortization, recognition_method, description,120source_document, data_quality_flags[]`.121- `recognition_method`: `straight_line`.122- `data_quality_flags` values seen: `rounded_amount`, `missing_contract_dates`.123124### GL balances `/api/prepaids/gl-balances` (= `/gl/balances`)125`account, account_name, entity, period (YYYY-MM), ending_balance, source, loaded_at`.126- Multiple periods per account; filter by `account` and `period`. Entity = legal entity string.127128### Close logs `/api/close/logs`129`log_id, area, period, related_account, message, owner, status, created_at`.130- `area`: `AP`, `Expense`, `Prepaids`, `Treasury`, `GL`, `Compliance`.131- `status`: `closed`, `ready_for_review`, `open`, `blocked`.132- `message` patterns: `Waiting on AP export refresh`, `Variance review pending`,133 `Reviewer cleared variance`, `Manual journal entry posted`, `Support uploaded`,134 `Legacy import created duplicate line`.135136---137138## 3. Task A — Reimbursement-to-AP close review139140**Goal:** partition a list of candidate claim IDs into payable / blocked / paid, compute the141open AP balance, and set an overall batch status. Variant with a stale CSV snapshot also142classifies a per-claim snapshot correction.143144### SOP — query order1451. `GET /api/claims?claim_id=<each>` → claim status, amount, vendor_id, policy_flags.1462. `GET /api/ap/bills?claim_id=<each>` → every bill linked to the claim.1473. For each bill, `GET /api/ap/payments?bill_id=<each bill_id>` → payment evidence.1484. Reconcile per claim (rules below). Sum open AP for payable claims only.149150### Claim classification rules151- **paid:** claim has an AP bill whose `status=paid` AND whose `amount` equals the claim152 amount AND a payment with `status=cleared` for that amount. (Claim `status=paid` is a hint;153 the paid bill + cleared payment is the proof.)154- **payable (eligible):** claim `status=approved` AND there is an **open** AP bill155 (`status` in `approved`/`scheduled` — i.e. not paid, not void, not draft-blocking) whose156 `amount` equals the claim amount and (when claim has a vendor) vendor matches. In-flight157 payments (`processing`/`scheduled`) **do not** make it paid — the bill is still an open158 payable.159- **blocked:** not paid AND not payable, because the AP link is wrong/missing. Reasons:160 - no bill at all for the claim;161 - the only linked bill is `void`;162 - the linked bill's `amount` ≠ claim amount, or (when both have a vendor) vendor mismatch163 (these are AP-link problems); or164 - the claim itself is unapproved (`needs_receipt`/`rejected`) — blocked until the expense165 case is fixed.166- Policy flags (`over_limit`, `late_receipt`, `manual_rate`, `duplicate_amount`) and167 `partial` receipts do **not** auto-block an *approved* claim; they are case-side metadata.168 They only matter when the claim is not approved, or to explain a separate case-cleanup need.169170### crm_required vs blocked171`crm_required_claim_ids` = blocked claims that need **expense-case owner cleanup OR AP-link172remediation**. In practice this equals the blocked set (every blocked claim needs one or the173other). Do not put payable or paid claims here.174175### ap_open_balance_total176Sum of open AP balances for **payable claims only**. Open AP balance per claim =177`bill.amount` − (sum of `cleared` payments on that bill). In-flight payments are not178subtracted. Void/mismatched bills contribute 0 (excluded).179180### batch_status (A, simple template)181`blocked` if any claim is blocked; else `open_payables` if any payable (valid unpaid AP182remains); else `ready_to_close`.183184### Stale-snapshot variant (separate template) — `stale_snapshot_corrections`185Compare the circulated CSV snapshot to the current API. Per candidate claim pick **one** enum:186- `current_snapshot_ok` — snapshot matches current ERP (bill id/amount/status, payment) and187 claim is an open payable → keep as-is.188- `mark_in_flight_payment` — snapshot shows payment `none/0.00` but current ERP has a189 `processing`/`scheduled` (uncleared) payment on the bill.190- `replace_with_matched_paid_bill` — snapshot points to a wrong/scheduled bill (often a191 non-matching prepaid bill) but the real matched bill is `paid` + cleared for the claim192 amount; replace the snapshot row with that matched paid bill (claim is settled, not in the193 open batch).194- `exclude_amount_or_vendor_mismatch` — the snapshot/current bill `amount` ≠ claim amount or195 vendor mismatches; exclude that AP row from the claim's balance.196- `ignore_void_bill` — snapshot shows the bill as `approved` but current ERP `status=void`;197 ignore it (contributes 0 to AP balance).198- `block_unapproved_claim` — the claim is not approved (`needs_receipt`/`rejected`); block it199 regardless of bill state.200201### ap_balance_by_claim (stale variant)202Keys = **all candidate claim IDs**. Value = open AP balance after applying `cleared` payments203and **ignoring stale/voided/mismatched** rows. Paid claims and blocked/mismatched/void claims204= `0.00`.205206### not_ready_claim_ids (stale variant)207Claims that should not remain in the batch: paid (settled), unapproved, void-bill-only, or208amount/vendor-mismatched. (A paid claim is "not ready" for the *open* batch because it is209already settled.)210211### close_log_required (stale variant)212- `required: true` when non-closed close logs whose `message` is `Waiting on AP export refresh`213 exist for accounts touched by the batch's bills.214- `ids` = those non-closed (`ready_for_review`/`open`/`blocked`) `Waiting on AP export refresh`215 log IDs, ascending. Filter to `related_account` ∈ the set of bill accounts in the candidate216 batch. (Closed `Waiting on AP export refresh` logs do **not** count.)217- `required: false`, `ids: []` only when no such pending AP-refresh log touches the batch.218219### batch_status (stale variant)220`ready_to_send` | `needs_ap_refresh` | `blocked`.221- `needs_ap_refresh` when the batch was built from a stale AP export and non-closed222 AP-refresh logs are pending (typical). `blocked` only when a candidate is hard-blocked223 (e.g. unapproved claim) and nothing is eligible; otherwise prefer `needs_ap_refresh` if at224 least one claim is eligible.225226---227228## 4. Task B — Vendor onboarding / KYC release board229230**Goal:** for a list of `business_id`s, produce per-business `decision`231(`approve`/`awaiting_information`/`escalate`), `reportable_ubo_counts`,232`hard_stop_flags`, `follow_up_business_ids`, `overall_release_ready`.233234### SOP2351. `GET /api/compliance/objects?business_id=<each>` → KYC evidence.2362. `GET /api/vendors?vendor_id=<compliance.vendor_id>` → master status, tax_id, bank last4.2373. Derive hard-stop flags; then decision; then summary fields.238239### hard_stop_flags (alphabetical within each business; `[]` if none)240Map each condition to its enum value:241- `bank_closed` ← `bank_account_status == "closed"`242- `bank_name_mismatch` ← `bank_account_status == "name_mismatch"`243- `confirmed_pep` ← `pep_status == "confirmed_pep"` *(possible_pep is NOT this flag)*244- `expired_license` ← `license_expiry < as_of_date`245- `missing_required_documents` ← `missing_fields` non-empty246- `sanctions_confirmed` ← `sanctions_check_status == "confirmed"` (sanctioned)247- `screening_not_run` ← `sanctions_check_status == "not_run"` **OR** `pep_status == "not_run"`248- `shell_company_suspected` ← `shell_company_suspected == true`249- `vendor_on_hold` ← vendor `status == "on_hold"`250If none apply → `[]`.251252### Decision (approve / awaiting_information / escalate)253- **escalate** if any `hard_stop_flag` is set.254- **approve** if **all** evidence is clean: no hard stops, `bank verified`, `pep` `none`,255 `sanctions` `clear`, `shell` false, license valid, `missing_fields` empty, vendor `active`,256 tax id matches master, risk below threshold. **`review_status=in_review` alone does not257 block approval** — decisions are evidence-based ("suitable for release control, not just a258 copy of the current review status"), so clean evidence with an in-progress review still259 approves.260- **awaiting_information** otherwise (moderate issues without a hard stop):261 `bank_name_mismatch` (already a hard stop, so escalates), `possible_pep`, pending screening,262 missing docs, or `review_status` in `not_started`/`awaiting_information`.263264(If a business has `bank_name_mismatch` it is in `hard_stop_flags` and therefore escalates;265`bank_name_mismatch` is treated as a hard stop in onboarding.)266267### reportable_ubo_counts268- **Reporting threshold = 25% ownership.** Count **unique UBO `name`s** with269 `ownership_pct >= 25`. Duplicate name entries count once. (Design cue: 24% entries sit just270 below the threshold and must be excluded.)271- Whole-number integer, 0 if none qualify.272273### follow_up_business_ids / overall_release_ready274- `follow_up_business_ids` = all businesses whose decision ≠ `approve`, ascending by275 `business_id`.276- `overall_release_ready` = `true` only if **every** business is `approve`.277278---279280## 5. Task C — Prepaid amortization & GL reconciliation close281282**Goal:** for a scoped set of prepaid invoice IDs, accounts, entity, and close period283(`YYYY-MM`), build the straight-line amortization schedule, reconcile to GL ending balances,284flag variances and data-quality exceptions, and set per-account status.285286### SOP2871. Read scope: `entity`, `close_period`, `accounts[]`, `selected_prepaid_invoice_ids[]`288 (preserve their order — outputs use "same order as scope"), `variance_threshold_abs`.2892. `GET /api/prepaids/invoices?prepaid_invoice_id=<each>` → schedule inputs.2903. For each account, `GET /api/prepaids/gl-balances?account=<acct>&period=<close_period>` →291 `gl_ending_balance` (the period's `ending_balance`).2924. Compute per-invoice amortization and roll up per account.293294### Amortization math (straight-line monthly)295- Use the stored `monthly_amortization` as the per-month charge (do not recompute from296 `original_amount`/months; the stored value is what the schedule represents).297- A month counts as a full month if it falls within `[service_start month … service_end298 month]`; the start month counts in full even when `service_start` is mid-month.299- `march_amortization` (period amortization) = `monthly_amortization` if the close period300 month is within the service window, else `0`.301- `cumulative_amortization_through_<period>` = `monthly_amortization` × (number of service302 months from `service_start` through the end of the close period).303- `ending_balance` = `original_amount − cumulative_amortization_through_period` (≥ 0;304 small rounding residuals such as 0.01 from `monthly×n` ≠ `original` are kept as-is — do not305 true-up the final month unless a task says so).306- All currency to 2 decimals.307308### Account rollup (per account)309- `selected_invoice_count` = invoices in scope for that account.310- `original_amount_total`, `march_amortization_total`,311 `cumulative_amortization_through_march`, `schedule_ending_balance` = sums of the per-invoice312 values. (Check: `schedule_ending_balance == original_amount_total −313 cumulative_total`.)314- `gl_ending_balance` = GL `ending_balance` for that `account`+`period`.315- `variance_amount` = `schedule_ending_balance − gl_ending_balance` (**signed**:316 schedule minus GL).317- `variance_flag` = `abs(variance_amount) >= variance_threshold_abs`.318- `has_default_missing_term_flag` = any invoice on the account has a default/missing-term flag.319320### Flags & lists321- `default_missing_term_flag` (per invoice) = invoice `data_quality_flags` contains322 `missing_contract_dates`. (`rounded_amount` is **not** a missing-term flag.)323- `default_missing_term_invoice_ids` = invoices with that flag, ascending by invoice id.324- `exception_flag` (per invoice) = invoice `data_quality_flags` is **non-empty** (any data325 quality issue: `rounded_amount`, `missing_contract_dates`, …). "Invoice-level data quality326 exceptions from the source records."327- `exception_invoice_ids` = invoices with any data-quality flag, ascending by invoice id.328- `selected_invoice_ids` and `invoice_results` keep the **scope file's order** (not sorted).329330### account_status enum mapping (priority order)3311. `requires_reconciliation` — account `has_default_missing_term_flag` is true (schedule is332 unreliable without source remediation), regardless of variance.3332. `variance_review` — else if `variance_flag` is true (variance exceeds threshold, but no334 missing-term issues).3353. `reconciled` — else (variance within threshold, no missing-term issues).336337### Ordering gotcha338Invoices prefixed `PPD-2025-NNNN` sort **before** `PPD-AUR-…` / `PPD-xxx-…` because digit339`2` (0x32) < letter `A` (0x41). When a list says "ascending by invoice id," apply plain string340sort (so `PPD-2025-0001` precedes `PPD-AUR-1251-GOOD-001`).341342---343344## 6. Task D — Payment-release risk review after account-change events345346**Goal:** for a batch of account-change tickets (`business_id`, `vendor_id`,347`requested_bank_last4`, `change_type`, `requested_release_amount_usd`, `priority`), decide348`release` / `hold` / `escalate` per business and surface `bank_mismatch_ids`,349`invalid_tax_ids`, `expired_license_ids`, `review_queue_ids`, `risk_score_override_flags`.350`as_of_date` from the batch (e.g. `2025-06-01`).351352### SOP3531. `GET /api/compliance/objects?business_id=<each>` and `GET /api/vendors?vendor_id=<each>`.3542. Per business compute the derived lists, then the decision.355356### Derived lists (ascending by business_id)357- `bank_mismatch_ids` ← `compliance.bank_account_status == "name_mismatch"`.358- `invalid_tax_ids` ← `compliance.tax_id != vendors.tax_id` (mismatch) **or** malformed359 placeholder (e.g. `TIN999999`, `TIN12X899` with a letter). Both a mismatch and a placeholder360 qualify; a clean master-matching tax id does not.361- `expired_license_ids` ← `license_expiry < as_of_date` (pure date compare; a license whose362 doc is `missing_fields` but whose `license_expiry` is in the future is **not** expired by363 this list — it is a separate missing-docs concern).364- `risk_score_override_flags` ← `risk_score >= 70` (the threshold is ">= 70"; 70 qualifies).365- `review_queue_ids` ← businesses not `release`d (i.e. `hold` or `escalate`) — those that366 require review before release.367368### Decision (release / hold / escalate) — evidence-based, not a copy of review_status369- **escalate** if any hard block: `confirmed_pep`, `sanctions_check_status` `not_run`/`confirmed`,370 `bank_account_status == "closed"`, `shell_company_suspected`, **tax invalid**, **license371 expired**, vendor `on_hold`, or `review_status == "escalated"`.372- **hold** if moderate issues without a hard block: `bank_account_status == "name_mismatch"`,373 `possible_pep`, `missing_fields` non-empty, `risk_score >= 70`, risky `change_type`374 (`new_account_after_remittance_failure`, `reactivation_after_closed_bank_notice`), or375 `review_status` in `not_started`/`awaiting_information`.376- **release** if clean: `bank_account_status == "verified"`, `requested_bank_last4` ==377 `vendors.bank_account_last4`, license valid, `sanctions` clear, `pep` `none`, `shell` false,378 tax matches master, vendor `active`, `risk_score < 70`. **`review_status == "in_review"`379 alone does not block release** when all evidence is clean — decisions are evidence-based.380381### Bank account-change sanity check382Always compare `requested_bank_last4` to `vendors.bank_account_last4`. A mismatch means the383requested payout account is not the on-file account → cannot release (hold/escalate). In this384environment the tickets generally match the master last4; a mismatch is a red flag.385386---387388## 7. Common misjudgments & exclusion rules (apply across all task types)3893901. **Stale snapshot vs. current ERP:** a circulated CSV/export is **context only**, never the391 system of record. Always re-pull claims/bills/payments from the API and compare.3922. **Paid vs. payable:** `processing`/`scheduled` payments are **not** cleared → the bill is393 still an open payable. Only `status=cleared` payment (and `paid` bill) means settled.3943. **Claim-vs-AP alignment:** a bill `claim_id` link is not enough — require **amount match**395 (and vendor match when both exist). A prepaid-account bill with a different amount/vendor396 that happens to share `claim_id` is **not** the claim's reimbursement bill.3974. **Void bills:** a `void` bill is ignored for AP balance and cannot make a claim payable or398 paid.3995. **Multiple bills per claim:** fetch all; the matching bill is the one whose amount equals400 the claim amount (and matching vendor). Non-matching bills are excluded.4016. **`possible_pep` is not `confirmed_pep`:** only `confirmed_pep` is a hard stop. `possible_pep`402 is a moderate/hold signal.4037. **`screening_not_run`:** triggers on `sanctions_check_status == "not_run"` **or**404 `pep_status == "not_run"` (any screening channel not run).4058. **License expiry vs. license-document-missing:** `expired_license` is a pure406 `license_expiry < as_of_date` date compare. A license listed in `missing_fields` but with a407 future `license_expiry` is a missing-docs issue, not an expired-license issue. Both can408 apply simultaneously when the date is also in the past.4099. **Tax id:** compare `compliance.tax_id` to `vendors.tax_id`. Placeholders410 (`TIN999999`) and malformed (`TIN12X899`) or simple mismatches all → invalid.41110. **UBO threshold:** 25%. 24% entries are deliberately below — exclude them. Count unique412 names, not entries.41311. **risk_score threshold:** `>= 70` for the override flag (70 inclusive). `64`/`57` etc. do414 **not** qualify.41512. **review_status is not the decision:** `review_status=approved` with hard stops must416 escalate; `review_status=in_review` with clean evidence can release/approve. Drive the417 decision from evidence, not the status string. (This is the explicit "not just a copy of418 the current review status" instruction.)41913. **Policy flags ≠ block:** `over_limit`/`late_receipt`/`manual_rate`/`duplicate_amount` on420 an `approved` claim do not block AP release. They are case metadata.42114. **Variance direction:** `variance_amount = schedule_ending_balance − gl_ending_balance`422 (schedule minus GL). Keep the sign; `variance_flag` uses the absolute value.42315. **Missing-term ≠ rounded-amount:** `missing_contract_dates` drives424 `default_missing_term_flag`/`requires_reconciliation`; `rounded_amount` drives only the425 generic `exception_flag`.42616. **ap/aging duplicates:** aging rows can share `bill_id` with the canonical bills endpoint427 under a different status. Do not age-rank from `/api/ap/aging` unless explicitly asked; use428 `/api/ap/bills` + `/api/ap/payments`.42917. **Ordering strings:** "ascending by claim_id/business_id/invoice id" = plain string sort.430 `CLM-2025-00…` precedes `CLM-2025-FIN-…`; `PPD-2025-…` precedes `PPD-AUR-…`. Numbers431 inside are zero-padded so lexical sort works.43218. **List all keys:** when a template says "keys: candidate claim/business IDs," include433 **every** candidate in the object even if the value is 0/empty — don't omit settled/434 blocked ones.435436---437438## 8. Output field & controlled-vocabulary reference439440### Task A (reimbursement-to-AP, simple)441Top-level: `payable_claim_ids[]`, `blocked_claim_ids[]`, `paid_claim_ids[]`,442`ap_open_balance_total` (USD 2dp), `crm_required_claim_ids[]`, `batch_status`, 443`reviewed_claim_count`.444- `batch_status` ∈ {`ready_to_close`, `open_payables`, `blocked`}.445- All claim-id lists ascending by claim_id.446447### Task A stale-snapshot variant448Top-level: `eligible_claim_ids[]`, `not_ready_claim_ids[]`, `ap_balance_by_claim{}`,449`stale_snapshot_corrections{}`, `close_log_required{required, ids[]}`, `batch_status`.450- `stale_snapshot_corrections` value ∈ {`current_snapshot_ok`, `mark_in_flight_payment`,451 `replace_with_matched_paid_bill`, `exclude_amount_or_vendor_mismatch`, `ignore_void_bill`,452 `block_unapproved_claim`}.453- `close_log_required.ids` ascending by log id.454- `batch_status` ∈ {`ready_to_send`, `needs_ap_refresh`, `blocked`}.455- All claim-id lists ascending by claim_id; `ap_balance_by_claim` values USD 2dp.456457### Task B (onboarding/KYC)458Top-level order: `per_business[]`, `reportable_ubo_counts{}`, `hard_stop_flags{}`,459`follow_up_business_ids[]`, `overall_release_ready`.460- `per_business[].decision` ∈ {`approve`, `awaiting_information`, `escalate`}; list ascending461 by `business_id`.462- `hard_stop_flags` value ∈ {`bank_closed`, `bank_name_mismatch`, `confirmed_pep`,463 `expired_license`, `missing_required_documents`, `sanctions_confirmed`,464 `screening_not_run`, `shell_company_suspected`, `vendor_on_hold`} — alphabetical within each465 list; `[]` when none.466- `reportable_ubo_counts` integer ≥ 0.467- `follow_up_business_ids` ascending; `overall_release_ready` boolean.468469### Task C (prepaid close)470Top-level: `period` (YYYY-MM), `entity`, `selected_invoice_ids[]` (scope order),471`account_rollup{}`, `invoice_results[]` (scope order), `default_missing_term_invoice_ids[]`,472`exception_invoice_ids[]`.473- Per invoice object keys: `prepaid_invoice_id, account, march_amortization,474 cumulative_amortization_through_march, ending_balance, default_missing_term_flag,475 exception_flag` (2dp numbers).476- `account_rollup.<acct>` value fields: `account_name, selected_invoice_count,477 original_amount_total, march_amortization_total, cumulative_amortization_through_march,478 schedule_ending_balance, gl_ending_balance, variance_amount, variance_flag,479 has_default_missing_term_flag, account_status`.480- `account_status` ∈ {`reconciled`, `variance_review`, `requires_reconciliation`}.481- `variance_amount = schedule_ending_balance − gl_ending_balance`.482- `default_missing_term_invoice_ids` / `exception_invoice_ids` ascending by invoice id.483484### Task D (payment-release risk)485Top-level: `task_id`, `batch_id`, `as_of_date (YYYY-MM-DD)`, `target_business_ids[]`,486`decisions{}`, `bank_mismatch_ids[]`, `invalid_tax_ids[]`, `expired_license_ids[]`,487`review_queue_ids[]`, `risk_score_override_flags[]`.488- `decisions` value ∈ {`release`, `hold`, `escalate`}.489- `task_id`, `batch_id`, `as_of_date`, `target_business_ids` are fixed by the batch payload.490- All id lists ascending by business_id.491492---493494## 9. Currency / precision conventions & the "USD cents" trap495496- **Default:** USD, 2 decimals (dollars), per `environment_access.md` and the templates'497 `precision:2, unit:USD`. This applies to AP balances, prepaid totals, variance, etc.498- **The trap:** some prompts say "Use USD cents for currency totals" while the matching499 template field still declares `precision:2, unit:USD`. The template field schema is the500 machine-validated output contract, so emit **dollars with 2 decimals** (e.g. `1842.36`) into501 a `precision:2, unit:USD` field — do **not** emit bare integer cents. Treat "USD cents" as502 "cent-level precision (2 decimal dollars)." Always confirm against the template's503 `precision`/`unit` for each numeric field rather than the prompt wording alone.504- Variance/amortization math: compute in full precision, round only the final reported values505 to 2 decimals.506507---508509## 10. Concrete SOPs (quick reference)510511### Reconciliation close (A)5121. Pull claims (status, amount, vendor).5132. Pull bills by `claim_id`; for each pull payments by `bill_id`.5143. Classify: paid (paid bill + cleared payment = amount) / payable (approved + open matching515 bill) / blocked (no valid matching bill or unapproved).5164. `ap_open_balance_total` = sum over payable of (bill.amount − cleared payments).5175. `batch_status` = blocked if any blocked else open_payables if any payable else ready_to_close.518519### Stale-snapshot reconciliation (A variant)5201. Same pulls as above; diff against the CSV snapshot.5212. Assign one correction enum per candidate (see §3). Payable+clean snapshot →522 `current_snapshot_ok`.5233. `ap_balance_by_claim` for every candidate (0 for paid/blocked/mismatched/void).5244. `close_log_required`: collect non-closed `Waiting on AP export refresh` close logs whose525 `related_account` is among the batch's bill accounts; required=true if any.5265. `batch_status`: `needs_ap_refresh` if a stale refresh is pending and ≥1 eligible;527 `blocked` if hard-blocked with nothing eligible; `ready_to_send` if all eligible and528 current (rare).529530### Onboarding / payment release (B / D)5311. Pull compliance + vendor per business.5322. Build hard-stop/derived lists (§4/§6).5333. Decision = escalate (any hard block) → hold (moderate) → release/approve (clean). Evidence534 drives it; `review_status=in_review` does not override clean evidence; `review_status=535 approved` does not override hard blocks.5364. Summary lists ascending; `overall_release_ready`/`review_queue` per rules.537538### Prepaid close (C)5391. Pull scoped invoices + GL balances for each account@period.5402. Per invoice: `march_amortization` (monthly if period in window else 0),541 `cumulative = monthly × months service_start→end-of-period`,542 `ending_balance = original − cumulative`.5433. Roll up per account (sums), then `variance_amount = schedule − gl`,544 `variance_flag = |variance| >= threshold`.5454. Flags: `default_missing_term_flag` ← `missing_contract_dates`; `exception_flag` ← any546 data_quality_flag.5475. `account_status`: missing-term → `requires_reconciliation`; else variance_flag →548 `variance_review`; else `reconciled`.5496. Preserve scope order for `selected_invoice_ids`/`invoice_results`; sort the two id lists550 ascending.551552### Payment-priority ranking (general, when a task asks to rank payable bills)553- Candidates: open bills (`balance > 0`, payable status `approved`/`scheduled`) from554 `/api/ap/bills`, excluding `void`/`draft` and fully-paid.555- **Rank by `due_date` ascending, then `bill_id` ascending.** Apply cleared payments first to556 derive each bill's open balance.