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

/processes/approval (shadowed — see below) and /proceses/approval

Component

processes/balance-approval/balance-approval.component.*

Contract

processes/balance-approval/balance-approval.ts — the IBalanceApproval interface

Service

processes/balance-approval/balance-approval.service.ts, plus balance-approval-fake-service.ts and balance-approval-mock-data.ts

Types

processes/balance-approval/balance-approval-interfaces.ts

Dialogs

approval-notes-modal/, description-modal/

Styling

models/balance-headers-classes.enum.ts

Reach this screen via /proceses/approval (one s). In the /processes table the :supKey wildcard is declared before approval, so /processes/approval renders PendingEditComponent instead. See processes for why.

Endpoints

BalanceApprovalService on internalApiUrl:

Endpoint Purpose

GET pending_review/

The review queue. Takes limit, offset and arbitrary filter keys copied from a DataTableQueryParams object (limit/offset are excluded from the copy so they are not duplicated).

GET pending_review/statistic

Queue-level counters (IReviewStatistics).

PUT pending_review/:supplierKey

Record a decision.

GET lookup/codes/payment.approval.outcomeReason

The outcome-reason dictionary (IOutcomeReason[]: codeValue, displayName, description).

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

supplier_key, supplier_name, supplier_transfer_approval_ml_key, last_txid, create_ts, update_ts, last_login_id, superceded (sic).

Balances

avail_balance, delta_avail, delta_check, last_end_balance, transfer_balance, current_transfer_balance, liability.

Marketplace exposure

pyb_due_from_mp_balance, mp_due_from_marketplace_balance, mp_due_from_mp_balance, due_from_mp_delta, mp_payments_7days, mp_pmt_excess_24.

Receivables

rcv_purchased_24, rcv_released_24, dilution.

Arrears

past_due_days, past_due_amount, past_due_check.

Marketplace performance

cancellation_rate, order_defect_rate, order_defect_value_short_term, late_responses, late_shipment_rate, late_shipment_value_30_days, ml_complaints.

Trend

short_to_long_mov_avg_ratio and its two variants …_rcv (receivables) and …_cbk (chargebacks).

Context

holidays, check_payee_diverted, payability_status, cancelation_status (sic).

ML output

ml_approved, ml_probability, ml_balance_approval, ml_balance_approval_details, ml_financial, ml_financial_reason, ml_order_defect, ml_cancellation, ml_late_shipment, ml_past_due, ml_ok_days, ml_ba_payments_decision, plus the previous-model comparison ml_balance_approval_old and ml_probability_old.

Decision

treasury_approved, treasury_approved_ts, treasury_approved_by, rejected, reasons, notes, exceptions, existing_exceptions.

ml_*old alongside ml* means the screen shows two model generations side by side — reviewers can see whether a new model would have decided differently. Do not remove the _old fields assuming they are dead.

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 0, not null/undefined `Value`s. If you are reconciling a displayed ratio against the raw API response, this is why they differ.

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

treasuryApprovedTrue / …False / …Reject / …None

Decision distribution — note that False and Reject are distinct: an explicit "not approved" versus a rejection.

amazonHealthPass / amazonHealthFail

Amazon account-health gate results.

financialMlPass / financialMLFail

Financial-model gate results. Note the inconsistent casing (Ml vs ML) between the two members — match the interface exactly.

Testing seam

Unusually, this feature has three data sources:

  • BalanceApprovalService — the real one.

  • balance-approval-fake-service.ts — a fake implementing IBalanceApproval.

  • 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.

Balance approval is one of several checks before money moves:

Gate Where

Pending edit approval

Supplier changes flagged pendApprovNeeded, reviewed in processes/pending-edit/.

Amazon health rules

underwriting.

Batch review

Per-payment approve/hold/cancel inside a batch — processes.

Underwriting status

UwStatus on the advance — advances.

Gotchas

  • Use /proceses/approval, not /processes/approval.

  • superceded and cancelation_status are misspelled in the API contract; the interface matches the API, so keep the misspellings.

  • Value and Highlight are capitalised.

  • The receivables-ratio substitution is silent.

  • The route at /proceses has no NgxPermissionsGuard; access control is enforced by the endpoints, not the route.