Advances and offers
What it is
An advance is cash paid to a supplier ahead of marketplace remittance and repaid out of subsequent remittances. An advance line is the revolving version of the same facility.
Getting there means pricing an advance offer request: an amount, a risk rating, and a schedule of fee periods. The pricing arithmetic runs in the browser — this is the one area where the front end genuinely computes money numbers before submitting them.
|
The browser computes the proposal. The backend validates and books it. Everything on this page describes what the operator sees and submits, not the system of record. |
Where it lives
| Concern | Location |
|---|---|
Routes |
|
Feature root |
|
Service |
|
Calculator store |
|
Calculator services |
|
Own module |
|
Routes and screens
| Route | Screen |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
All standard-deny (REST_UW_ACCESS, ADVANCE_AUDITORS excluded) at the
advances parent.
The lifecycle
ML eligibility request underwriting
┌────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ ml_offer │────────►│ create_request │─────►│ update_uw_request│
│ ml_offer_details│ │ advance_request │ │ manually_update_ │
│ pyb_eligibility│ └─────────────────┘ │ request_uw_data │
│ ranking │ └────────┬─────────┘
└────────────────┘ │
▼
repayment / recovery booked ┌──────────────────────┐
┌────────────────────┐ ┌──────────────────┐ │ pricing (calculator) │
│ advance_liability │◄──│ active_advance │◄───│ update_offer_request │
│ requests_history │ │ advance_line │ │ periods, fee, reserve│
│ advance_line_history│ └──────────────────┘ └──────────────────────┘
└────────────────────┘ │
▼
┌──────────────────────────┐
│ re_offer_eligibility │
│ re_offer │
│ retract (proposed line) │
└──────────────────────────┘
Endpoints
All on internalApiUrl. Most sit under supplier_instant_advance/:
| Endpoint | Purpose |
|---|---|
|
Create an offer request. |
|
The pending request. |
|
The currently active advance. |
|
Outstanding liability. |
|
All advances for the supplier. |
|
ML-generated offer and its detail. |
|
Re-offer eligibility and submission. |
|
Past requests. |
|
Update the underwriting portion of a request. |
|
Update the priced offer. |
|
Manual override of UW data on a request. |
|
Advance-line history and current state. |
|
Advance-line underwriting history and update. |
|
Retract a proposed advance line. |
ML offer eligibility
IEligibilityData (store/calculator-interfaces.ts) is what the model returns:
| Field | Meaning |
|---|---|
|
Payability’s computed eligibility figure. |
|
The model’s recommended amount. |
|
The model’s ranking band. |
|
Marketplace performance inputs. |
|
Count of days meeting the threshold. |
|
True when the model had insufficient input — the UI must handle this rather than showing zeros. |
|
Identifier used by the |
Loading eligibility dispatches SET_ELIGIBILITY_DATA, which also seeds
netDeliveryAmount from offer_amount and the current total fee.
The progressive-pricing calculator
This is the calc state slice. It is unusual in two ways: it is a
map-of-handlers reducer (a Map from action type to handler function rather than a
switch), and it is the only slice with substantial arithmetic.
The model
Pricing is a list of periods, each with a length (weeks) and a weekly fee percentage. Up to three periods, giving a stepped fee schedule.
export class Period { periodLength: number; fee: number; }
Derived values (PeriodsCalculationsService):
| Value | Formula |
|---|---|
|
|
|
|
|
|
rounding |
|
The two money formulas
// approved → net delivered to the supplier
calculateNetDeliveryAmount(approvedAmount, totalFee) {
if (isUndefined(approvedAmount)) return undefined;
if (!totalFee) return approvedAmount;
return standardRound(((100 - totalFee) * approvedAmount) / 100, 2);
}
// net delivered → gross/approved (the inverse)
calculateApprovedAmount(netDeliveryAmount, totalFee) {
return standardRound((netDeliveryAmount / (100 - totalFee)) * 100, 2);
}
So totalFee is a percentage of the gross advance, deducted up front. The two
functions let the operator work from either end: type an approved amount and see the
net, or type a net and see the required gross.
|
|
Business limits
Declared as constants in calculator.reducers.ts:
| Constant | Value | Rule |
|---|---|---|
|
5 |
Total fee below 5 % → |
|
40 |
Total fee above 40 % → |
|
30 |
Total length above 30 → |
|
1 |
Cannot remove the last period ( |
|
3 |
Cannot add a fourth period ( |
|
25 |
Default repayment reserve percentage. |
|
40 |
Reserve input is clamped to |
SET_RESERVE_FOR_REPAYMENTS also sets isMpReserveForPaymentEmpty when the clamped
ratio is ⇐ 0.
Risk-rating defaults
PeriodsCalculationsService.getPeriodsFromDefaults(settings, riskRating) picks the
default period table:
riskRating < -1 → undefined (no defaults)
riskRating === -1 → settings.WhenNoRiskRating
riskRating >= settings.ForEveryRiskRating.length → undefined
otherwise → settings.ForEveryRiskRating[riskRating]
-1 is the sentinel for "no rating", and it is the initial value of
underwritingRiskRating. The settings themselves come from
PendingOfferRiskRatingService.getDefaultProgressiveAdvanceSettings(), which
currently returns the local fixture ProgressivePricingSettings from
service/calculator-data-fixture.ts rather than a backend call.
|
The default pricing tables are a committed front-end fixture, not configuration.
Changing default pricing per risk rating means changing
|
Calculator state
CalculatorState (store/calculator-interfaces.ts) carries, besides the periods and
amounts:
| Field | Meaning |
|---|---|
|
|
|
The offer being built. |
|
A repayment grace period, tracked as current-vs-saved so the UI can show unsaved changes. |
|
Suppress rebate. |
|
Default |
|
Default |
|
Inputs to duration expectations. |
|
|
|
UI mode flags. |
|
Derived validation flags, recomputed by |
The *Saved shadow fields exist because the offer is edited in place against a
persisted record; the pair lets the UI mark dirty fields without a separate form
state.
Reset semantics
Two resets, and the difference matters:
| Action | Effect |
|---|---|
|
Returns to |
|
Returns to |
SET_STATE_FROM_PERSISTED_DATA rehydrates from a saved offer: it runs compute()
over the persisted periods and sets the risk rating.
Submission
PendingOfferRiskRatingService.prepareDataToSave(state, requestKey?) builds a
ProgressivePricingOfferModel, sets SerializeKeysTo(UnderscoreCase) and serialises
it. Two paths:
| Method | Behaviour |
|---|---|
|
New offer → |
|
Existing offer → |
|
On update, Also note |
Instant-advance underwriting
pending-offer/pending-offer-calculator/pending-offer-progressive-details/instant-advance-underwriting/
(routed as advances/progressive-details[/:advanceOfferRequestKey]) is the screen
that pairs the calculator with the underwriting inputs, and the caller of
manually_update_request_uw_data.
advance-offer-inputs-modal/ is the dialog for editing the connected inputs
(requested/recommended amounts and their descriptions).
Offer status
OfferStatuses (store/calculator.enums.ts) contains a single member:
export enum OfferStatuses { PROCESSED = 'processed' }
Other offer statuses arrive as raw strings on IAdvanceOfferRequest.offer_status and
uw_status. Do not assume the enum is exhaustive.
Testing
store/calculator.spec.ts (~560 lines) is the most thoroughly tested unit in the
repository and is the reference for how to test a reducer here. If you change any
formula or limit, that spec is where it must be proven.
Gotchas
| Issue | Detail |
|---|---|
Map-of-handlers reducer |
|
Clamping, not validating |
Reserve ratio is silently clamped to |
|
The reducer instantiates |
Fixture-backed defaults |
Default pricing per risk rating is a committed fixture, not a setting. |
|
Not |
|
Despite the name it holds a ratio (25 = 25 %), and it is submitted as
|