ERP Finance Close & Release Tasks
You are answering finance-operations tasks against a shared ERP JSON API. Each task gives you
a small batch (claim IDs or business IDs), a local payload, and an answer_template.json. Your
job is to fetch the live records, apply the correct business rules, and emit one JSON object
matching the template exactly. Graders check exact enum values, list membership, ascending
sorting, and 2-decimal rounding — so precision matters more than prose.
0. Golden rules (apply to every task)
- The live API is the system of record. Local payloads (CSV snapshots, batch JSON, "current
review status") are context only. If the prompt cites a base URL that differs from the one in
your environment access notes, ignore the prompt's URL and use the environment's. Never read
server source or local env data files; only HTTP GET the API.
- Pagination: list endpoints return
{count,total,offset,limit,data:[...]}. Read data.
Always pass limit=500 (the max) and page with offset until you have all total rows.
Default limit is only 100, so a naive call silently truncates.
- Money: round every currency value to 2 decimals. Some templates say "USD cents" (integers);
read the template's
unit/precision and match it (cents = round(dollars*100) as integer).
- Sorting: every list of IDs is sorted ascending by ID string unless the template says
otherwise (e.g. prepaid
selected_invoice_ids keep the scope order). Object keys for ID-keyed
maps are also typically ascending.
- Output only the JSON object the template defines, with exactly its keys. If the template
lists
required_top_level_keys or top_level_order, honor it and add nothing extra.
- Identify the task type from the template's keys, then follow the matching SOP below. The six
recognizable shapes:
payable_claim_ids/paid_claim_ids/blocked_claim_ids/batch_status → §2 reimbursement close
per_business/reportable_ubo_counts/hard_stop_flags/overall_release_ready → §3 onboarding
account_rollup/invoice_results/march_amortization... → §4 prepaid close
eligible_claim_ids/stale_snapshot_corrections/close_log_required → §5 stale AP snapshot
decisions(release/hold/escalate)/bank_mismatch_ids/risk_score_override_flags → §6 AP release
1. The API surface (base URL from environment access notes)
| Endpoint |
Holds |
Key fields |
/api/claims?limit=500 or /api/claims/{id} |
expense claims |
claim_id, status, amount, vendor_id, receipt_status, policy_flags, approved_date |
/api/ap/bills?limit=500 |
AP bills |
bill_id, claim_id, amount, vendor_id, status, account, due_date |
/api/ap/payments?limit=500 |
payments |
payment_id, bill_id, amount, status, vendor_id, payment_date |
/api/ap/aging?as_of=YYYY-MM-DD |
per-bill aging |
bill_id, amount, paid_amount, balance, status |
/api/vendors?limit=500 |
vendor master |
vendor_id, status, tax_id, bank_account_last4, payment_terms |
/api/compliance/profile/{business_id} |
KYC profile |
vendor_id, tax_id, missing_fields[] |
/api/compliance/ownership/{business_id} |
UBOs |
ubo_list[{name,ownership_pct}], shell_company_suspected, ownership_layer_count |
/api/compliance/registry/{business_id} |
registry |
license_expiry, tax_id, registration_number |
/api/compliance/screening/{business_id} |
screening |
pep_status, sanctions_check_status |
/api/compliance/bank/{business_id} |
bank |
bank_account_status |
/api/compliance/risk/{business_id} |
risk |
risk_score, review_status |
/api/prepaids/invoices?limit=500 |
prepaid schedule |
prepaid_invoice_id, account, original_amount, monthly_amortization, service_start, service_end, recognition_method, data_quality_flags[] |
/api/prepaids/gl-balances?limit=500 |
GL ending balances |
account, period, entity, account_name, ending_balance |
/api/close/logs?limit=500 |
close-log entries |
log_id, area, period, related_account, message, status |
Critical aging caveat: /api/ap/aging computes balance = amount − paid_amount, but its
paid_amount includes scheduled and processing payments, not just cleared ones. The business
"open AP balance" you must report counts cleared payments only. So do not trust the aging
balance directly for payable/open figures — recompute it (see §2). Aging is still handy for a
quick read of paid_amount totals and for spotting which bills have any payment activity.
Duplicate IDs exist. The same bill_id can appear on two different bill rows (one unrelated,
one the real reimbursement bill linked to the claim). Always match on the row whose claim_id,
amount, and vendor_id line up with the claim — don't grab the first row with that ID.
A helper script that fetches and paginates everything is in scripts/fetch.py (use it or curl).
2. Reimbursement-to-AP close (payable / paid / blocked)
For each requested claim_id: pull the claim, then find its linked AP bill(s) via
/api/ap/bills filtered/scanned on claim_id, then the bill's payments via /api/ap/payments
on bill_id. Classify the claim into exactly one bucket.
Define a bill as a valid matched reimbursement bill for a claim when ALL hold:
bill.claim_id == claim.claim_id
bill.amount == claim.amount (to the cent)
bill.vendor_id == claim.vendor_id (and the claim's vendor_id is not null)
bill.status != "void"
Compute the bill's cleared total = sum of payments on that bill with status == "cleared".
Open balance = bill.amount − cleared total (round 2 dp).
Buckets:
- paid — claim
status == "paid" AND a valid matched bill exists whose cleared total equals
the bill amount (open balance 0). These are already settled; they leave the open queue.
- payable — claim
status == "approved", has a valid matched bill, and open balance > 0
(i.e. payment is missing or only scheduled/processing, not cleared). These stay in the batch.
- blocked — anything else: claim not approved (e.g.
needs_receipt, draft), no linked bill,
bill is void, or the linked bill's amount/vendor doesn't match the claim. Vendor-null claims
whose bill cannot be validated land here too.
Assemble the template fields:
payable_claim_ids = payable bucket; paid_claim_ids = paid; blocked_claim_ids = blocked.
ap_open_balance_total = sum of open balances of payable claims only (not paid, not blocked).
crm_required_claim_ids = the blocked claims that need owner cleanup / AP-link remediation
(in practice = the blocked set, since each needs a fix before release).
batch_status: blocked if any claim is blocked; else open_payables if any payable remain;
else ready_to_close.
reviewed_claim_count = number of requested IDs.
- Sort every ID list ascending. Read the template's
unit — if "cents", convert.
Illustrative: an approved travel claim whose only payment is still processing (not cleared)
is payable with open balance = full amount; a claim marked paid with a cleared payment equal
to a vendor/amount-matched bill is paid; a claim whose linked bill has a wildly different
amount and vendor is blocked.
3. Vendor onboarding / intake compliance release
For each business_id, pull all six compliance endpoints plus the vendor record (via
profile.vendor_id → /api/vendors?vendor_id=...). Compute hard-stop flags, then a decision.
hard_stop_flags (list, alphabetical, empty if none) — raise each that applies:
bank_closed — bank.bank_account_status == "closed"
bank_name_mismatch — bank.bank_account_status == "name_mismatch"
confirmed_pep — screening.pep_status == "confirmed_pep"
sanctions_confirmed — screening.sanctions_check_status indicates a confirmed hit
screening_not_run — screening.sanctions_check_status == "not_run" OR pep_status == "not_run"
expired_license — license is expired by month granularity: (license_expiry.year, license_expiry.month) < (as_of.year, as_of.month). Use the batch as_of_date. But if
license is in profile.missing_fields, raise missing_required_documents instead (there is
no license to expire). Month granularity matters: a license expiring on the 17th of the as_of
month is NOT yet expired for a month-end as_of.
missing_required_documents — profile.missing_fields is non-empty
shell_company_suspected — ownership.shell_company_suspected == true
vendor_on_hold — vendor master status == "on_hold"
decision enum approve | awaiting_information | escalate:
escalate — any severe hard stop present: confirmed_pep, sanctions_confirmed,
shell_company_suspected, vendor_on_hold, bank_closed, or expired_license.
awaiting_information — only fixable/info hard stops present:
missing_required_documents, screening_not_run, bank_name_mismatch.
approve — no hard stops at all.
reportable_ubo_counts — per business, count unique beneficial-owner names whose
ownership_pct >= 25 (the reporting threshold). De-duplicate by name (the same name can appear
multiple times); count distinct names, not rows. A name below 25% does not count.
Summary fields:
per_business — one {business_id, decision} per business, ascending by business_id.
follow_up_business_ids — every business whose decision is not approve, ascending.
overall_release_ready — true only if every business decision is approve.
4. Prepaid-to-GL close reconciliation
Scope: the invoice IDs in the payload, the named accounts (e.g. 1250/1251), and the close period
(e.g. 2025-03 = March). Pull invoices from /api/prepaids/invoices and GL ending balances from
/api/prepaids/gl-balances (filter by account, period, and entity).
Per invoice, with straight-line monthly amortization (monthly_amortization = ma,
original_amount = orig):
term_months = (service_end.y - service_start.y)*12 + (service_end.m - service_start.m) + 1.
months_through_period = (period.y - service_start.y)*12 + (period.m - service_start.m) + 1.
march_amortization (period amortization) = ma if the period falls within
[service_start, service_end] else 0.
cumulative_amortization_through_march = min(months_through_period, term_months) * ma,
rounded 2 dp. Do not cap at original_amount — cumulative is months×ma even if that differs
from orig by a rounding cent (this is intentional; ending balances of 0.00 or 0.01 are normal).
ending_balance = round(orig − cumulative, 2).
default_missing_term_flag = "missing_contract_dates" is in data_quality_flags.
exception_flag = data_quality_flags is non-empty (ANY flag, e.g. rounded_amount,
missing_contract_dates, etc.). Empty flags ⇒ no exception.
account_rollup per account (key = account number as string):
account_name from the GL row; selected_invoice_count = scoped invoices on that account.
original_amount_total, march_amortization_total, cumulative_amortization_through_march,
schedule_ending_balance = sums of the per-invoice (already-rounded) values, then round.
gl_ending_balance = the GL row's ending_balance for that account/period/entity.
variance_amount = schedule_ending_balance − gl_ending_balance (round 2 dp).
variance_flag = abs(variance_amount) > variance_threshold_abs (default threshold 100.0 from
the payload).
has_default_missing_term_flag = any scoped invoice on that account has the missing-term flag.
account_status: requires_reconciliation if variance_flag is true (a real GL break);
variance_review for a within-tolerance soft difference needing eyes; reconciled if no
variance and clean. In practice a tripped variance flag dominates → requires_reconciliation.
Top-level: period, entity, selected_invoice_ids (in scope order), invoice_results (in
scope order), default_missing_term_invoice_ids and exception_invoice_ids (each ascending).
5. Stale AP snapshot reconciliation
A stale CSV/JSON snapshot is provided as context only. Re-derive everything from the live API for
each candidate claim_id, then describe how the snapshot must be corrected.
For each claim, find its valid matched bill and cleared total exactly as in §2.
ap_balance_by_claim[claim] = open AP balance = bill.amount − cleared total, ignoring stale
or voided AP rows and amount/vendor-mismatched rows (those contribute 0). A paid/cleared bill ⇒
0; an approved bill with only an in-flight (scheduled/processing) payment ⇒ full amount.
stale_snapshot_corrections[claim] — pick the one enum that explains the snapshot↔live delta:
block_unapproved_claim — live claim status is not approved (e.g. needs_receipt).
ignore_void_bill — the linked bill is void in live data.
exclude_amount_or_vendor_mismatch — the linked bill's amount or vendor doesn't match the claim.
replace_with_matched_paid_bill — the snapshot pointed at the wrong/old bill; live data has a
correctly matched paid bill that should replace it.
mark_in_flight_payment — a payment exists but is not cleared (scheduled/processing); mark the
payment as in-flight rather than treating the claim as settled or unpaid.
current_snapshot_ok — snapshot already matches live (rare).
Buckets:
eligible_claim_ids — claims that can stay in the batch: those with a valid matched bill that is
either a settled/paid match (replace_with_matched_paid_bill) or an in-flight payable
(mark_in_flight_payment).
not_ready_claim_ids — the rest (unapproved, void, mismatch).
close_log_required — when corrections required an AP refresh (in-flight payment or
replace-with-matched-paid-bill present), a close log documenting the AP manual journal entry for
the relevant period is required: set required=true and list the matching close/logs IDs
(area == "AP", message about a manual journal entry, for the affected period), ascending. If no
such correction is needed, required=false with an empty list.
batch_status: blocked if a candidate is fundamentally unworkable; needs_ap_refresh if the
batch is usable but the AP export must be refreshed (corrections like in-flight / replace / exclude
are present); ready_to_send if everything already reconciles.
6. AP payment release after vendor account changes
For each business_id in the batch, pull all six compliance endpoints plus the vendor record, as
of the review/as_of_date. Build the list fields, then the decision.
List fields (each ascending by business_id):
bank_mismatch_ids — bank.bank_account_status == "name_mismatch".
invalid_tax_ids — tax_id is malformed or a placeholder: not matching TIN + exactly 6 digits
(e.g. a letter inside like TIN12X899), or an obvious sentinel like TIN999999. Use the
compliance tax_id (profile/registry).
expired_license_ids — same month-granularity expiry rule as §3, comparing license_expiry to
as_of_date; skip businesses whose license is in missing_fields.
risk_score_override_flags — risk.risk_score >= 70.
review_queue_ids — every business whose decision is not release (needs review first).
decisions[business_id] enum release | hold | escalate:
escalate — a serious compliance/screening red flag: confirmed_pep, confirmed sanctions,
vendor on_hold, OR multiple stacked data-integrity failures (e.g. invalid tax and expired
license together).
hold — a single fixable blocker: bank name_mismatch or closed, expired license, missing docs,
invalid tax, screening not run, or risk_score >= 70.
release — no blockers at all (note: a risk_score below 70 and an in-review risk status do
not by themselves block release).
Echo the required literal fields from the template (task_id, batch_id, as_of_date,
target_business_ids ascending). additional_properties_allowed:false means emit exactly the
template's keys — nothing more.
7. Pre-submit checklist
- Re-derived everything from the live API, not the local snapshot/status.
- Paginated to
total rows; matched bills on claim_id + amount + vendor (handling duplicate IDs).
- Used cleared payments only for open balances; did not trust aging
balance directly.
- Excluded void / mismatched / unapproved items per the task's rules.
- Rounded money to 2 dp (or cents per template); sorted ID lists ascending; kept scope order where
the template requires it.
- Used the exact enum spellings from the template; emitted only the template's keys.
1---2name: erp-finance-close-tasks3description: Executable playbook for the shared ERP finance JSON API (claims, AP bills, payments, vendors, compliance, prepaids, GL, close logs). Use this whenever a task asks you to make a finance close/release decision from this API and return a JSON answer matching a provided answer_template.json — including reimbursement-to-AP close, expense-claim batch triage, vendor onboarding / intake compliance release, prepaid-to-GL amortization reconciliation, stale AP snapshot reconciliation, AP payment release after vendor account-change events, and month-end exception reporting. Trigger this skill even when the prompt only mentions "claim IDs", "business IDs", "AP batch", "prepaid close", "onboarding decision", "release posture", "aging", or "answer_template" without naming the workflow explicitly. It encodes the exact field definitions, decision enums, and rounding/sorting rules these graders check.4---56# ERP Finance Close & Release Tasks78You are answering finance-operations tasks against a shared ERP JSON API. Each task gives you9a small batch (claim IDs or business IDs), a local payload, and an `answer_template.json`. Your10job is to fetch the **live** records, apply the correct business rules, and emit one JSON object11matching the template exactly. Graders check exact enum values, list membership, ascending12sorting, and 2-decimal rounding — so precision matters more than prose.1314## 0. Golden rules (apply to every task)1516- **The live API is the system of record.** Local payloads (CSV snapshots, batch JSON, "current17 review status") are *context only*. If the prompt cites a base URL that differs from the one in18 your environment access notes, ignore the prompt's URL and use the environment's. Never read19 server source or local env data files; only HTTP GET the API.20- **Pagination:** list endpoints return `{count,total,offset,limit,data:[...]}`. Read `data`.21 Always pass `limit=500` (the max) and page with `offset` until you have all `total` rows.22 Default limit is only 100, so a naive call silently truncates.23- **Money:** round every currency value to 2 decimals. Some templates say "USD cents" (integers);24 read the template's `unit`/`precision` and match it (cents = round(dollars*100) as integer).25- **Sorting:** every list of IDs is sorted **ascending by ID string** unless the template says26 otherwise (e.g. prepaid `selected_invoice_ids` keep the scope order). Object keys for ID-keyed27 maps are also typically ascending.28- **Output only the JSON object** the template defines, with exactly its keys. If the template29 lists `required_top_level_keys` or `top_level_order`, honor it and add nothing extra.30- **Identify the task type** from the template's keys, then follow the matching SOP below. The six31 recognizable shapes:32 - `payable_claim_ids/paid_claim_ids/blocked_claim_ids/batch_status` → §2 reimbursement close33 - `per_business/reportable_ubo_counts/hard_stop_flags/overall_release_ready` → §3 onboarding34 - `account_rollup/invoice_results/march_amortization...` → §4 prepaid close35 - `eligible_claim_ids/stale_snapshot_corrections/close_log_required` → §5 stale AP snapshot36 - `decisions(release/hold/escalate)/bank_mismatch_ids/risk_score_override_flags` → §6 AP release3738## 1. The API surface (base URL from environment access notes)3940| Endpoint | Holds | Key fields |41|---|---|---|42| `/api/claims?limit=500` or `/api/claims/{id}` | expense claims | `claim_id, status, amount, vendor_id, receipt_status, policy_flags, approved_date` |43| `/api/ap/bills?limit=500` | AP bills | `bill_id, claim_id, amount, vendor_id, status, account, due_date` |44| `/api/ap/payments?limit=500` | payments | `payment_id, bill_id, amount, status, vendor_id, payment_date` |45| `/api/ap/aging?as_of=YYYY-MM-DD` | per-bill aging | `bill_id, amount, paid_amount, balance, status` |46| `/api/vendors?limit=500` | vendor master | `vendor_id, status, tax_id, bank_account_last4, payment_terms` |47| `/api/compliance/profile/{business_id}` | KYC profile | `vendor_id, tax_id, missing_fields[]` |48| `/api/compliance/ownership/{business_id}` | UBOs | `ubo_list[{name,ownership_pct}], shell_company_suspected, ownership_layer_count` |49| `/api/compliance/registry/{business_id}` | registry | `license_expiry, tax_id, registration_number` |50| `/api/compliance/screening/{business_id}` | screening | `pep_status, sanctions_check_status` |51| `/api/compliance/bank/{business_id}` | bank | `bank_account_status` |52| `/api/compliance/risk/{business_id}` | risk | `risk_score, review_status` |53| `/api/prepaids/invoices?limit=500` | prepaid schedule | `prepaid_invoice_id, account, original_amount, monthly_amortization, service_start, service_end, recognition_method, data_quality_flags[]` |54| `/api/prepaids/gl-balances?limit=500` | GL ending balances | `account, period, entity, account_name, ending_balance` |55| `/api/close/logs?limit=500` | close-log entries | `log_id, area, period, related_account, message, status` |5657**Critical aging caveat:** `/api/ap/aging` computes `balance = amount − paid_amount`, but its58`paid_amount` includes **scheduled and processing payments**, not just cleared ones. The business59"open AP balance" you must report counts **cleared payments only**. So do **not** trust the aging60`balance` directly for payable/open figures — recompute it (see §2). Aging is still handy for a61quick read of `paid_amount` totals and for spotting which bills have any payment activity.6263**Duplicate IDs exist.** The same `bill_id` can appear on two different bill rows (one unrelated,64one the real reimbursement bill linked to the claim). Always match on the row whose `claim_id`,65`amount`, and `vendor_id` line up with the claim — don't grab the first row with that ID.6667A helper script that fetches and paginates everything is in `scripts/fetch.py` (use it or curl).6869## 2. Reimbursement-to-AP close (payable / paid / blocked)7071For each requested `claim_id`: pull the claim, then find its linked AP bill(s) via72`/api/ap/bills` filtered/scanned on `claim_id`, then the bill's payments via `/api/ap/payments`73on `bill_id`. Classify the claim into exactly one bucket.7475Define a bill as a **valid matched reimbursement bill** for a claim when ALL hold:76- `bill.claim_id == claim.claim_id`77- `bill.amount == claim.amount` (to the cent)78- `bill.vendor_id == claim.vendor_id` (and the claim's `vendor_id` is not null)79- `bill.status != "void"`8081Compute the bill's **cleared total** = sum of payments on that bill with `status == "cleared"`.82**Open balance** = `bill.amount − cleared total` (round 2 dp).8384Buckets:85- **paid** — claim `status == "paid"` AND a valid matched bill exists whose cleared total equals86 the bill amount (open balance 0). These are already settled; they leave the open queue.87- **payable** — claim `status == "approved"`, has a valid matched bill, and open balance > 088 (i.e. payment is missing or only scheduled/processing, not cleared). These stay in the batch.89- **blocked** — anything else: claim not approved (e.g. `needs_receipt`, `draft`), no linked bill,90 bill is `void`, or the linked bill's amount/vendor doesn't match the claim. Vendor-null claims91 whose bill cannot be validated land here too.9293Assemble the template fields:94- `payable_claim_ids` = payable bucket; `paid_claim_ids` = paid; `blocked_claim_ids` = blocked.95- `ap_open_balance_total` = sum of open balances of **payable claims only** (not paid, not blocked).96- `crm_required_claim_ids` = the blocked claims that need owner cleanup / AP-link remediation97 (in practice = the blocked set, since each needs a fix before release).98- `batch_status`: `blocked` if any claim is blocked; else `open_payables` if any payable remain;99 else `ready_to_close`.100- `reviewed_claim_count` = number of requested IDs.101- Sort every ID list ascending. Read the template's `unit` — if "cents", convert.102103*Illustrative:* an approved travel claim whose only payment is still `processing` (not cleared)104is **payable** with open balance = full amount; a claim marked `paid` with a cleared payment equal105to a vendor/amount-matched bill is **paid**; a claim whose linked bill has a wildly different106amount and vendor is **blocked**.107108## 3. Vendor onboarding / intake compliance release109110For each `business_id`, pull all six compliance endpoints plus the vendor record (via111`profile.vendor_id` → `/api/vendors?vendor_id=...`). Compute hard-stop flags, then a decision.112113**`hard_stop_flags`** (list, alphabetical, empty if none) — raise each that applies:114- `bank_closed` — `bank.bank_account_status == "closed"`115- `bank_name_mismatch` — `bank.bank_account_status == "name_mismatch"`116- `confirmed_pep` — `screening.pep_status == "confirmed_pep"`117- `sanctions_confirmed` — `screening.sanctions_check_status` indicates a confirmed hit118- `screening_not_run` — `screening.sanctions_check_status == "not_run"` OR `pep_status == "not_run"`119- `expired_license` — license is expired **by month granularity**: `(license_expiry.year,120 license_expiry.month) < (as_of.year, as_of.month)`. Use the batch `as_of_date`. **But** if121 `license` is in `profile.missing_fields`, raise `missing_required_documents` instead (there is122 no license to expire). Month granularity matters: a license expiring on the 17th of the as_of123 month is NOT yet expired for a month-end as_of.124- `missing_required_documents` — `profile.missing_fields` is non-empty125- `shell_company_suspected` — `ownership.shell_company_suspected == true`126- `vendor_on_hold` — vendor master `status == "on_hold"`127128**`decision`** enum `approve | awaiting_information | escalate`:129- `escalate` — any **severe** hard stop present: `confirmed_pep`, `sanctions_confirmed`,130 `shell_company_suspected`, `vendor_on_hold`, `bank_closed`, or `expired_license`.131- `awaiting_information` — only **fixable/info** hard stops present:132 `missing_required_documents`, `screening_not_run`, `bank_name_mismatch`.133- `approve` — no hard stops at all.134135**`reportable_ubo_counts`** — per business, count **unique beneficial-owner names** whose136`ownership_pct >= 25` (the reporting threshold). De-duplicate by name (the same name can appear137multiple times); count distinct names, not rows. A name below 25% does not count.138139**Summary fields:**140- `per_business` — one `{business_id, decision}` per business, ascending by business_id.141- `follow_up_business_ids` — every business whose decision is not `approve`, ascending.142- `overall_release_ready` — `true` only if every business decision is `approve`.143144## 4. Prepaid-to-GL close reconciliation145146Scope: the invoice IDs in the payload, the named accounts (e.g. 1250/1251), and the close period147(e.g. 2025-03 = March). Pull invoices from `/api/prepaids/invoices` and GL ending balances from148`/api/prepaids/gl-balances` (filter by `account`, `period`, and `entity`).149150Per invoice, with straight-line monthly amortization (`monthly_amortization` = ma,151`original_amount` = orig):152- `term_months` = `(service_end.y - service_start.y)*12 + (service_end.m - service_start.m) + 1`.153- `months_through_period` = `(period.y - service_start.y)*12 + (period.m - service_start.m) + 1`.154- `march_amortization` (period amortization) = `ma` if the period falls within155 `[service_start, service_end]` else `0`.156- `cumulative_amortization_through_march` = `min(months_through_period, term_months) * ma`,157 rounded 2 dp. **Do not cap at original_amount** — cumulative is months×ma even if that differs158 from orig by a rounding cent (this is intentional; ending balances of 0.00 or 0.01 are normal).159- `ending_balance` = `round(orig − cumulative, 2)`.160- `default_missing_term_flag` = `"missing_contract_dates"` is in `data_quality_flags`.161- `exception_flag` = `data_quality_flags` is **non-empty** (ANY flag, e.g. `rounded_amount`,162 `missing_contract_dates`, etc.). Empty flags ⇒ no exception.163164`account_rollup` per account (key = account number as string):165- `account_name` from the GL row; `selected_invoice_count` = scoped invoices on that account.166- `original_amount_total`, `march_amortization_total`, `cumulative_amortization_through_march`,167 `schedule_ending_balance` = **sums of the per-invoice (already-rounded) values**, then round.168- `gl_ending_balance` = the GL row's `ending_balance` for that account/period/entity.169- `variance_amount` = `schedule_ending_balance − gl_ending_balance` (round 2 dp).170- `variance_flag` = `abs(variance_amount) > variance_threshold_abs` (default threshold 100.0 from171 the payload).172- `has_default_missing_term_flag` = any scoped invoice on that account has the missing-term flag.173- `account_status`: `requires_reconciliation` if `variance_flag` is true (a real GL break);174 `variance_review` for a within-tolerance soft difference needing eyes; `reconciled` if no175 variance and clean. In practice a tripped variance flag dominates → `requires_reconciliation`.176177Top-level: `period`, `entity`, `selected_invoice_ids` (in scope order), `invoice_results` (in178scope order), `default_missing_term_invoice_ids` and `exception_invoice_ids` (each ascending).179180## 5. Stale AP snapshot reconciliation181182A stale CSV/JSON snapshot is provided as context only. Re-derive everything from the live API for183each candidate `claim_id`, then describe how the snapshot must be corrected.184185For each claim, find its valid matched bill and cleared total exactly as in §2.186- `ap_balance_by_claim[claim]` = open AP balance = `bill.amount − cleared total`, ignoring stale187 or voided AP rows and amount/vendor-mismatched rows (those contribute 0). A paid/cleared bill ⇒188 0; an approved bill with only an in-flight (scheduled/processing) payment ⇒ full amount.189190`stale_snapshot_corrections[claim]` — pick the one enum that explains the snapshot↔live delta:191- `block_unapproved_claim` — live claim status is not approved (e.g. `needs_receipt`).192- `ignore_void_bill` — the linked bill is `void` in live data.193- `exclude_amount_or_vendor_mismatch` — the linked bill's amount or vendor doesn't match the claim.194- `replace_with_matched_paid_bill` — the snapshot pointed at the wrong/old bill; live data has a195 correctly matched **paid** bill that should replace it.196- `mark_in_flight_payment` — a payment exists but is not cleared (scheduled/processing); mark the197 payment as in-flight rather than treating the claim as settled or unpaid.198- `current_snapshot_ok` — snapshot already matches live (rare).199200Buckets:201- `eligible_claim_ids` — claims that can stay in the batch: those with a valid matched bill that is202 either a settled/paid match (replace_with_matched_paid_bill) or an in-flight payable203 (mark_in_flight_payment).204- `not_ready_claim_ids` — the rest (unapproved, void, mismatch).205206`close_log_required` — when corrections required an AP refresh (in-flight payment or207replace-with-matched-paid-bill present), a close log documenting the AP manual journal entry for208the relevant period is required: set `required=true` and list the matching `close/logs` IDs209(`area == "AP"`, message about a manual journal entry, for the affected period), ascending. If no210such correction is needed, `required=false` with an empty list.211212`batch_status`: `blocked` if a candidate is fundamentally unworkable; `needs_ap_refresh` if the213batch is usable but the AP export must be refreshed (corrections like in-flight / replace / exclude214are present); `ready_to_send` if everything already reconciles.215216## 6. AP payment release after vendor account changes217218For each `business_id` in the batch, pull all six compliance endpoints plus the vendor record, as219of the review/`as_of_date`. Build the list fields, then the decision.220221List fields (each ascending by business_id):222- `bank_mismatch_ids` — `bank.bank_account_status == "name_mismatch"`.223- `invalid_tax_ids` — tax_id is malformed or a placeholder: not matching `TIN` + exactly 6 digits224 (e.g. a letter inside like `TIN12X899`), or an obvious sentinel like `TIN999999`. Use the225 compliance `tax_id` (profile/registry).226- `expired_license_ids` — same month-granularity expiry rule as §3, comparing `license_expiry` to227 `as_of_date`; skip businesses whose `license` is in `missing_fields`.228- `risk_score_override_flags` — `risk.risk_score >= 70`.229- `review_queue_ids` — every business whose decision is **not** `release` (needs review first).230231`decisions[business_id]` enum `release | hold | escalate`:232- `escalate` — a serious compliance/screening red flag: `confirmed_pep`, confirmed sanctions,233 vendor `on_hold`, OR multiple stacked data-integrity failures (e.g. invalid tax **and** expired234 license together).235- `hold` — a single fixable blocker: bank name_mismatch or closed, expired license, missing docs,236 invalid tax, screening not run, or `risk_score >= 70`.237- `release` — no blockers at all (note: a `risk_score` below 70 and an in-review risk status do238 not by themselves block release).239240Echo the required literal fields from the template (`task_id`, `batch_id`, `as_of_date`,241`target_business_ids` ascending). `additional_properties_allowed:false` means emit exactly the242template's keys — nothing more.243244## 7. Pre-submit checklist245246- Re-derived everything from the **live API**, not the local snapshot/status.247- Paginated to `total` rows; matched bills on claim_id + amount + vendor (handling duplicate IDs).248- Used **cleared payments only** for open balances; did not trust aging `balance` directly.249- Excluded void / mismatched / unapproved items per the task's rules.250- Rounded money to 2 dp (or cents per template); sorted ID lists ascending; kept scope order where251 the template requires it.252- Used the exact enum spellings from the template; emitted only the template's keys.