ERP Finance Expense-Control Skill
Overview
This skill covers five expense-control workflows in the ERP finance domain:
- Claims-to-AP close review — classify reimbursement claims against AP bills/payments
- Vendor onboarding release control — compliance screening for vendor access decisions
- Prepaid close reconciliation — schedule-to-GL variance analysis with straight-line amortization
- Stale AP snapshot reconciliation — reconcile candidate claims against current ERP state vs stale export
- Account-change payment release — compliance-gated payment release after vendor account-change events
All workflows use a shared ERP finance API. Always query the API for current data — never rely on stale snapshots or local files as the system of record.
API Reference
Base URL
Use the environment-provided base URL. All endpoints accept exact-match query parameters by field name, plus limit and offset for pagination.
Endpoints
| Endpoint |
Purpose |
/api/claims |
Expense claim records. Query by claim_id. |
/api/ap/bills |
AP reimbursement bills. Query by claim_id, bill_id. |
/api/ap/payments |
Payment records. Query by bill_id. |
/api/vendors |
Vendor master data. Query by vendor_id. |
/api/compliance/objects |
Compliance screening records. Query by business_id. |
/api/prepaids/invoices |
Prepaid invoice schedules. Query by prepaid_invoice_id. |
/api/prepaids/gl-balances |
GL ending balances by account/period/entity. Query by account, period, entity. |
/api/close/logs |
Close/review log entries. Query by period, area, related_account. |
/health, /endpoints |
Service health and endpoint listing. |
Data Field Reference
Claims (/api/claims):
claim_id, amount (USD), status (submitted/needs_receipt/approved/paid/rejected)
approved_date, submitted_date, category, department, employee_name
policy_flags (array: manual_rate, late_receipt, over_limit, weekend_spend, duplicate_amount)
receipt_status (attached/partial/missing), vendor_id, notes, currency
AP Bills (/api/ap/bills):
bill_id, claim_id (nullable — null means non-reimbursement bill), vendor_id
amount (USD), status (scheduled/approved/paid/void), account, currency
bill_date, due_date, invoice_number, memo
AP Payments (/api/ap/payments):
payment_id, bill_id, vendor_id, amount (USD), method
status (scheduled/processing/cleared), payment_date, bank_reference
Vendors (/api/vendors):
vendor_id, vendor_name, legal_name, status (active/inactive/on_hold)
bank_account_last4, tax_id, industry, payment_terms, default_account, updated_at
Compliance Objects (/api/compliance/objects):
business_id, business_name, vendor_id, jurisdiction, registration_number
bank_account_status (verified/name_mismatch/closed)
pep_status (none/possible_pep/confirmed_pep/not_run)
sanctions_check_status (clear/possible_match/confirmed_match/not_run)
license_expiry (date string), missing_fields (array)
risk_score (integer 0-100), review_status (not_started/in_review/awaiting_information/approved/escalated)
shell_company_suspected (boolean), ownership_layer_count (integer)
tax_id, ubo_list (array of {name, ownership_pct})
Prepaid Invoices (/api/prepaids/invoices):
prepaid_invoice_id, account, description, vendor_id
original_amount, monthly_amortization, recognition_method (straight_line)
service_start, service_end, invoice_date, invoice_number, source_document
data_quality_flags (array: rounded_amount, missing_contract_dates, manual_override, duplicate_invoice_number)
GL Balances (/api/prepaids/gl-balances):
account, account_name, entity, period (YYYY-MM)
ending_balance, source, loaded_at
Close Logs (/api/close/logs):
log_id, period, area (AP/Expense/Prepaids/GL/Treasury/Compliance)
status (open/ready_for_review/closed/blocked), message, owner
related_account, created_at
Workflow 1: Claims-to-AP Close Review
Business Rules
Classification logic for each claim in the batch:
PAID — ALL of the following must be true:
- A bill exists linked to the claim (
bill.claim_id == claim.claim_id)
- The bill's
status is "paid"
- A payment exists for that bill with
status "cleared"
- The bill amount matches the claim amount (exact match)
PAYABLE (can stay in AP queue) — ALL of:
- Claim
status is "approved"
- At least one bill exists linked to the claim
- The bill amount matches the claim amount (exact match)
- The bill
status is NOT "void"
- No blocking conditions apply
BLOCKED — ANY of:
- Claim
status is NOT "approved" (and not already marked paid) — e.g., submitted, needs_receipt, rejected
- No AP bill exists linked to the claim
- All linked bills are
"void"
- Bill amount does NOT match claim amount (amount mismatch indicates wrong AP link)
- Bill memo contains
"Duplicate check required" with a mismatched amount
- Claim has
receipt_status: "partial" combined with policy issues (over_limit, late_receipt)
CRM-required classification (subset of blocked):
- Claims needing expense-case owner cleanup: missing bill, partial receipt, policy flags (
over_limit, late_receipt), claim not approved
- Claims needing AP-link remediation: amount mismatch between claim and bill, void bill, duplicate check memo
AP Open Balance Total:
- Sum bill amounts for payable claims only
- Use the matching bill (same amount as claim) linked to the claim
- Report in USD cents (multiply by 100) or as a decimal with 2 decimal places (check template)
- Ignore void bills and mismatched bills in the balance computation
Batch Status:
"blocked" — any batch item is blocked
"open_payables" — no blocked items, but valid unpaid AP bills remain
"ready_to_close" — all items are paid, no open payables
Source Precedence
- Current API claim record (authoritative for claim status/amount)
- Current API bill records linked by claim_id (authoritative for bill status/amount)
- Current API payment records linked by bill_id (authoritative for payment status)
Common Pitfalls
- Multiple bills per claim: A claim can have multiple bills (e.g., legacy + current). Match by amount equality to the claim. Ignore stale/legacy bills with different amounts.
- Bill amount ≠ claim amount: This is always a blocking mismatch — do not assume the bill is correct.
- Void bills: A void bill linked to a claim makes the claim blocked unless another valid paid bill+payment exists.
- Processing payments: A bill with a
processing payment is NOT paid — payment must be cleared.
- Scheduled payments: A
scheduled payment is NOT cleared — do not classify as paid.
Workflow 2: Vendor Onboarding Release Control
Business Rules
Decision matrix per business:
| Condition |
Decision |
Any hard-stop flag present, OR vendor on_hold, OR review escalated |
escalate |
review_status is in_review or awaiting_information, no hard stops |
awaiting_information |
review_status is approved or not_started, no hard stops, no blocking issues |
approve |
sanctions_check_status is possible_match without hard stops |
awaiting_information |
pep_status is possible_pep without other hard stops |
awaiting_information |
Hard-stop flag mapping (compliance → flag):
| Compliance Field Value |
Hard-Stop Flag |
bank_account_status: "closed" |
bank_closed |
bank_account_status: "name_mismatch" |
bank_name_mismatch |
pep_status: "confirmed_pep" |
confirmed_pep |
license_expiry < as_of_date (expired) |
expired_license |
missing_fields is non-empty (contains "license", "beneficial_owner_id", "bank_statement", "website", etc.) |
missing_required_documents |
sanctions_check_status: "confirmed_match" |
sanctions_confirmed |
sanctions_check_status: "not_run" |
screening_not_run |
shell_company_suspected: true |
shell_company_suspected |
Vendor status: "on_hold" |
vendor_on_hold |
UBO reporting threshold: 25% ownership.
- Count unique UBO names across all ownership layers
- Aggregate multiple entries for the same name (sum their
ownership_pct)
- Count the name if the aggregated percentage ≥ 25%
pep_status: "not_run" does NOT trigger screening_not_run (that flag is for sanctions check not run)
Output Conventions
- All ID lists sorted ascending (lexicographic for business IDs, claim IDs, log IDs)
- Hard-stop flag lists sorted alphabetically by flag enum value
- Empty lists
[] when no items, never null
overall_release_ready: true only when ALL businesses in the batch have decision "approve"; false if any business is "awaiting_information" or "escalate"
follow_up_business_ids: all business IDs whose decision is NOT "approve" (i.e., "awaiting_information" or "escalate")
Common Pitfalls
pep_status: "not_run" ≠ screening_not_run: screening_not_run refers to sanctions screening, not PEP screening. Only sanctions_check_status: "not_run" triggers screening_not_run.
pep_status: "possible_pep" is NOT a hard stop — only confirmed_pep is.
- License expiry date comparison: Use the
as_of_date from the batch payload. If license_expiry < as_of_date, the license is expired.
- Vendor status
on_hold maps to the vendor_on_hold hard-stop flag, not a separate category.
- Multiple UBO entries for same name: Aggregate ownership percentages before checking the 25% threshold.
missing_fields containing "license": This is missing_required_documents, NOT expired_license. Only use expired_license when the license date has passed.
Workflow 3: Prepaid Close Reconciliation
Business Rules
Amortization method: Straight-line monthly.
monthly_amortization is provided in the invoice record — use it directly
- For March 2025 period: March is month index relative to
service_start
- If
service_start is 2025-01-01: January=month 1, February=month 2, March=month 3
- March amortization =
monthly_amortization (one month's worth)
Cumulative amortization through March:
- Count full months from
service_start through the close period end
- If
service_start <= 2025-03-01: count months from start through March inclusive
cumulative = monthly_amortization × months_elapsed
Schedule ending balance:
original_amount - cumulative_amortization_through_march
Invoice-level fields:
march_amortization: the monthly amortization for March (same as monthly_amortization unless the service started/ended mid-March)
- If
service_start is after March 2025, amortization is 0
- If
service_end is before March 2025, amortization is 0
- Otherwise, use the full
monthly_amortization value
cumulative_amortization_through_march: see formula above
ending_balance: original_amount - cumulative_amortization_through_march
default_missing_term_flag: true if data_quality_flags contains "missing_contract_dates" or "manual_override" or if the invoice is missing service dates
exception_flag: true if data_quality_flags is non-empty (any quality flag is present)
Account-level rollup (for each account):
selected_invoice_count: number of scoped invoices in that account
original_amount_total: sum of original_amount for all scoped invoices in that account
march_amortization_total: sum of march_amortization
cumulative_amortization_through_march: sum of cumulative across invoices
schedule_ending_balance: original_amount_total - cumulative_amortization_through_march
gl_ending_balance: from GL balances API for the account/period/entity
variance_amount: schedule_ending_balance - gl_ending_balance
variance_flag: true if abs(variance_amount) > variance_threshold_abs (from scope config)
Account status:
"reconciled" — variance_flag is false AND no invoice in the account has an exception flag AND no default/missing term flag
"variance_review" — variance_flag is true (regardless of exceptions)
"requires_reconciliation" — variance_flag is false BUT exceptions or default/missing terms exist
Default/missing term flag at account level:
true if ANY invoice in the account has default_missing_term_flag: true
Output Ordering
selected_invoice_ids: same order as prepaid_close_scope.json
invoice_results: same order as prepaid_close_scope.json
default_missing_term_invoice_ids: ascending by invoice ID
exception_invoice_ids: ascending by invoice ID
Common Pitfalls
- Service period boundaries: If
service_start is mid-month (e.g., 2025-03-15), March is month 1. Full amortization applies for that partial month (no proration).
- Months elapsed calculation: Count calendar months from start through the close period. A service starting 2025-01-15 has January as month 1, February as month 2, March as month 3.
data_quality_flags drives exceptions: Any non-empty flag array means exception_flag: true. The specific flag determines default_missing_term_flag.
- GL balance lookup: Match exactly on
account, period (YYYY-MM), and entity from the scope config.
Workflow 4: Stale AP Snapshot Reconciliation
Business Rules
Source precedence (always):
- Current API claims (authoritative for claim status/amount)
- Current API bills (authoritative for bill status/amount)
- Current API payments (authoritative for payment status)
- Stale CSV snapshot (context only — do NOT use as system of record)
Eligibility (eligible_claim_ids):
A claim can remain in the batch if:
- Claim
status is "approved" (current API)
- At least one non-void bill linked to the claim exists with a matching amount
- No blocking conditions (see not_ready below)
Not ready (not_ready_claim_ids):
- Claim
status is not "approved" (e.g., needs_receipt, submitted, rejected)
- All linked bills are void
- Bill amount ≠ claim amount (amount mismatch)
- Claim has
receipt_status: "partial" or "missing" with no cleared payment
- Bill is paid+cleared but for a different amount than the claim
AP balance computation:
- For each candidate claim, find the matching non-void bill (same amount as claim)
- Subtract any cleared payment amount for that bill
ap_balance = bill_amount - sum(cleared_payment_amounts)
- For claims with no bill or only void bills: balance is 0 (not in AP)
- For paid claims: balance is 0 (fully paid)
Stale snapshot corrections — map each claim to ONE correction:
| Current State |
Correction |
Payment exists in API (cleared/processing) but snapshot shows none/0 |
mark_in_flight_payment |
| Bill+payment match claim amount, snapshot shows different bill or status |
replace_with_matched_paid_bill |
| Bill amount or vendor differs between current API and snapshot |
exclude_amount_or_vendor_mismatch |
Bill status is void in current API |
ignore_void_bill |
Claim status is NOT approved in current API |
block_unapproved_claim |
| Snapshot matches current API state (same bill status, same payment status) |
current_snapshot_ok |
Close log requirements:
- Check close logs API for entries where:
area is "AP" or "Expense"
period matches the relevant close period
status is NOT "closed" (i.e., open, ready_for_review, blocked)
close_log_required.required: true if ANY non-closed AP/Expense log exists
close_log_required.ids: list of matching log IDs, ascending
Batch status:
"ready_to_send" — all claims eligible, no stale corrections needed beyond current_snapshot_ok
"needs_ap_refresh" — some claims eligible but snapshot corrections needed (stale data)
"blocked" — any claim is not ready
Common Pitfalls
- Snapshot is NOT authoritative: Always cross-reference against current API. The snapshot can be outdated (bill status changed, payment arrived after snapshot).
- One correction per claim: Choose the most severe/descriptive correction that applies.
- Multiple bills per claim: A claim can have both a stale/legacy bill and a current paid bill. Match by amount equality to the claim amount.
processing vs cleared payment: Only cleared payments reduce the AP balance. processing payments are in flight but not yet settled.
Workflow 5: Account-Change Payment Release
Business Rules
Decision matrix per business (release/hold/escalate):
| Condition |
Decision |
Any hard compliance flag (see below), OR vendor on_hold, OR risk_score >= 70 |
escalate |
review_status is in_review or awaiting_information, no hard flags, risk < 70 |
hold |
review_status is not_started, no hard flags, risk < 70 |
hold |
sanctions_check_status: "not_run", no other hard flags |
hold |
sanctions_check_status: "possible_match", no other hard flags |
hold |
pep_status: "possible_pep", no other hard flags |
hold |
review_status: "approved", no hard flags, risk < 70, sanctions clear, bank verified |
release |
Bank mismatch check:
- Compare the
requested_bank_last4 from the account-change event to the vendor's bank_account_last4
- If they differ, flag as bank mismatch AND check compliance
bank_account_status for name_mismatch
Hard compliance flags (same mapping as Workflow 2):
bank_account_status: "closed" → escalate
bank_account_status: "name_mismatch" → escalate
pep_status: "confirmed_pep" → escalate
license_expiry < as_of_date → escalate
missing_fields non-empty → escalate
sanctions_check_status: "confirmed_match" → escalate
sanctions_check_status: "not_run" → hold (not escalate)
shell_company_suspected: true → escalate
Specific output lists:
bank_mismatch_ids: business IDs where compliance bank_account_status == "name_mismatch"
invalid_tax_ids: business IDs where the vendor tax_id does not match the compliance tax_id
expired_license_ids: business IDs where license_expiry < as_of_date
review_queue_ids: business IDs where decision is hold OR escalate (any non-release)
risk_score_override_flags: business IDs where risk_score >= 70
Common Pitfalls
- Tax ID validation: Compare the tax_id from
/api/vendors against the tax_id from /api/compliance/objects. Non-matching or non-standard format tax IDs (e.g., containing letters beyond the TIN prefix) mean the business belongs in invalid_tax_ids.
- Risk score threshold of 70:
>= 70 means escalate and inclusion in risk_score_override_flags.
- Bank matching uses compliance data:
bank_mismatch_ids is populated from compliance bank_account_status: "name_mismatch", not from comparing last4 digits. The last4 comparison from the account-change event is additional context.
review_status: "escalated" in compliance means automatic escalate decision.
Cross-Cutting Conventions
Amount Handling
- All amounts in USD
- Report to 2 decimal places unless the template specifies USD cents (integer)
- When computing balances, sum exact values then round the final result to 2 decimals
Sort Order
- All ID lists: ascending lexicographic order (standard string sort)
- Hard-stop flag lists: alphabetical by enum value
- Invoice result lists: same order as input scope file
- Business ID lists: ascending by business_id
ID Formats
- Claims:
CLM-YYYY-NNNN or CLM-YYYY-CATEGORY-NNN
- Bills:
AP-YYYY-NNNN or AP-YYYY-REIM-NNN
- Payments:
PAY-YYYY-NNNN
- Businesses:
BUS-YYYY-NNNN
- Vendors:
VEN-NNNN
- Prepaid invoices:
PPD-YYYY-NNNN or PPD-AUR-ACCT-XXX-NNN
- Close logs:
CLOSE-YYYY-MM-NNN
- Account-change tickets:
ACT-YYMMDD-NNN
API Query Pattern
- Query the relevant list endpoint with the exact-match field parameter
- The API returns
{count, data[], endpoint, limit, offset, total}
- Use
limit and offset for pagination when total > count
- Filtering is exact-match only — query one ID at a time for precision
Data Quality Signals
policy_flags on claims: Non-empty flags indicate data quality or policy issues — these affect CRM-required classification
data_quality_flags on prepaid invoices: Non-empty flags trigger exception flags and may trigger default/missing term flags
missing_fields on compliance objects: Non-empty means missing_required_documents hard stop
memo fields on bills: Look for keywords like "Duplicate check required", "Imported from AP inbox", "Accrual review", "Partial receipt support noted" — these signal potential issues
Error Recovery
- If an API endpoint returns empty results for a known ID, treat it as "no record exists" (not an error)
- If a claim has no linked bill, it has no AP representation — classify accordingly
- If a bill has no linked payment, treat the payment amount as 0
1---2name: self-attempt-01-443description: ERP Finance Expense-Control Skill4---5# ERP Finance Expense-Control Skill67## Overview89This skill covers five expense-control workflows in the ERP finance domain:101. **Claims-to-AP close review** — classify reimbursement claims against AP bills/payments112. **Vendor onboarding release control** — compliance screening for vendor access decisions123. **Prepaid close reconciliation** — schedule-to-GL variance analysis with straight-line amortization134. **Stale AP snapshot reconciliation** — reconcile candidate claims against current ERP state vs stale export145. **Account-change payment release** — compliance-gated payment release after vendor account-change events1516All workflows use a shared ERP finance API. **Always query the API for current data** — never rely on stale snapshots or local files as the system of record.1718---1920## API Reference2122### Base URL2324Use the environment-provided base URL. All endpoints accept exact-match query parameters by field name, plus `limit` and `offset` for pagination.2526### Endpoints2728| Endpoint | Purpose |29|---|---|30| `/api/claims` | Expense claim records. Query by `claim_id`. |31| `/api/ap/bills` | AP reimbursement bills. Query by `claim_id`, `bill_id`. |32| `/api/ap/payments` | Payment records. Query by `bill_id`. |33| `/api/vendors` | Vendor master data. Query by `vendor_id`. |34| `/api/compliance/objects` | Compliance screening records. Query by `business_id`. |35| `/api/prepaids/invoices` | Prepaid invoice schedules. Query by `prepaid_invoice_id`. |36| `/api/prepaids/gl-balances` | GL ending balances by account/period/entity. Query by `account`, `period`, `entity`. |37| `/api/close/logs` | Close/review log entries. Query by `period`, `area`, `related_account`. |38| `/health`, `/endpoints` | Service health and endpoint listing. |3940### Data Field Reference4142**Claims** (`/api/claims`):43- `claim_id`, `amount` (USD), `status` (submitted/needs_receipt/approved/paid/rejected)44- `approved_date`, `submitted_date`, `category`, `department`, `employee_name`45- `policy_flags` (array: `manual_rate`, `late_receipt`, `over_limit`, `weekend_spend`, `duplicate_amount`)46- `receipt_status` (attached/partial/missing), `vendor_id`, `notes`, `currency`4748**AP Bills** (`/api/ap/bills`):49- `bill_id`, `claim_id` (nullable — null means non-reimbursement bill), `vendor_id`50- `amount` (USD), `status` (scheduled/approved/paid/void), `account`, `currency`51- `bill_date`, `due_date`, `invoice_number`, `memo`5253**AP Payments** (`/api/ap/payments`):54- `payment_id`, `bill_id`, `vendor_id`, `amount` (USD), `method`55- `status` (scheduled/processing/cleared), `payment_date`, `bank_reference`5657**Vendors** (`/api/vendors`):58- `vendor_id`, `vendor_name`, `legal_name`, `status` (active/inactive/on_hold)59- `bank_account_last4`, `tax_id`, `industry`, `payment_terms`, `default_account`, `updated_at`6061**Compliance Objects** (`/api/compliance/objects`):62- `business_id`, `business_name`, `vendor_id`, `jurisdiction`, `registration_number`63- `bank_account_status` (verified/name_mismatch/closed)64- `pep_status` (none/possible_pep/confirmed_pep/not_run)65- `sanctions_check_status` (clear/possible_match/confirmed_match/not_run)66- `license_expiry` (date string), `missing_fields` (array)67- `risk_score` (integer 0-100), `review_status` (not_started/in_review/awaiting_information/approved/escalated)68- `shell_company_suspected` (boolean), `ownership_layer_count` (integer)69- `tax_id`, `ubo_list` (array of `{name, ownership_pct}`)7071**Prepaid Invoices** (`/api/prepaids/invoices`):72- `prepaid_invoice_id`, `account`, `description`, `vendor_id`73- `original_amount`, `monthly_amortization`, `recognition_method` (straight_line)74- `service_start`, `service_end`, `invoice_date`, `invoice_number`, `source_document`75- `data_quality_flags` (array: `rounded_amount`, `missing_contract_dates`, `manual_override`, `duplicate_invoice_number`)7677**GL Balances** (`/api/prepaids/gl-balances`):78- `account`, `account_name`, `entity`, `period` (YYYY-MM)79- `ending_balance`, `source`, `loaded_at`8081**Close Logs** (`/api/close/logs`):82- `log_id`, `period`, `area` (AP/Expense/Prepaids/GL/Treasury/Compliance)83- `status` (open/ready_for_review/closed/blocked), `message`, `owner`84- `related_account`, `created_at`8586---8788## Workflow 1: Claims-to-AP Close Review8990### Business Rules9192**Classification logic for each claim in the batch:**93941. **PAID** — ALL of the following must be true:95 - A bill exists linked to the claim (`bill.claim_id == claim.claim_id`)96 - The bill's `status` is `"paid"`97 - A payment exists for that bill with `status` `"cleared"`98 - The bill amount matches the claim amount (exact match)991002. **PAYABLE** (can stay in AP queue) — ALL of:101 - Claim `status` is `"approved"`102 - At least one bill exists linked to the claim103 - The bill amount matches the claim amount (exact match)104 - The bill `status` is NOT `"void"`105 - No blocking conditions apply1061073. **BLOCKED** — ANY of:108 - Claim `status` is NOT `"approved"` (and not already marked paid) — e.g., `submitted`, `needs_receipt`, `rejected`109 - No AP bill exists linked to the claim110 - All linked bills are `"void"`111 - Bill amount does NOT match claim amount (amount mismatch indicates wrong AP link)112 - Bill memo contains `"Duplicate check required"` with a mismatched amount113 - Claim has `receipt_status: "partial"` combined with policy issues (over_limit, late_receipt)114115**CRM-required classification (subset of blocked):**116- Claims needing **expense-case owner cleanup**: missing bill, partial receipt, policy flags (`over_limit`, `late_receipt`), claim not approved117- Claims needing **AP-link remediation**: amount mismatch between claim and bill, void bill, duplicate check memo118119**AP Open Balance Total:**120- Sum bill amounts for **payable claims only**121- Use the matching bill (same amount as claim) linked to the claim122- Report in USD cents (multiply by 100) or as a decimal with 2 decimal places (check template)123- Ignore void bills and mismatched bills in the balance computation124125**Batch Status:**126- `"blocked"` — any batch item is blocked127- `"open_payables"` — no blocked items, but valid unpaid AP bills remain128- `"ready_to_close"` — all items are paid, no open payables129130### Source Precedence1311. Current API claim record (authoritative for claim status/amount)1322. Current API bill records linked by claim_id (authoritative for bill status/amount)1333. Current API payment records linked by bill_id (authoritative for payment status)134135### Common Pitfalls136- **Multiple bills per claim**: A claim can have multiple bills (e.g., legacy + current). Match by amount equality to the claim. Ignore stale/legacy bills with different amounts.137- **Bill amount ≠ claim amount**: This is always a blocking mismatch — do not assume the bill is correct.138- **Void bills**: A void bill linked to a claim makes the claim blocked unless another valid paid bill+payment exists.139- **Processing payments**: A bill with a `processing` payment is NOT paid — payment must be `cleared`.140- **Scheduled payments**: A `scheduled` payment is NOT cleared — do not classify as paid.141142---143144## Workflow 2: Vendor Onboarding Release Control145146### Business Rules147148**Decision matrix per business:**149150| Condition | Decision |151|---|---|152| Any hard-stop flag present, OR vendor `on_hold`, OR review `escalated` | `escalate` |153| `review_status` is `in_review` or `awaiting_information`, no hard stops | `awaiting_information` |154| `review_status` is `approved` or `not_started`, no hard stops, no blocking issues | `approve` |155| `sanctions_check_status` is `possible_match` without hard stops | `awaiting_information` |156| `pep_status` is `possible_pep` without other hard stops | `awaiting_information` |157158**Hard-stop flag mapping (compliance → flag):**159160| Compliance Field Value | Hard-Stop Flag |161|---|---|162| `bank_account_status: "closed"` | `bank_closed` |163| `bank_account_status: "name_mismatch"` | `bank_name_mismatch` |164| `pep_status: "confirmed_pep"` | `confirmed_pep` |165| `license_expiry` < as_of_date (expired) | `expired_license` |166| `missing_fields` is non-empty (contains `"license"`, `"beneficial_owner_id"`, `"bank_statement"`, `"website"`, etc.) | `missing_required_documents` |167| `sanctions_check_status: "confirmed_match"` | `sanctions_confirmed` |168| `sanctions_check_status: "not_run"` | `screening_not_run` |169| `shell_company_suspected: true` | `shell_company_suspected` |170| Vendor `status: "on_hold"` | `vendor_on_hold` |171172**UBO reporting threshold: 25% ownership.**173174- Count unique UBO **names** across all ownership layers175- Aggregate multiple entries for the same name (sum their `ownership_pct`)176- Count the name if the aggregated percentage ≥ 25%177- `pep_status: "not_run"` does NOT trigger `screening_not_run` (that flag is for sanctions check not run)178179### Output Conventions180- All ID lists sorted **ascending** (lexicographic for business IDs, claim IDs, log IDs)181- Hard-stop flag lists sorted **alphabetically** by flag enum value182- Empty lists `[]` when no items, never `null`183- `overall_release_ready`: `true` only when ALL businesses in the batch have decision `"approve"`; `false` if any business is `"awaiting_information"` or `"escalate"`184- `follow_up_business_ids`: all business IDs whose decision is NOT `"approve"` (i.e., `"awaiting_information"` or `"escalate"`)185186### Common Pitfalls187- **`pep_status: "not_run"` ≠ `screening_not_run`**: `screening_not_run` refers to sanctions screening, not PEP screening. Only `sanctions_check_status: "not_run"` triggers `screening_not_run`.188- **`pep_status: "possible_pep"`** is NOT a hard stop — only `confirmed_pep` is.189- **License expiry date comparison**: Use the `as_of_date` from the batch payload. If `license_expiry < as_of_date`, the license is expired.190- **Vendor status `on_hold`** maps to the `vendor_on_hold` hard-stop flag, not a separate category.191- **Multiple UBO entries for same name**: Aggregate ownership percentages before checking the 25% threshold.192- **`missing_fields` containing `"license"`**: This is `missing_required_documents`, NOT `expired_license`. Only use `expired_license` when the license date has passed.193194---195196## Workflow 3: Prepaid Close Reconciliation197198### Business Rules199200**Amortization method: Straight-line monthly.**201202- `monthly_amortization` is provided in the invoice record — use it directly203- For March 2025 period: March is month index relative to `service_start`204 - If `service_start` is 2025-01-01: January=month 1, February=month 2, March=month 3205 - March amortization = `monthly_amortization` (one month's worth)206207**Cumulative amortization through March:**208- Count full months from `service_start` through the close period end209- If `service_start <= 2025-03-01`: count months from start through March inclusive210- `cumulative = monthly_amortization × months_elapsed`211212**Schedule ending balance:**213- `original_amount - cumulative_amortization_through_march`214215**Invoice-level fields:**216- `march_amortization`: the monthly amortization for March (same as `monthly_amortization` unless the service started/ended mid-March)217 - If `service_start` is after March 2025, amortization is 0218 - If `service_end` is before March 2025, amortization is 0219 - Otherwise, use the full `monthly_amortization` value220- `cumulative_amortization_through_march`: see formula above221- `ending_balance`: `original_amount - cumulative_amortization_through_march`222- `default_missing_term_flag`: `true` if `data_quality_flags` contains `"missing_contract_dates"` or `"manual_override"` or if the invoice is missing service dates223- `exception_flag`: `true` if `data_quality_flags` is non-empty (any quality flag is present)224225**Account-level rollup (for each account):**226- `selected_invoice_count`: number of scoped invoices in that account227- `original_amount_total`: sum of `original_amount` for all scoped invoices in that account228- `march_amortization_total`: sum of `march_amortization`229- `cumulative_amortization_through_march`: sum of cumulative across invoices230- `schedule_ending_balance`: `original_amount_total - cumulative_amortization_through_march`231- `gl_ending_balance`: from GL balances API for the account/period/entity232- `variance_amount`: `schedule_ending_balance - gl_ending_balance`233- `variance_flag`: `true` if `abs(variance_amount) > variance_threshold_abs` (from scope config)234235**Account status:**236- `"reconciled"` — `variance_flag` is false AND no invoice in the account has an exception flag AND no default/missing term flag237- `"variance_review"` — `variance_flag` is true (regardless of exceptions)238- `"requires_reconciliation"` — `variance_flag` is false BUT exceptions or default/missing terms exist239240**Default/missing term flag at account level:**241- `true` if ANY invoice in the account has `default_missing_term_flag: true`242243### Output Ordering244- `selected_invoice_ids`: same order as `prepaid_close_scope.json`245- `invoice_results`: same order as `prepaid_close_scope.json`246- `default_missing_term_invoice_ids`: ascending by invoice ID247- `exception_invoice_ids`: ascending by invoice ID248249### Common Pitfalls250- **Service period boundaries**: If `service_start` is mid-month (e.g., 2025-03-15), March is month 1. Full amortization applies for that partial month (no proration).251- **Months elapsed calculation**: Count calendar months from start through the close period. A service starting 2025-01-15 has January as month 1, February as month 2, March as month 3.252- **`data_quality_flags` drives exceptions**: Any non-empty flag array means `exception_flag: true`. The specific flag determines `default_missing_term_flag`.253- **GL balance lookup**: Match exactly on `account`, `period` (YYYY-MM), and `entity` from the scope config.254255---256257## Workflow 4: Stale AP Snapshot Reconciliation258259### Business Rules260261**Source precedence (always):**2621. Current API claims (authoritative for claim status/amount)2632. Current API bills (authoritative for bill status/amount)2643. Current API payments (authoritative for payment status)2654. Stale CSV snapshot (context only — do NOT use as system of record)266267**Eligibility (eligible_claim_ids):**268A claim can remain in the batch if:269- Claim `status` is `"approved"` (current API)270- At least one non-void bill linked to the claim exists with a matching amount271- No blocking conditions (see not_ready below)272273**Not ready (not_ready_claim_ids):**274- Claim `status` is not `"approved"` (e.g., `needs_receipt`, `submitted`, `rejected`)275- All linked bills are void276- Bill amount ≠ claim amount (amount mismatch)277- Claim has `receipt_status: "partial"` or `"missing"` with no cleared payment278- Bill is paid+cleared but for a different amount than the claim279280**AP balance computation:**281- For each candidate claim, find the matching non-void bill (same amount as claim)282- Subtract any cleared payment amount for that bill283- `ap_balance = bill_amount - sum(cleared_payment_amounts)`284- For claims with no bill or only void bills: balance is 0 (not in AP)285- For paid claims: balance is 0 (fully paid)286287**Stale snapshot corrections — map each claim to ONE correction:**288289| Current State | Correction |290|---|---|291| Payment exists in API (cleared/processing) but snapshot shows `none`/0 | `mark_in_flight_payment` |292| Bill+payment match claim amount, snapshot shows different bill or status | `replace_with_matched_paid_bill` |293| Bill amount or vendor differs between current API and snapshot | `exclude_amount_or_vendor_mismatch` |294| Bill status is `void` in current API | `ignore_void_bill` |295| Claim status is NOT `approved` in current API | `block_unapproved_claim` |296| Snapshot matches current API state (same bill status, same payment status) | `current_snapshot_ok` |297298**Close log requirements:**299- Check close logs API for entries where:300 - `area` is `"AP"` or `"Expense"`301 - `period` matches the relevant close period302 - `status` is NOT `"closed"` (i.e., `open`, `ready_for_review`, `blocked`)303- `close_log_required.required`: `true` if ANY non-closed AP/Expense log exists304- `close_log_required.ids`: list of matching log IDs, ascending305306**Batch status:**307- `"ready_to_send"` — all claims eligible, no stale corrections needed beyond `current_snapshot_ok`308- `"needs_ap_refresh"` — some claims eligible but snapshot corrections needed (stale data)309- `"blocked"` — any claim is not ready310311### Common Pitfalls312- **Snapshot is NOT authoritative**: Always cross-reference against current API. The snapshot can be outdated (bill status changed, payment arrived after snapshot).313- **One correction per claim**: Choose the most severe/descriptive correction that applies.314- **Multiple bills per claim**: A claim can have both a stale/legacy bill and a current paid bill. Match by amount equality to the claim amount.315- **`processing` vs `cleared` payment**: Only `cleared` payments reduce the AP balance. `processing` payments are in flight but not yet settled.316317---318319## Workflow 5: Account-Change Payment Release320321### Business Rules322323**Decision matrix per business (release/hold/escalate):**324325| Condition | Decision |326|---|---|327| Any hard compliance flag (see below), OR vendor `on_hold`, OR `risk_score >= 70` | `escalate` |328| `review_status` is `in_review` or `awaiting_information`, no hard flags, risk < 70 | `hold` |329| `review_status` is `not_started`, no hard flags, risk < 70 | `hold` |330| `sanctions_check_status: "not_run"`, no other hard flags | `hold` |331| `sanctions_check_status: "possible_match"`, no other hard flags | `hold` |332| `pep_status: "possible_pep"`, no other hard flags | `hold` |333| `review_status: "approved"`, no hard flags, risk < 70, sanctions clear, bank verified | `release` |334335**Bank mismatch check:**336- Compare the `requested_bank_last4` from the account-change event to the vendor's `bank_account_last4`337- If they differ, flag as bank mismatch AND check compliance `bank_account_status` for `name_mismatch`338339**Hard compliance flags (same mapping as Workflow 2):**340- `bank_account_status: "closed"` → escalate341- `bank_account_status: "name_mismatch"` → escalate342- `pep_status: "confirmed_pep"` → escalate343- `license_expiry < as_of_date` → escalate344- `missing_fields` non-empty → escalate345- `sanctions_check_status: "confirmed_match"` → escalate346- `sanctions_check_status: "not_run"` → hold (not escalate)347- `shell_company_suspected: true` → escalate348349**Specific output lists:**350- `bank_mismatch_ids`: business IDs where compliance `bank_account_status == "name_mismatch"`351- `invalid_tax_ids`: business IDs where the vendor `tax_id` does not match the compliance `tax_id`352- `expired_license_ids`: business IDs where `license_expiry < as_of_date`353- `review_queue_ids`: business IDs where decision is `hold` OR `escalate` (any non-release)354- `risk_score_override_flags`: business IDs where `risk_score >= 70`355356### Common Pitfalls357- **Tax ID validation**: Compare the tax_id from `/api/vendors` against the tax_id from `/api/compliance/objects`. Non-matching or non-standard format tax IDs (e.g., containing letters beyond the TIN prefix) mean the business belongs in `invalid_tax_ids`.358- **Risk score threshold of 70**: `>= 70` means `escalate` and inclusion in `risk_score_override_flags`.359- **Bank matching uses compliance data**: `bank_mismatch_ids` is populated from compliance `bank_account_status: "name_mismatch"`, not from comparing last4 digits. The last4 comparison from the account-change event is additional context.360- **`review_status: "escalated"`** in compliance means automatic `escalate` decision.361362---363364## Cross-Cutting Conventions365366### Amount Handling367- All amounts in **USD**368- Report to **2 decimal places** unless the template specifies USD cents (integer)369- When computing balances, sum exact values then round the final result to 2 decimals370371### Sort Order372- All ID lists: **ascending** lexicographic order (standard string sort)373- Hard-stop flag lists: **alphabetical** by enum value374- Invoice result lists: **same order as input scope file**375- Business ID lists: **ascending** by business_id376377### ID Formats378- Claims: `CLM-YYYY-NNNN` or `CLM-YYYY-CATEGORY-NNN`379- Bills: `AP-YYYY-NNNN` or `AP-YYYY-REIM-NNN`380- Payments: `PAY-YYYY-NNNN`381- Businesses: `BUS-YYYY-NNNN`382- Vendors: `VEN-NNNN`383- Prepaid invoices: `PPD-YYYY-NNNN` or `PPD-AUR-ACCT-XXX-NNN`384- Close logs: `CLOSE-YYYY-MM-NNN`385- Account-change tickets: `ACT-YYMMDD-NNN`386387### API Query Pattern3881. Query the relevant list endpoint with the exact-match field parameter3892. The API returns `{count, data[], endpoint, limit, offset, total}`3903. Use `limit` and `offset` for pagination when `total > count`3914. Filtering is exact-match only — query one ID at a time for precision392393### Data Quality Signals394- **`policy_flags` on claims**: Non-empty flags indicate data quality or policy issues — these affect CRM-required classification395- **`data_quality_flags` on prepaid invoices**: Non-empty flags trigger exception flags and may trigger default/missing term flags396- **`missing_fields` on compliance objects**: Non-empty means `missing_required_documents` hard stop397- **`memo` fields on bills**: Look for keywords like `"Duplicate check required"`, `"Imported from AP inbox"`, `"Accrual review"`, `"Partial receipt support noted"` — these signal potential issues398399### Error Recovery400- If an API endpoint returns empty results for a known ID, treat it as "no record exists" (not an error)401- If a claim has no linked bill, it has no AP representation — classify accordingly402- If a bill has no linked payment, treat the payment amount as 0