WIP — merchant profile CRUD endpoints + tier-cap wire-up (part 4)

Backend is now feature-complete for :52. Admin UI still has to consume
these endpoints (part 5) but every operation the UI needs has a
working API surface behind it.

api/merchant_profiles.rs (new module)
  Axum handlers wrapping the merchant_profiles::* business-logic helpers
  and the rail-preference repo helpers. Each endpoint writes an audit
  entry so the operator can see every profile/rail-preference change
  in the audit log.

    GET    /v1/admin/merchant-profiles                                    list + summarize
    POST   /v1/admin/merchant-profiles                                    create (tier-gated)
    GET    /v1/admin/merchant-profiles/:id                                detail + providers + rail prefs + counts
    PATCH  /v1/admin/merchant-profiles/:id                                partial update
    DELETE /v1/admin/merchant-profiles/:id                                refuses if attached
    POST   /v1/admin/merchant-profiles/:id/set-default                    transactional flip
    PUT    /v1/admin/merchant-profiles/:id/rail-preferences/:rail         validates + persists
    DELETE /v1/admin/merchant-profiles/:id/rail-preferences/:rail         clears the override

  set_rail_preference validates THREE things before persisting: rail
  name is one of lightning/onchain/card; the provider exists; the
  provider is attached to THIS profile; AND it serves this rail. So
  the operator can't pin "Card" to a BTCPay row, and can't pin a
  provider that belongs to a different profile.

  list/get redact SMTP password (smtp_configured: bool is enough for
  the UI to render "configured/not configured" status; the actual
  password stays write-only). The edit form submits a new password
  only when the operator explicitly rotates it.

api/tier.rs
  New enforce_merchant_profile_cap helper. Refuses with HTTP 402
  AppError::PaymentRequired when a Creator-tier operator already has
  one profile (the default) and the self-license lacks the new
  `unlimited_merchant_profiles` entitlement. Same shape as the
  existing enforce_product_cap / enforce_policy_cap helpers — the
  admin UI's existing tier-cap modal renders the upgrade CTA from
  the upgrade_url field.

  Note: master Keysat's Pro and Patron policies need
  `unlimited_merchant_profiles` added to their entitlement JSON as a
  separate admin action on the master keysat.xyz instance — purely
  data, no code change. Master operator self-license must be re-
  issued (or naturally renewed) to pick up the new entitlement.

merchant_profiles.rs
  create() now calls enforce_merchant_profile_cap before INSERT.
  Replaces the TODO comment from part 1.

api/mod.rs
  Registers the merchant_profiles module and wires the routes above.

Build: cargo check passes. Two warnings remaining — both expected:
  - recover.rs unused-import (pre-existing, unrelated)
  - SETTING_ACTIVE_PROVIDER inside the shim's own pre-migration
    fallback branch

Backend status: every multi-provider story (purchase routing,
subscription snapshot, webhook delivery, connect/disconnect, profile
CRUD, tier gating) is now wired to the new schema. Only the admin UI
+ a version bump remain.

What's left for :52:
  - Admin UI in web/index.html — Merchant Profiles section, product
    picker, buy-page brand block + rail picker. Roughly 600-1000 lines
    of HTML/CSS/JS consuming the new endpoints. Largest single
    remaining piece.
  - Version bump to :52 + release notes flagging the one-way migration
    + the post-migration manual Zaprite-webhook-URL update.
  - End-to-end sandbox test against two profiles + two Zaprite orgs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Grant
2026-06-03 22:48:54 -05:00
parent 9df1908328
commit 89f1b89705
4 changed files with 401 additions and 6 deletions
+22 -1
View File
@@ -75,6 +75,7 @@ pub mod tier;
pub mod validate;
pub mod community;
pub mod db_info;
pub mod merchant_profiles;
pub mod payment_provider;
pub mod rates_admin;
pub mod recover;
@@ -395,11 +396,31 @@ pub fn router(state: AppState) -> Router {
// model providers attach to profiles and products pick a profile
// at resolution time; there's no singleton "active" preference to
// flip. Multi-profile operators should use the new
// /v1/admin/merchant-profiles endpoints instead.
// /v1/admin/merchant-profiles endpoints below.
.route(
"/v1/admin/payment-provider/status",
get(payment_provider::status),
)
// Merchant profile CRUD + rail preferences.
.route(
"/v1/admin/merchant-profiles",
get(merchant_profiles::list).post(merchant_profiles::create),
)
.route(
"/v1/admin/merchant-profiles/:id",
get(merchant_profiles::get)
.patch(merchant_profiles::update)
.delete(merchant_profiles::delete),
)
.route(
"/v1/admin/merchant-profiles/:id/set-default",
post(merchant_profiles::set_default),
)
.route(
"/v1/admin/merchant-profiles/:id/rail-preferences/:rail",
axum::routing::put(merchant_profiles::set_rail_preference)
.delete(merchant_profiles::clear_rail_preference),
)
// Zaprite webhook landing — operator points Zaprite's
// webhook setting at this URL. Same handler as
// /v1/btcpay/webhook because the underlying validate_webhook