Commit Graph

2 Commits

Author SHA1 Message Date
Grant 89f1b89705 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>
2026-06-03 22:48:54 -05:00
Grant 04e0dcd591 WIP — merchant profile foundation (multi-provider payment model, part 1)
Lays the schema + types + resolution layer for the merchant-profile-aware
multi-provider model documented in plans/multi-provider-payment-model.md.
Does NOT yet migrate any existing call site — legacy `state.payment_provider()`
and the singleton config tables continue to work via deprecation shims so
the daemon keeps running unchanged on this checkpoint.

This commit is intentionally a WIP foundation, not a shippable release —
no version bump, no release notes, no admin UI, no call-site migration.
A follow-up cycle ports purchase / subscriptions / reconcile / upgrade /
tipping to the new resolution layer, rebuilds the BTCPay + Zaprite connect
flows around merchant_profile_id, refactors webhook URLs to
/v1/{kind}/webhook/{provider_id}, ships the Merchant Profiles admin UI
section, wires the tier-cap, and bumps to :52 with the one-way migration
release notes.

What landed:

migrations/0020_merchant_profiles.sql
  Full schema + data port + DROP of the singleton tables. Creates
  merchant_profiles, payment_providers (FK to profile, unique per
  (profile, kind)), merchant_profile_rail_preferences (tie-breaker
  when a profile has 2 providers serving the same rail). Adds
  merchant_profile_id to products + (merchant_profile_id, payment_provider_id)
  to subscriptions for the snapshot-on-create semantics. Ports
  btcpay_config + zaprite_config + active_payment_provider setting
  into the new tables, then drops them. Master operator post-migration
  step: update the Zaprite webhook URL on the Zaprite dashboard to
  the new /v1/zaprite/webhook/{provider-id} form (or click Reconnect
  Zaprite in the new UI once it ships).

src/merchant_profiles.rs (new module)
  MerchantProfile struct + NewMerchantProfile + MerchantProfileUpdate
  input types. Business-logic CRUD helpers: create, get, get_default,
  require_default, list, update, set_default, delete, for_product.
  Delete refuses if products or active subs are attached or if it's
  the default profile. Tier-cap check stubbed with a TODO for the
  next chunk's tier.rs wire-up.

src/db/repo.rs (+469 lines)
  Repo helpers: create/get_by_id/get_default/get_for_product/list/
  update/set_default/delete for merchant_profiles + count helpers
  for products/active_subscriptions per profile. PaymentProviderRow
  struct + create/get/list_for_profile/list_all/delete. RailPreference
  struct + list/set/clear helpers. update_merchant_profile builds a
  dynamic SET clause so partial updates don't clobber fields the
  caller didn't touch.

src/payment/mod.rs
  Rail enum (Lightning / Onchain / Card) + ProviderKind::parse +
  rails_for_kind static mapping. build_provider(row, public_base) ->
  Arc<dyn PaymentProvider> factory that dispatches on kind to construct
  a typed BtcpayProvider or ZapriteProvider from a payment_providers
  row. PaymentProvider trait gains a default served_rails() impl
  returning rails_for_kind(self.kind()).

  Deprecation shims: SETTING_ACTIVE_PROVIDER constant +
  read_active_provider_preference + write_active_provider_preference
  stay callable so btcpay_authorize/zaprite_authorize/main.rs/the
  thank-you page still build. read_active_provider_preference now
  reads from the new payment_providers table (returns the kind of
  the first provider attached to the default profile), falling back
  to the legacy settings-table read pre-migration. write_* is a no-op.
  Each shim has a #[deprecated] attribute so the build surfaces
  exactly which call sites still need porting (lit up in the
  follow-up cycle's TODO).

src/api/mod.rs (AppState)
  New methods alongside the existing payment_provider() shim:
    - payment_provider_by_id(id) — looks up a row, builds the provider
    - merchant_profile_for_product(product_id) — resolves via products.merchant_profile_id, falls back to default
    - resolve_provider_for_profile_rail(profile_id, rail) —
      preference table -> single candidate -> deterministic earliest-
      connected with WARN. Returns (row, Arc<dyn PaymentProvider>).
    - resolve_provider_for_product_rail(product_id, rail) — convenience
      wrapping the previous two.

src/lib.rs
  Registers the new merchant_profiles module.

Build state: cargo check passes. Only warnings are the pre-existing
unused-import in recover.rs and the deprecation lint firing on the
five legacy call sites enumerated in the WIP plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 22:00:00 -05:00