Add self-serve billing: tiers, credits, BTCPay and Zaprite
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { configFile } from '../file-models/config.json'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
// Operator-editable bundle pricing for credit purchases. The bundle
|
||||
// SIZES are fixed (5, 10, 20 credits — three bundles fit cleanly in
|
||||
// the buyer modal's single row). Only their sats prices move via
|
||||
// this action. If you need different bundle sizes, edit
|
||||
// relay_credit_packages_json directly on the relay's data volume.
|
||||
const inputSpec = InputSpec.of({
|
||||
pkg_5_sats: Value.number({
|
||||
name: '5 credit bundle — price (sats)',
|
||||
description: 'Price the buyer pays for a 5-credit top-up.',
|
||||
required: true,
|
||||
default: 4000,
|
||||
min: 1,
|
||||
max: 100_000_000,
|
||||
integer: true,
|
||||
step: 1,
|
||||
units: 'sats',
|
||||
placeholder: null,
|
||||
}),
|
||||
pkg_10_sats: Value.number({
|
||||
name: '10 credit bundle — price (sats)',
|
||||
description: 'Price the buyer pays for a 10-credit top-up.',
|
||||
required: true,
|
||||
default: 6000,
|
||||
min: 1,
|
||||
max: 100_000_000,
|
||||
integer: true,
|
||||
step: 1,
|
||||
units: 'sats',
|
||||
placeholder: null,
|
||||
}),
|
||||
pkg_20_sats: Value.number({
|
||||
name: '20 credit bundle — price (sats)',
|
||||
description: 'Price the buyer pays for a 20-credit top-up.',
|
||||
required: true,
|
||||
default: 10000,
|
||||
min: 1,
|
||||
max: 100_000_000,
|
||||
integer: true,
|
||||
step: 1,
|
||||
units: 'sats',
|
||||
placeholder: null,
|
||||
}),
|
||||
})
|
||||
|
||||
const FIXED_CREDIT_SIZES = [5, 10, 20] as const
|
||||
|
||||
export const setCreditPackages = sdk.Action.withInput(
|
||||
'set-credit-packages',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'Set Credit Bundle Prices',
|
||||
description:
|
||||
'Per-bundle sats prices shown to buyers in the Recap credit-purchase modal. Bundle sizes (5, 10, 20 credits) are fixed by this action; edit relay_credit_packages_json directly if you need different sizes. Changes apply to the next buyer immediately — no daemon restart.',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: 'Tiers',
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
inputSpec,
|
||||
|
||||
async ({ effects }) => {
|
||||
const config = await configFile.read().once()
|
||||
// Translate the stored JSON array back into per-bundle inputs.
|
||||
// We only care about the four fixed sizes; anything else in the
|
||||
// stored array is preserved on save (see merge step below) but
|
||||
// not exposed in the form.
|
||||
let parsed: Array<{ credits: number; sats: number }> = []
|
||||
try {
|
||||
const raw = JSON.parse(config?.relay_credit_packages_json || '[]')
|
||||
if (Array.isArray(raw)) {
|
||||
parsed = raw
|
||||
.map((p: any) => ({
|
||||
credits: Number(p?.credits),
|
||||
sats: Number(p?.sats),
|
||||
}))
|
||||
.filter((p) => Number.isFinite(p.credits) && Number.isFinite(p.sats))
|
||||
}
|
||||
} catch {
|
||||
// ignored — fall back to defaults
|
||||
}
|
||||
const lookup = (n: number, fallback: number) =>
|
||||
parsed.find((p) => p.credits === n)?.sats ?? fallback
|
||||
return {
|
||||
pkg_5_sats: lookup(5, 4000),
|
||||
pkg_10_sats: lookup(10, 6000),
|
||||
pkg_20_sats: lookup(20, 10000),
|
||||
}
|
||||
},
|
||||
|
||||
async ({ effects, input }) => {
|
||||
const packages = [
|
||||
{ credits: 5, sats: Number(input.pkg_5_sats) },
|
||||
{ credits: 10, sats: Number(input.pkg_10_sats) },
|
||||
{ credits: 20, sats: Number(input.pkg_20_sats) },
|
||||
].filter(
|
||||
(p) =>
|
||||
Number.isFinite(p.sats) && p.sats > 0 && FIXED_CREDIT_SIZES.includes(p.credits as 5 | 10 | 20)
|
||||
)
|
||||
await configFile.merge(effects, {
|
||||
relay_credit_packages_json: JSON.stringify(packages),
|
||||
})
|
||||
return null
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user