04e0dcd591
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>
37 lines
1.0 KiB
Rust
37 lines
1.0 KiB
Rust
//! Keysat library — the daemon's internal modules, exposed as a library
|
|
//! so integration tests under `tests/` can drive the API directly without
|
|
//! re-implementing the bootstrap.
|
|
//!
|
|
//! The binary at `src/main.rs` is a thin wrapper that loads runtime config
|
|
//! from environment variables, starts the HTTP server, and spawns
|
|
//! background tasks. Tests bypass that wrapper and construct `AppState`
|
|
//! programmatically.
|
|
|
|
pub mod analytics;
|
|
pub mod api;
|
|
pub mod btcpay;
|
|
pub mod config;
|
|
pub mod crypto;
|
|
pub mod db;
|
|
pub mod error;
|
|
pub mod license_self;
|
|
pub mod merchant_profiles;
|
|
pub mod models;
|
|
pub mod payment;
|
|
pub mod rate_limit;
|
|
pub mod rates;
|
|
pub mod reconcile;
|
|
pub mod subscriptions;
|
|
pub mod tipping;
|
|
pub mod upgrades;
|
|
pub mod webhooks;
|
|
|
|
/// Hex-encoded SHA-256 of a string — used everywhere we need a deterministic
|
|
/// id from a raw value (machine fingerprints, admin key hashes).
|
|
pub fn hex_sha256(s: &str) -> String {
|
|
use sha2::{Digest, Sha256};
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(s.as_bytes());
|
|
hex::encode(hasher.finalize())
|
|
}
|