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:
@@ -221,6 +221,39 @@ pub async fn enforce_policy_cap(state: &AppState, product_id: &str) -> AppResult
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refuse a new merchant profile if the operator is at the Creator-tier
|
||||
/// merchant-profile cap (= 1) and lacks `unlimited_merchant_profiles`.
|
||||
/// Counts every profile including the auto-created default. So Creator
|
||||
/// operators have the default profile (auto-created by migration 0020)
|
||||
/// and can't add more; Pro and Patron operators are unlimited.
|
||||
///
|
||||
/// The `unlimited_merchant_profiles` entitlement needs to be added to
|
||||
/// the master Keysat's Pro and Patron policies as a separate admin
|
||||
/// action — see plans/multi-provider-payment-model.md "Tier gating"
|
||||
/// section.
|
||||
pub async fn enforce_merchant_profile_cap(state: &AppState) -> AppResult<()> {
|
||||
let tier = current(state).await;
|
||||
if tier.has("unlimited_merchant_profiles") {
|
||||
return Ok(());
|
||||
}
|
||||
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM merchant_profiles")
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
// Creator gets 1 (the default profile).
|
||||
if count >= 1 {
|
||||
return Err(AppError::PaymentRequired {
|
||||
message: format!(
|
||||
"Your {} tier allows a single merchant profile (the default). \
|
||||
You're at {}. Upgrade to Pro to run multiple businesses \
|
||||
from one Keysat instance.",
|
||||
tier.display_name, count
|
||||
),
|
||||
upgrade_url: UPGRADE_URL_PRO.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refuse to mark a policy as recurring unless the operator's self-tier
|
||||
/// carries the `recurring_billing` entitlement. Pro and Patron tiers
|
||||
/// have it; Creator does not. Called from both create-policy and
|
||||
|
||||
Reference in New Issue
Block a user