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

/suppliers/:supplierKey/advances/*

Feature root

suppliers/supplier/supplier-advances/

Service

supplier-advances/service/supplier-advances.service.ts (internalApiUrl, mostly under supplier_instant_advance/)

Calculator store

supplier-advances/pending-offer/pending-offer-calculator/store/ — the calc slice

Calculator services

…/pending-offer-calculator/service/periods-calculations.service.ts, pending-offer-risk-rating.service.ts, calculator-data-fixture.ts

Own module

pending-offer-calculator/pending-offer-calculator.module.ts

Routes and screens

Route Screen

advances ('' child)

BaseAdvancesComponent — the overview: requested, approved, active advances and advance lines. Sub-folders requested-advances/, approved-advances/, approved-advance-lines/, active-advance/, underwritings/.

advances/new

NewOfferComponent — start a new offer.

advances/advance/:advanceKey

ActiveAdvanceDetailsComponent.

advances/advance-line/:advanceLineKey

ActiveAdvanceLineDetailsComponent.

advances/pending-request/:requestKey

PendingOfferComponent — the pricing screen, host of the calculator.

advances/ml-offer/:mlOfferKey

OfferEligibilityDetailsComponent — the ML-generated offer.

advances/progressive-details[/:advanceOfferRequestKey]

InstantAdvanceUnderwritingComponent.

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

POST supplier_instant_advance/:key/create_request

Create an offer request.

GET supplier_instant_advance/:key/advance_request

The pending request.

GET supplier_instant_advance/:key/active_advance

The currently active advance.

GET supplier_instant_advance/:key/advance_liability

Outstanding liability.

GET suppliers/:key/supplier_advances

All advances for the supplier.

GET supplier_instant_advance/:key/ml_offer, …/ml_offer_details, supplier_instant_advance/offer_details

ML-generated offer and its detail.

GET supplier_instant_advance/:key/re_offer_eligibility, POST …/:key/re_offer

Re-offer eligibility and submission.

GET supplier_instant_advance/:key/requests_history

Past requests.

POST supplier_instant_advance/update_uw_request

Update the underwriting portion of a request.

POST supplier_instant_advance/update_offer_request

Update the priced offer.

POST supplier_instant_advance/:key/manually_update_request_uw_data/:advanceOfferRequestKey

Manual override of UW data on a request.

GET supplier_instant_advance/:key/advance_line_history, …/advance_line/:key/last, …/advance_line/:advanceLineKey

Advance-line history and current state.

GET suppliers/:key/advance_line/:advanceLineKey/uw_history, PATCH suppliers/:key/advance_line/:advanceLineKey/update

Advance-line underwriting history and update.

POST supplier_instant_advance/:proposedAdvanceLineKey/retract

Retract a proposed advance line.

ML offer eligibility

IEligibilityData (store/calculator-interfaces.ts) is what the model returns:

Field Meaning

pyb_eligibility

Payability’s computed eligibility figure.

offer_amount

The model’s recommended amount.

ranking

The model’s ranking band.

delivery_rate, cancellation_rate, order_defect_rate

Marketplace performance inputs.

days_ok_count

Count of days meeting the threshold.

no_data

True when the model had insufficient input — the UI must handle this rather than showing zeros.

ml_offer_key

Identifier used by the ml-offer/:mlOfferKey route.

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

totalPeriodLength

Σ periodLength

totalFee

Σ (fee × periodLength)

averageFee

totalFee / totalPeriodLength (0 if either is falsy)

rounding

standardRound(v, precision = 3) — Math.round(v × 10^p) / 10^p. Money conversions use precision 2; period/fee totals use the default 3.

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.

calculateApprovedAmount divides by 100 - totalFee with no guard. A totalFee of exactly 100 yields Infinity. In practice isTotalFeeIncorrect flags anything above MAX_TOTAL_FEE (40), so the UI blocks submission — but the computed value is still displayed.

Business limits

Declared as constants in calculator.reducers.ts:

Constant Value Rule

MIN_TOTAL_FEE

5

Total fee below 5 % → isTotalFeeIncorrect.

MAX_TOTAL_FEE

40

Total fee above 40 % → isTotalFeeIncorrect.

MAX_TOTAL_PERIOD_LENGTH

30

Total length above 30 → isTotalPeriodToLong.

MIN_PERIOD_COUNT

1

Cannot remove the last period (isRemoveDisabled).

MAX_PERIOD_COUNT

3

Cannot add a fourth period (isAddDisabled).

INITIAL_RESERVE_RATIO

25

Default repayment reserve percentage.

MAX_RESERVE_RATIO

40

Reserve input is clamped to [0, 40] — values outside are silently corrected, not rejected.

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-data-fixture.ts and shipping the app.

Calculator state

CalculatorState (store/calculator-interfaces.ts) carries, besides the periods and amounts:

Field Meaning

requested, recommended

AdvanceOfferConnectedInputs — the supplier’s request and the system’s recommendation, each with givenAmount, grossAmount, netDeliveryAmount, reserveAmount, riskRanking plus human-readable description* strings.

approvedAmount, netDeliveryAmount, reserveForRepayments

The offer being built.

gracePeriodEndDate / gracePeriodSavedEndDate

A repayment grace period, tracked as current-vs-saved so the UI can show unsaved changes.

noRebate / noRebateSaved

Suppress rebate.

returnStrategy / returnStrategySaved

Default 'no_return'.

forceFull / forceFullSaved

Default true — force full repayment.

expectedMonthlyEarnings, expectedDuration

Inputs to duration expectations.

uwStatus

UwStatus, initial PENDING — links pricing to underwriting.

isReadonly, isTouched, isNewOffer

UI mode flags. SET_TO_READONLY / SET_TO_EDITABLE switch the first.

isEmptyFee, isEmptyPeriodLength, isTotalFeeIncorrect, isTotalPeriodToLong, isAddDisabled, isRemoveDisabled, isMpReserveForPaymentEmpty

Derived validation flags, recomputed by compute() on every period change.

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

CALCULATOR_RESET

Returns to initialState.

CALCULATOR_HARD_RESET

Returns to initialState but preserves defaultProgressiveAdvanceSettings — so the (expensive to obtain) pricing tables survive.

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

saveProgressivePricingOffer(state, supplierKey)

New offer → createOfferRequest (POST …/create_request).

updateProgressivePricingOffer(state, requestKey)

Existing offer → postRequest (POST …/update_offer_request), and overrides approvedAmount with +state.netDeliveryAmount before serialising.

On update, approvedAmount is replaced by netDeliveryAmount. On create it is not. The two paths therefore submit different semantics for the same field — verify against the backend contract before changing either.

Also note weeklyFeePercent is hardcoded to 1 and extraReservePercent is set from state.reserveForRepayments (a percentage) in prepareDataToSave.

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

calculatorReducers dispatches through a Map, and uses an isWithPayload(action) type guard. Adding an action means adding a calculatorMap.set(TYPE, handler) entry, not a case.

Clamping, not validating

Reserve ratio is silently clamped to [0, 40]. The operator’s typed value may differ from what is submitted.

CurrencyPipe inside the reducer

The reducer instantiates new CurrencyPipe('en-US') to build the description* strings. Locale is hardcoded and formatting happens in state, not the template.

Fixture-backed defaults

Default pricing per risk rating is a committed fixture, not a setting.

-1 means "no rating"

Not 0, not null. 0 is a valid rating that indexes ForEveryRiskRating[0].

reserveForRepayments is a percentage

Despite the name it holds a ratio (25 = 25 %), and it is submitted as extraReservePercent.