ERP Finance Expense-Control Skill (task_group_005)
Reusable workflow rules for solving ERP finance expense-control tasks against the shared remote ERP API. Distilled from reflect-loop practice on train tasks. These are workflow rules, not per-task answers.
0. Ground rules
- Use ONLY the remote ERP Web API (system of record). Do NOT read local
env/source files or test fixtures. - Report all currency in USD to 2 decimals unless a task explicitly says otherwise. Follow the
answer_template.jsonunit/precisionfields (they override ambiguous prose). Example: a template that saysunit: USD, precision: 2means dollars with 2 decimals — do NOT convert to cents even if prose says "cents". - Sort every ID list exactly as the template specifies (usually ascending by ID; digits sort before uppercase letters, so
CLM-2025-00xx<CLM-2025-FIN-xx<CLM-2025-OPS-xx;PPD-2025-*<PPD-AUR-*). - Preserve list orders that the template says must match an input payload order (e.g.
selected_invoice_ids"same order as scope file"). - Query specific records by exact-match params (claim_id, bill_id, business_id, account, period, prepaid_invoice_id, vendor_id). Do not pull whole collections.
1. API contract
- Base URL +
GET /endpointslists all paths and the filtering contract. - Health:
GET /health,GET /api/health. - Resources (namespaced
/api/...preferred; bare aliases also exist):/api/claims,/api/ap/bills,/api/ap/payments,/api/vendors,/api/close/logs,/api/prepaids/invoices,/api/prepaids/gl-balances,/api/compliance/objects. - Compliance detail routes:
/api/compliance/{profile,ownership,registry,screening,bank,risk}/{business_id}. TIP:/api/compliance/objects?business_id=Xreturns a consolidated object with every field (bank_account_status, license_expiry, missing_fields, pep_status, sanctions_check_status, shell_company_suspected, tax_id, ubo_list, ownership_layer_count, risk_score, review_status, vendor_id, business_name, jurisdiction) — fetch this one endpoint per business instead of 6 detail calls. - All list endpoints: exact-match query by field name +
limit/offset.
2. Claims / AP / payment reconciliation (reimbursement-to-AP close)
For each candidate claim, fetch the claim, then bills by claim_id, then payments by bill_id.
- paid: claim has an AP bill whose amount AND vendor match the claim, bill status
paid, and a payment with statusclearedfor the claim amount. Ignore any other (mismatched/stale) bills linked to the same claim. - payable (stays in AP queue): claim approved + a valid OPEN AP bill (status
scheduled/approved, amount & vendor match) + no cleared payment yet. Aprocessingpayment is NOT cleared — the bill stays open at full amount. - blocked (needs owner/AP-link remediation): not paid AND the AP link is wrong — bill amount/vendor mismatch, bill
void, or no bill at all.crm_required= the blocked set when the cause is expense-case/AP-link remediation. - batch_status:
blockedif any item blocked; elseopen_payablesif valid unpaid AP bills remain; elseready_to_close. - ap_open_balance_total: sum of valid open AP bill amounts for payable claims only (USD, 2 decimals). Apply only CLEARED payments to reduce a balance; ignore void/mismatched/stale bills.
3. Stale-AP-export batch review (conference reimbursement)
The local CSV/stale snapshot is context only — the API is the system of record. Compare snapshot vs current API per claim.
- eligible (can remain in batch): approved claim with a valid AP bill. A PAID claim that has a matching paid bill + cleared payment is still ELIGIBLE with open balance 0 (it is correctly reconciled, not "not ready").
- not_ready: claim unapproved / not
approved; or only a void bill; or only an amount/vendor-mismatched bill; (a paid claim is NOT not_ready). - ap_balance_by_claim: full bill amount when no cleared payment; 0 when a cleared payment covers it; ignore void/mismatched/stale rows.
- stale_snapshot_corrections (one enum per claim):
mark_in_flight_payment(snapshot had no payment, API showsprocessing),replace_with_matched_paid_bill(snapshot cited wrong/stale bill, API has the matched paid bill),exclude_amount_or_vendor_mismatch(bill exists but amount/vendor ≠ claim),ignore_void_bill(bill isvoid),block_unpaid/block_unapproved_claim(claim status ≠ approved),current_snapshot_ok(snapshot already matches API). - batch_status:
needs_ap_refreshwhen the batch came from a stale AP export and has correctable items (even with unapproved/mismatch/void present — those become not_ready, they do not make the whole batchblocked);ready_to_sendwhen clean;blockedonly for a hard whole-batch block. - close_log_required: for a stale-AP batch a close log is typically
required=true; select the close-log IDs that are non-closed and thematically tied to the batch (e.g. statusblocked/open/ready_for_reviewon the batch's AP accounts, or messages about "AP export refresh"). Fetch/api/close/logsand filter byrelated_account/area/status. (Close logs have no claim_id/bill_id field — relate by account and message theme.)
4. Vendor onboarding / compliance release (finance-risk)
Use as_of_date from the batch payload for all date comparisons.
- reportable_ubo_counts: count UNIQUE beneficial-owner NAMES whose
ownership_pct >= 25(the standard BO reporting threshold; values like 24% and 10% are deliberately below). Dedupe by name; a name counts if ANY of its entries is >= 25. - hard_stop_flags (per business, list sorted alphabetically; empty list when none):
bank_closed—bank_account_status == "closed"bank_name_mismatch—bank_account_status == "name_mismatch"confirmed_pep—pep_status == "confirmed_pep"(NOTpossible_pep)expired_license—license_expiry < as_of_datemissing_required_documents—missing_fieldsnon-emptysanctions_confirmed—sanctions_check_statusis a positive/confirmed hit (none observed whenclear)screening_not_run—sanctions_check_status == "not_run"(and treatpep_status == "not_run"as a screening gap too)shell_company_suspected—shell_company_suspected == truevendor_on_hold— vendor recordstatus == "on_hold"(fetch/api/vendors)
- decision:
escalateONLY when a hard stop in {confirmed_pep,shell_company_suspected,sanctions_confirmed} applies.awaiting_informationwhen only the other (remediable) hard stops apply.approvewhen no hard stops. Do NOT copyreview_status— areview_statusofescalateddoes NOT by itself forceescalate(confirmed: high-risk-but-no-pep/shell/sanctions businesses stayawaiting_information). - follow_up_business_ids: all non-approve (ascending). overall_release_ready: true only if every business is
approve.
5. Payment-release risk review (post account-change)
Same compliance/vender endpoints. Decisions: release / hold / escalate.
- escalate is STRICT — identical to onboarding: only
confirmed_pep,shell_company_suspected, orsanctions_confirmed. Confirmed:risk_score >= 70,bank_account_status == "closed",sanctions_check_status == "not_run",bank_name_mismatch,expired_license,invalid_tax,missing_required_documentsall →hold(NOT escalate) when none of the three escalation triggers are present. - release: no compliance hard flags at all AND
risk_score < 70. A business withreview_status == "in_review"but no hard flags is stillrelease(do not hold merely for in-review status). - risk_score_override_flags: business IDs with
risk_score >= 70(ascending). This is a flag list, not an escalation trigger. - bank_mismatch_ids:
bank_account_status == "name_mismatch"(ascending). - invalid_tax_ids: tax_id that is malformed or a placeholder — non-digit chars (e.g.
TIN12X899) or all-same-digit sentinel (e.g.TIN999999); also flagged when compliancetax_id≠ vendortax_id(fetch/api/vendors). Ascending. - expired_license_ids:
license_expiry < as_of_date(the template namesas_of_dateas the comparison date). A license dated one day AFTER as_of (e.g.2025-06-02vs2025-06-01) is NOT expired. Consider whether a business with a MISSING license (inmissing_fields) should also be treated as not-currently-licensed. - review_queue_ids: all non-
releasebusiness IDs (hold + escalate), ascending. A released (no-flag) business is NOT in the queue even if itsreview_statusisin_review. target_business_idsascending; echo the fixedtask_id,batch_id,as_of_dateexactly as the template requires.
6. Prepaid close (straight-line amortization reconciliation)
Scope = invoices listed in the payload; reconcile only the named accounts; use the GL ending balance for the close period.
- monthly amortization: use the
monthly_amortizationfield AS RECORDED. cumulative_amortization_through_march =monthly_amortization× (number of months from service_start through the close month inclusive). Do NOT prorate asoriginal × months/term— using the rounded record monthly × count is correct (prorating breaks the score). - March counts as a full amortization month for every invoice active in March, including mid-month starts (e.g. service_start
2025-03-15). Term months =original_amount / monthly_amortization. - ending_balance =
original_amount − cumulative_amortization_through_march(can be 0.00 when fully amortized by the close month; small 0.01 residuals from rounded monthly are expected — keep them). - march_amortization =
monthly_amortizationif the invoice is active in the close month, else 0. - default_missing_term_flag = invoice
data_quality_flagscontainsmissing_contract_dates. Account-levelhas_default_missing_term_flag= any selected invoice in that account has it. - exception_flag = invoice has ANY
data_quality_flagsentry (e.g.rounded_amount,missing_contract_dates).exception_invoice_ids= all such invoices, ascending. - variance_amount =
schedule_ending_balance − gl_ending_balance.variance_flag=|variance| > variance_threshold_abs(from scope).gl_ending_balance=/api/prepaids/gl-balances?account=Xrow withperiod== close period. - account_status:
requires_reconciliationwhenhas_default_missing_term_flagis true (missing-term data prevents reliable reconciliation — this OVERRIDES variance_review). Elsevariance_reviewwhenvariance_flagtrue. Elsereconciled. (Confirmed: an account with a missing-term invoice staysrequires_reconciliationeven though its variance is also large.) - Account rollup totals are sums across the account's selected invoices; keep 2 decimals.
selected_invoice_idsin the scope-file order; the two exception ID lists ascending.
7. Common misjudgments to avoid
- Prorating prepaid amortization — use recorded
monthly_amortization × month_count. - Using a UBO threshold other than 25%, or counting all UBOs regardless of ownership.
- Letting
review_statusdrive the decision — it does not; only hard compliance flags do (escalatedstatus ≠ escalate decision). - Over-broad escalate:
risk_score>=70,bank_closed,sanctions_not_run,invalid_tax,bank_name_mismatch,expired_license→ HOLD (not escalate) in both onboarding and payment-release. Onlyconfirmed_pep/shell_company_suspected/sanctions_confirmedescalate. - Treating a paid (matched-bill, cleared-payment) claim as
not_ready— it iseligiblewith 0 balance. - Setting batch_status
blockedfor a stale-AP batch — useneeds_ap_refresh. - Converting to cents when the template says
unit: USD, precision: 2. - Setting
account_status = variance_reviewfor an account that has a missing-term invoice — it must berequires_reconciliation. - Excluding March amortization for mid-month-start invoices — March counts.
- Forgetting to apply ONLY cleared payments to AP open balances (processing/scheduled payments do not reduce the open balance).
- Using
<=or a grace window for license expiry — it is a strictlicense_expiry < as_of_date.