Skip to content

feat(ui): Volume Contracts configuration page - #369

Open
prajjwalkumar17 wants to merge 2 commits into
feat/volume-contract-dslfrom
feat/volume-contract-ui
Open

feat(ui): Volume Contracts configuration page#369
prajjwalkumar17 wants to merge 2 commits into
feat/volume-contract-dslfrom
feat/volume-contract-ui

Conversation

@prajjwalkumar17

@prajjwalkumar17 prajjwalkumar17 commented Aug 20, 2026

Copy link
Copy Markdown
Member

What

Dashboard UI for the volume-commitment contract DSL introduced in #367. New Volume Contracts entry in the sidebar's Routing section (beta badge) at /routing/volume-contracts, styled identically to the other routing pages and built from the existing UI kit (Card/InsetPanel/Button/Badge/Combobox/SearchableSelect/ConfirmDialog, SWR + POST fetchers, routing:write permission gating). Stacked on the #367 branch; merge that first.

  • Builder covering the full DSL (parameter reference below), with the archetype-specific term blocks switching by archetype and the "exactly one targeted retroactive tier" rule enforced directly in the form (radio per contract; marginal tiers can't be targeted).
  • Document list with Active/Inactive badges, expandable stored-JSON view (shows the canonicalized document), and activate / deactivate / delete lifecycle with confirm dialogs — same flow as the Rule-Based page.
  • types/api.ts gains VolumeContractConfig types and volume_contract in the algorithm type unions; EuclidRulesPage now excludes volume-contract documents from its rules list (they have their own page).

No new endpoints: everything rides POST /routing/{create,list,list/active,activate,deactivate,delete} with algorithm_for: volume_commitment and algorithm: { type: "volume_contract", data: … }.

DSL parameter reference

Document (merchant-level)

Field Type Required Default Notes
schema_version int no 1 only 1 writable
routing_mode pace_guarded | volume_commitment yes Mode 1 / Mode 2
tolerance "5pp" | "550bps" | int bps yes stored as tolerance_bps; ≤ 2000 bps
metric gmv | volume no gmv one metric per merchant
currency.denomination ISO-4217 yes one currency per document
currency.amount_units major | minor no minor input units; stored canonicalized to minor
expected_daily_traffic int | decimal string yes total across PSPs, > 0
forecast_interval_secs int no engine default 60–604800
steering_interval_secs int no engine default 60–604800
volume_contracts list yes 1–50 entries

Per-PSP contract

Field Type Required Default Notes
id string yes [A-Za-z0-9_-]{1,64}, unique in document
connector string yes exact gateway name routing uses
status active | inactive no active one active contract per connector
billing_cycle.type calendar_month | calendar_quarter | calendar_year yes
billing_cycle.anchor int yes 1–30 / 1–3 / 1–12 by cycle type
billing_cycle.timezone IANA string yes validated against tz database
billing_cycle.proration full_period no full_period
archetype lumpsum | tiered (| min_commitment) yes B parses but is gated off in v1
scope reserved future euclid-condition cluster scoping; must be absent

lumpsum terms (archetype A)

Field Type Required Notes
target amount yes GMV/count that unlocks the reward, > 0
reward.kind flat | percentage yes tagged enum — mutual exclusion on the wire
reward.value.flat_amount amount iff flat > 0
reward.value.rebate_bps int iff percentage 1–10000

tiered terms (archetype C) — per tier

Field Type Required Default Notes
kind retroactive | marginal yes retroactive pays on whole period, marginal above threshold
rate.rebate_bps / rate.rate_bps int yes field pairs with kind; 1–10000
threshold amount yes strictly increasing across tiers
targeted bool one per contract false the goal tier the engine steers for; must be retroactive
rebate_lag_days int no 0 ≤ 365
rebate_settlement cash | credit_note no cash

Amounts are JSON integers or decimal strings in the declared amount_units; the backend stores everything canonicalized to integer minor units. No derived values (goals, rewards, period ends) are computed by the UI or stored — resolution stays the engine's job.

Verified locally

Against a live backend (postgres) + vite: logged in, created a document from the form ($6M target / $15k flat / $800k daily traffic in major units, 5pp tolerance), confirmed the stored JSON came back canonicalized (tolerance_bps: 500, target: 600000000, expected_daily_traffic: 80000000, amount_units: minor), activated it (Active badge, mapper slot), deactivate/delete guarded by confirm dialogs. tsc --noEmit and npm run build green; website/dist regenerated per repo convention.

Screenshot 2026-08-20 at 16 43 53

prajjwalkumar17 and others added 2 commits August 20, 2026 15:55
Adds a Volume Contracts page (Routing section of the sidebar, beta badge)
for configuring volume-commitment contract documents through the existing
/routing/* endpoints with algorithm_for: volume_commitment:

- Builder covering the whole DSL: routing mode, tolerance (pp), metric,
  currency + amount units, expected daily traffic, optional
  forecast/steering interval overrides, and per-PSP contracts with
  lumpsum (target + flat/percentage reward) and tiered (rebate ladder
  with a single targeted retroactive tier, enforced in the form)
  archetypes, billing cycle + IANA timezone.
- Document list with active badge, expandable stored-JSON view, and the
  activate/deactivate/delete lifecycle with confirm dialogs, mirroring
  the Rule-Based page's patterns (SWR + POST fetchers, cache
  revalidation, permission gating via routing:write).
- Reuses the existing UI kit (Card/InsetPanel/Button/Badge/Combobox/
  SearchableSelect/ConfirmDialog/typography) so the page matches the
  rest of the dashboard.
- types/api.ts: VolumeContractConfig types + volume_contract in the
  algorithm type unions.
- EuclidRulesPage now excludes volume_contract documents from its rules
  list (they live on their own page).

Verified against a local backend: create canonicalizes (5pp -> 500 bps,
major -> minor units), activate/deactivate round-trips, tsc + vite build
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SearchableSelect's compact cond-select trigger (built for the dense Euclid
condition builder) and unstyled Combobox inputs looked cramped next to the
regular form fields. SearchableSelect gains an opt-in triggerClassName prop
that swaps the compact trigger for full input styling (block-level, no 10rem
label truncation); existing call sites are untouched. The volume contracts
page passes its shared input class to every dropdown so selects, comboboxes
and text inputs render at identical height, padding and type size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant