v0.2.0:3 — durable payment-provider switching (Option B)
Closes the gap from :2 where Connect Zaprite swapped the
in-memory provider but BTCPay would silently re-take active on
the next daemon restart (because the boot-time loader picked
BTCPay first whenever btcpay_config was present, regardless of
operator intent).
What changed:
**New settings key `active_payment_provider`** in the existing
settings table. Records the operator's last explicit choice
('btcpay' | 'zaprite' | NULL = no preference). Both
btcpay_config and zaprite_config can coexist; the flag is what
determines which one the daemon loads.
**Boot-time loader respects the preference.** main.rs now reads
the flag at startup. If set to 'zaprite', Zaprite wins; if set to
'btcpay', BTCPay wins; if unset (legacy installs), falls back to
the previous BTCPay-first ordering. Cross-load fallbacks log a
WARN and try the other provider — operators with a stale flag
pointing at a wiped config don't boot unconfigured.
**Connect endpoints write the preference.**
- finish_connect (BTCPay) now sets the flag to 'btcpay' on
successful authorize-callback completion.
- ZapriteAuthorize::connect now sets the flag to 'zaprite' on
successful API-key validation.
- Both Disconnect endpoints clear the flag IF it pointed at the
provider being disconnected — but leave it alone if it pointed
at the OTHER provider (different operator intent).
**New endpoints for fast switching without re-Connect:**
- GET /v1/admin/payment-provider/status — both configs' state +
current preference + runtime active provider, in one call.
- POST /v1/admin/payment-provider/activate { provider: "btcpay" |
"zaprite" } — flips the active provider and the flag together,
without going through the full Connect flow. 400 if the named
provider isn't configured (operator must run Connect first).
**New StartOS Actions** under existing groups:
- "Activate BTCPay" (in BTCPay group)
- "Activate Zaprite" (in Zaprite group)
Both call the new activate endpoint. Operators with both
providers configured can flip back and forth in one click.
**Test:** payment_provider_preference_round_trip pre-seeds both
configs, walks through Activate-Zaprite → Activate-BTCPay →
attempt-Activate-on-wiped-config → bad-provider-name → manual
write/read of the preference key. Pins the contract.
Test count: 42 (was 41; +1).
Migration not needed — settings table from 0005 already has the
key/value/updated_at shape we need.
This commit is contained in:
@@ -40,6 +40,46 @@ use std::any::Any;
|
||||
pub mod btcpay;
|
||||
pub mod zaprite;
|
||||
|
||||
/// Settings-table key that records which provider the operator
|
||||
/// last activated. Used by the boot-time loader to pick which
|
||||
/// provider to load when both `btcpay_config` and `zaprite_config`
|
||||
/// are populated. Values: `'btcpay'` | `'zaprite'`. Absent means
|
||||
/// "use whichever single provider is configured" (back-compat
|
||||
/// for installs that pre-date this setting).
|
||||
pub const SETTING_ACTIVE_PROVIDER: &str = "active_payment_provider";
|
||||
|
||||
/// Convenience getter for the active-provider setting. Returns
|
||||
/// `Some(ProviderKind)` if the operator has explicitly chosen
|
||||
/// one, `None` if they haven't (caller falls back to the
|
||||
/// load-order heuristic).
|
||||
pub async fn read_active_provider_preference(
|
||||
pool: &sqlx::SqlitePool,
|
||||
) -> Option<ProviderKind> {
|
||||
match crate::db::repo::settings_get(pool, SETTING_ACTIVE_PROVIDER).await {
|
||||
Ok(Some(s)) => match s.as_str() {
|
||||
"btcpay" => Some(ProviderKind::Btcpay),
|
||||
"zaprite" => Some(ProviderKind::Zaprite),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist the operator's active-provider preference. Called by
|
||||
/// the connect endpoints (Connect BTCPay, Connect Zaprite) and
|
||||
/// by the new "Activate <provider>" endpoint that flips between
|
||||
/// already-configured providers without re-authorizing.
|
||||
pub async fn write_active_provider_preference(
|
||||
pool: &sqlx::SqlitePool,
|
||||
kind: ProviderKind,
|
||||
) -> anyhow::Result<()> {
|
||||
let value = kind.as_str();
|
||||
crate::db::repo::settings_set(pool, SETTING_ACTIVE_PROVIDER, Some(value))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("write active provider preference: {e:#}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProviderKind {
|
||||
|
||||
Reference in New Issue
Block a user