ec2b21d8f7
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.
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
// Switch the active payment provider WITHOUT re-running Connect.
|
|
// Use case: operator has both BTCPay and Zaprite configured (i.e.,
|
|
// they ran Connect on both at some point) and wants to flip which
|
|
// one currently handles purchases. Two convenience actions —
|
|
// "Activate BTCPay" / "Activate Zaprite" — each POSTs to the
|
|
// daemon's /v1/admin/payment-provider/activate endpoint.
|
|
//
|
|
// If the named provider isn't yet configured, the daemon returns
|
|
// 400 with a "Run Connect first" message; we surface that to the
|
|
// operator unchanged.
|
|
|
|
import { sdk } from '../sdk'
|
|
import { store } from '../fileModels/store'
|
|
import { adminCall, LICENSING_URL } from '../utils'
|
|
|
|
async function activate(provider: 'btcpay' | 'zaprite') {
|
|
const storeData = await store.read().once()
|
|
if (!storeData) throw new Error('Store not initialized — restart the service.')
|
|
const resp = await adminCall(
|
|
LICENSING_URL,
|
|
storeData.admin_api_key,
|
|
'/v1/admin/payment-provider/activate',
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({ provider }),
|
|
},
|
|
)
|
|
if (!resp.ok) {
|
|
throw new Error(`Activate failed: HTTP ${resp.status} — ${await resp.text()}`)
|
|
}
|
|
const body = (await resp.json()) as { ok: true; active: string }
|
|
return body
|
|
}
|
|
|
|
export const activateBtcpay = sdk.Action.withoutInput(
|
|
'activate-btcpay',
|
|
async () => ({
|
|
name: 'Activate BTCPay',
|
|
description:
|
|
'Switch the active payment provider to BTCPay. Use this if both ' +
|
|
'BTCPay and Zaprite are already connected and you want to flip ' +
|
|
"which one handles new purchases. Existing license keys aren't " +
|
|
'affected by the swap.',
|
|
warning: null,
|
|
allowedStatuses: 'only-running',
|
|
group: 'BTCPay',
|
|
visibility: 'enabled',
|
|
}),
|
|
async () => {
|
|
const body = await activate('btcpay')
|
|
return {
|
|
version: '1',
|
|
title: 'BTCPay is now the active provider',
|
|
message:
|
|
`Active payment provider is now ${body.active}. New purchases ` +
|
|
`route through BTCPay. Zaprite remains configured but inactive ` +
|
|
`until you run "Activate Zaprite" or "Disconnect Zaprite".`,
|
|
result: null,
|
|
}
|
|
},
|
|
)
|
|
|
|
export const activateZaprite = sdk.Action.withoutInput(
|
|
'activate-zaprite',
|
|
async () => ({
|
|
name: 'Activate Zaprite',
|
|
description:
|
|
'Switch the active payment provider to Zaprite. Use this if both ' +
|
|
'BTCPay and Zaprite are already connected and you want to flip ' +
|
|
"which one handles new purchases. Existing license keys aren't " +
|
|
'affected by the swap.',
|
|
warning: null,
|
|
allowedStatuses: 'only-running',
|
|
group: 'Zaprite',
|
|
visibility: 'enabled',
|
|
}),
|
|
async () => {
|
|
const body = await activate('zaprite')
|
|
return {
|
|
version: '1',
|
|
title: 'Zaprite is now the active provider',
|
|
message:
|
|
`Active payment provider is now ${body.active}. New purchases ` +
|
|
`route through Zaprite. BTCPay remains configured but inactive ` +
|
|
`until you run "Activate BTCPay" or "Disconnect BTCPay".`,
|
|
result: null,
|
|
}
|
|
},
|
|
)
|