94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
import { sdk } from '../sdk'
|
||
import { configFile } from '../file-models/config.json'
|
||
|
||
const { InputSpec, Value } = sdk
|
||
|
||
// Operator-set per-tier monthly subscription prices, in USD. Used by
|
||
// the dashboard to compute revenue and operating margin (Gemini cost
|
||
// already comes out of the audit log). Pure accounting — the relay
|
||
// itself does no billing.
|
||
const inputSpec = InputSpec.of({
|
||
core_price: Value.number({
|
||
name: 'Core (Free) — Monthly Price',
|
||
description:
|
||
'Monthly subscription price for the Core tier in USD. Typically $0 since Core is the free entry tier. Used by the dashboard to compute total revenue; leave at 0 unless you actually charge for Core.',
|
||
required: true,
|
||
default: 0,
|
||
min: 0,
|
||
max: 10_000,
|
||
integer: false,
|
||
step: 0.01,
|
||
units: 'USD',
|
||
placeholder: null,
|
||
}),
|
||
pro_price: Value.number({
|
||
name: 'Pro — Monthly Price',
|
||
description:
|
||
'Monthly subscription price for the Pro tier in USD. Should match what you actually charge Pro customers on the licensing side.',
|
||
required: true,
|
||
default: 5,
|
||
min: 0,
|
||
max: 10_000,
|
||
integer: false,
|
||
step: 0.01,
|
||
units: 'USD',
|
||
placeholder: null,
|
||
}),
|
||
max_price: Value.number({
|
||
name: 'Max — Monthly Price',
|
||
description:
|
||
'Monthly subscription price for the Max tier in USD. Should match what you actually charge Max customers on the licensing side.',
|
||
required: true,
|
||
default: 15,
|
||
min: 0,
|
||
max: 10_000,
|
||
integer: false,
|
||
step: 0.01,
|
||
units: 'USD',
|
||
placeholder: null,
|
||
}),
|
||
})
|
||
|
||
export const setTierPrices = sdk.Action.withInput(
|
||
'set-tier-prices',
|
||
|
||
async ({ effects }) => ({
|
||
name: 'Set Tier Prices (USD)',
|
||
description:
|
||
'Configure the monthly USD price you charge per tier. The dashboard uses these numbers to compute revenue and operating margin against Gemini API cost. Has no effect on actual billing — it is for the operator’s accounting view only.',
|
||
warning: null,
|
||
allowedStatuses: 'any',
|
||
group: 'Tiers',
|
||
visibility: 'enabled',
|
||
}),
|
||
|
||
inputSpec,
|
||
|
||
async ({ effects }) => {
|
||
const config = await configFile.read().once()
|
||
let parsed: any = {}
|
||
try {
|
||
parsed = JSON.parse(config?.relay_tier_prices_usd_json || '{}')
|
||
} catch {
|
||
parsed = {}
|
||
}
|
||
return {
|
||
core_price: typeof parsed?.core === 'number' ? parsed.core : 0,
|
||
pro_price: typeof parsed?.pro === 'number' ? parsed.pro : 5,
|
||
max_price: typeof parsed?.max === 'number' ? parsed.max : 15,
|
||
}
|
||
},
|
||
|
||
async ({ effects, input }) => {
|
||
const prices = {
|
||
core: Number(input.core_price ?? 0),
|
||
pro: Number(input.pro_price ?? 5),
|
||
max: Number(input.max_price ?? 15),
|
||
}
|
||
await configFile.merge(effects, {
|
||
relay_tier_prices_usd_json: JSON.stringify(prices),
|
||
})
|
||
return null
|
||
},
|
||
)
|