user-stories
US-04 — Admin Section with Fees
US-04 — Admin Section with Fees
An admin changes a fee — either a platform-wide knob or one merchant's rate — and it applies to future transactions immediately. Every save is auditable.
Persona
Admin / ops — users.role = 'admin', gated server-side by requireAdmin.
Goal
"Drop this merchant's payout rate from 0.40% to 0.30%, and raise the flat send fee from €5 to €6 — both with a paper trail, without redeploying."
Two places fees live
The old tier model (user / silver / gold / platinum) is gone. There are now two independent surfaces:
| Surface | What it controls | Stored in |
|---|---|---|
/admin/fees |
Platform-wide knobs that apply to everyone | platform_fees (one row per named key) |
/admin/users/[id] |
One merchant's per-operation percentages | Columns on that merchant's users row |
Platform-wide knobs (/admin/fees)
| Key | Meaning | Default |
|---|---|---|
send_eur_minor |
Flat send fee, EUR cents | 500 (€5) |
invoice_open_fee_eur_minor |
Flat fee to open an invoice, EUR cents | — |
buy_external_pct |
On-ramp reconciliation rate | 1.5% |
payout_max_recipients |
Cap on recipients per payout batch | 100 |
Per-merchant rates (/admin/users/[id])
invoice_pct, payout_pct, api_pct, receive_pct, sell_pct, convert_pct, plus invoice_open_fee_eur_minor as a per-merchant override. Resolved at quote time by resolveMerchantOpRate.
Preconditions
- Admin is signed in and not blocked.
- Admin has access to the
/adminnav.
Happy path
- Admin opens
/admin/fees. - The page renders one form per knob, each showing its current value and label.
- Admin edits a value and submits that form.
- The server action re-checks
requireAdmin, then validates the input:- numeric, finite and ≥ 0 — otherwise redirect with
?error=invalid - percentages additionally capped at 100
payout_max_recipientsis additionally bounded by the hard ceiling inlib/payouts/limits.ts
- numeric, finite and ≥ 0 — otherwise redirect with
- On success the
platform_feesrow is updated withupdated_byset to the acting admin, and anaudit_logrow records the previous and next value. - Admin is redirected back with
?ok=1; the next quote uses the new value.
Changing one merchant's rate follows the same shape on /admin/users/[id], writing to that user's row instead.
Alternate flows
- Inspect history — filter
audit_logfor fee actions to see every change with actor and before/after. There is no dedicated search UI yet; see ../10-Roadmap#Beta. - Feature flags — the same user detail page toggles
orders_enabled,payouts_enabledandinvoices_enabled, each audited separately.
Edge cases & failure modes
- Concurrent edits — last write wins; both writes are audited, so the order stays reconstructible.
- Invalid input (negative, non-numeric, over cap) — rejected with
?error=invalid; nothing is written. - Mid-quote race — an invoice already
openedkeeps its persistedplatform_fee_minor. Fee changes are prospective only; see ../08-Risks and Drawbacks#Fees. - Admin self-block attempt — the console refuses to block another admin.
- Impersonating a blocked user — refused.
Acceptance criteria
- Only
requireAdmincallers can read or write/admin/fees. - Every save writes an
audit_logrow with the actor plus previous and next values. - Input validation is enforced server-side, not just in the form.
- Rate changes never retroactively affect already-frozen invoices.
- Per-merchant rate changes on
/admin/users/[id]are audited. -
audit_logis append-only at the database level (0012_audit_log_append_only).
Out of scope
- A public marketing-page fee schedule — see ../09-Open Questions#Fee schedule public commitment. Harder now that pricing is per-merchant rather than per-published-tier.
Drawbacks
- No undo on a fee change — the admin must read the audit row and re-apply by hand. ../08-Risks and Drawbacks#Admin
- No overview of non-standard pricing. Rates live on each user row, so there is no view of "which merchants are off the default". ../08-Risks and Drawbacks#Fees
- No bulk export of the audit log; investigators copy and paste. ../08-Risks and Drawbacks#Admin
Open questions
- ../09-Open Questions#Admin gaps — bulk export, refund/reverse, notification preferences
- ../09-Open Questions#Fee schedule public commitment
Implementing surfaces
- UI:
app/admin/fees/page.tsx,app/admin/users/[id]/page.tsx - Auth gate:
requireAdmin(lib/auth/guards.ts) - Fee resolution:
lib/fees/rates.ts—resolveMerchantOpRate,resolveSendFeeEurMinor,resolveBuyExternalRate - Schema:
platform_fees, per-op columns onusers,audit_log - Domain doc: ../03-Domains#Fees, ../06-Admin Console