Platform and provider fees are data-driven fee groups on per-merchant plans
Context
Inkress earns a platform fee on transactions and passes through the acquirer's provider ("bank") fee, and different merchants are on different commercial terms — a merchant on a higher plan pays a lower platform rate, a merchant on a given processor pays that processor's rate. Hard-coding rates, or scattering per-provider rate columns, means every rate change is a code change and every new plan or processor touches the calculator. We also need fees to compose exactly with the rest of the checkout math (shipping, tax, discounts) rather than be bolted on.
Decision
Express platform and provider fees as data-driven fee groups run through the same TransactionCalculator fee-group engine as the rest of checkout (ADR 0002), with the rates living on per-merchant plans, not in code.
- Two more fee kinds in the one engine. Platform fee and provider ("bank") fee are ordered fee kinds in the same pipeline as discount/shipping/tax, applied after tax (provider fee last, so it applies to the grossed-up customer total). Fee totals are bucketed by kind, so "platform total" vs "provider total" stays distinct through to the result.
- Rates are data on plans. Fees are rows grouped into fee sets attached to a plan: platform-fee rates come from the merchant's billing plan, provider-fee rates from a provider plan (per processor). Checkout loads both plans' fee sets, concatenates them, and runs the calculator. Changing a rate, adding a plan, or onboarding a processor is a data change, not a deploy.
- A merchant is assigned a plan by an active subscription row. A merchant's plan is set by an active billing-subscription record pointing at a billing plan — so plan changes are data, and a merchant's effective rates are resolved at calc time.
- Who pays is a per-merchant switch. Whether the merchant absorbs a fee or the customer pays it is a per-merchant setting (
platform_fee_structure,provider_fee_structure) stamped onto each fee's payer at calculation time; when fees are offset to the customer, the customer total is grossed up so the merchant nets the sub-total.
Consequences
- Positive: fees compose exactly with discounts, shipping and tax because they are the same engine; rate/plan/processor changes are data, and platform vs provider amounts are separable everywhere.
- Positive: per-merchant commercial terms (plan tier, negotiated rates, who absorbs the fee) are configuration, not special-cased code.
- Neutral / to revisit: the effective fee for a checkout depends on resolving both the billing plan and the provider plan (and the merchant's fee-structure) at calc time — several inputs must be loaded and concatenated correctly for the total to be right.
- Neutral / to revisit: the older scalar rate columns on plans (a single percentage/flat field) are superseded by fee sets and now read only by migrators; and the separate Shopify-style
shop_plansfeature-flag concept is not implemented in the live code — "plan" here means the fee-set-bearing billing plan, not that catalogue.
Alternatives considered
- Hard-coded or per-provider rate constants: rejected (explicitly, in code history) — every rate change or new processor would be a deploy, and the calculator would grow a branch per provider; rates as fee-set data keep the engine closed and the rates open.
- A separate fee subsystem outside the checkout calculator: rejected — it would duplicate the ordering, rounding and currency handling the fee engine already owns and risk platform/provider fees disagreeing with the checkout total (same reasoning as ADR 0002 for discounts).
- A flat platform rate for everyone: rejected — merchants are on different commercial terms; plan- and processor-scoped fee sets express that without code.