Balance approval
What it is
Before a supplier’s balance is released, treasury reviews it. A machine-learning model produces a recommendation and a set of risk signals; a human accepts, rejects or overrides it with a reason.
This is the last control before a payment enters a batch, so it is the screen with the densest information in the application — around 70 fields per row.
Where it lives
| Concern | Location |
|---|---|
Routes |
|
Component |
|
Contract |
|
Service |
|
Types |
|
Dialogs |
|
Styling |
|
|
Reach this screen via |
Endpoints
BalanceApprovalService on internalApiUrl:
| Endpoint | Purpose |
|---|---|
|
The review queue. Takes |
|
Queue-level counters ( |
|
Record a decision. |
|
The outcome-reason dictionary ( |
PendingReviewService (in processes/service/) hits the same pending_review
endpoints with a narrower signature — two services, one backend resource. See the
double-slash note in processes.
The IResultField wrapper
Every field in a queue row is wrapped, not bare:
export interface IResultField {
Value?: string | boolean | number;
Highlight: string;
}
Highlight is a server-supplied styling hint — the backend decides which cells to
draw attention to. So visual emphasis in this grid is data, not front-end logic. Note
the capitalised property names; they do not follow the app’s usual camelCase and are
not remapped.
BalanceHeaderClasses.Db = 'db' is the one front-end styling constant, marking
database-sourced header groups.
What a row shows
Roughly seventy fields per supplier, grouped by what they tell the reviewer:
| Group | Fields |
|---|---|
Identity |
|
Balances |
|
Marketplace exposure |
|
Receivables |
|
Arrears |
|
Marketplace performance |
|
Trend |
|
Context |
|
ML output |
|
Decision |
|
|
|
The ratio substitution rule
getData contains the one piece of business logic in the service:
if (item.hasOwnProperty('short_to_long_mov_avg_ratio_rcv') &&
item['short_to_long_mov_avg_ratio_rcv'].Value !== 0) {
mappedData.results[i]['short_to_long_mov_avg_ratio'] =
item['short_to_long_mov_avg_ratio_rcv'];
}
When a non-zero receivables-based ratio is present it overwrites the general short-to-long moving-average ratio. The reviewer sees the receivables variant in the general column, with no indication that a substitution happened.
|
This substitution is silent and only guards against |
The mapping only runs when the response has both total and results; otherwise the
raw response is passed through unchanged.
Recording a decision
setDecision(key, value, suppKey, mlKey, reason?) {
const data = { [key]: value, supplier_transfer_approval_ml_key: mlKey };
if (reason) { data['reasons'] = reason; }
return this.httpClient.put(`${apiUrl}pending_review/${suppKey}`, data);
}
The field being set is a parameter, so one method covers every decision column
(treasury_approved, rejected, notes, exceptions, …). mlKey scopes the
decision to a specific model evaluation, which is what prevents a decision being
applied to a superseded row.
Reasons come from the payment.approval.outcomeReason lookup. approval-notes-modal/
captures free-text notes and description-modal/ shows a field’s explanation.
Queue statistics
IReviewStatistics drives the header counters:
| Counter | Meaning |
|---|---|
|
Decision distribution — note that |
|
Amazon account-health gate results. |
|
Financial-model gate results. Note the inconsistent casing ( |
Testing seam
Unusually, this feature has three data sources:
-
BalanceApprovalService— the real one. -
balance-approval-fake-service.ts— a fake implementingIBalanceApproval. -
balance-approval-mock-data.ts— static rows.
The IBalanceApproval interface is what makes the substitution clean. It is a good
pattern to copy for other data-dense screens.
Related gates
Balance approval is one of several checks before money moves:
| Gate | Where |
|---|---|
Pending edit approval |
Supplier changes flagged |
Amazon health rules |
|
Batch review |
Per-payment approve/hold/cancel inside a batch — processes. |
Underwriting status |
|
Gotchas
-
Use
/proceses/approval, not/processes/approval. -
supercededandcancelation_statusare misspelled in the API contract; the interface matches the API, so keep the misspellings. -
ValueandHighlightare capitalised. -
The receivables-ratio substitution is silent.
-
The route at
/proceseshas noNgxPermissionsGuard; access control is enforced by the endpoints, not the route.