export type PriceRule = | { kind: 'free' } | { kind: 'fixed'; fixedCents: number } | { kind: 'variable'; baseCents: number; perKbCents: number; perMbCents: number }; export interface TierConfig { id: string; name: string; multiplier: number; monthlyCreditCents: number; defaultRule?: PriceRule; } export interface RateCard { endpoints: Record; } export interface TierCatalog { find(id: string): TierConfig | undefined; list(): TierConfig[]; } export class ConfigTierCatalog implements TierCatalog { constructor(private tiers: TierConfig[]) {} find(id: string): TierConfig | undefined { return this.tiers.find((t) => t.id === id); } list(): TierConfig[] { return [...this.tiers]; } } export interface CallUsage { metadataBytes: number; attachmentBytes: number; } export interface Quote { endpointId: string; listCents: number; totalCents: number; breakdown: { baseCents: number; metadataCents: number; attachmentCents: number }; } export interface PricingContext { rateCard: RateCard; tiers: TierCatalog; } export interface PricingStore { getRateCard(): RateCard; getTiers(): TierConfig[]; upsertEndpoint(endpointId: string, rule: PriceRule): void; deleteEndpoint(endpointId: string): void; upsertTier(tier: TierConfig): void; deleteTier(tierId: string): void; } export const DEFAULT_RATE_CARD: RateCard = { endpoints: { status: { kind: 'free' }, 'storage-list': { kind: 'free' }, transform: { kind: 'fixed', fixedCents: 4 }, add: { kind: 'free' }, timestamp: { kind: 'fixed', fixedCents: 4 }, 'hash-lookup': { kind: 'free' }, receipt: { kind: 'free' }, storage: { kind: 'variable', baseCents: 10, perKbCents: 1, perMbCents: 50 }, }, }; export const DEFAULT_TIERS: TierConfig[] = [ { id: 'free', name: 'Free', multiplier: 1, monthlyCreditCents: 100 }, { id: 'pro', name: 'Pro', multiplier: 0.5, monthlyCreditCents: 1000, defaultRule: { kind: 'fixed', fixedCents: 8 }, }, { id: 'business', name: 'Business', multiplier: 0.25, monthlyCreditCents: 10000, defaultRule: { kind: 'fixed', fixedCents: 8 }, }, ]; export const DEFAULT_PRICING: PricingContext = { rateCard: DEFAULT_RATE_CARD, tiers: new ConfigTierCatalog(DEFAULT_TIERS), }; export class InMemoryPricingStore implements PricingStore { private endpoints: Record; private tiers: TierConfig[]; constructor(rateCard: RateCard = DEFAULT_RATE_CARD, tiers: TierConfig[] = DEFAULT_TIERS) { this.endpoints = { ...rateCard.endpoints }; this.tiers = [...tiers]; } getRateCard(): RateCard { return { endpoints: { ...this.endpoints } }; } getTiers(): TierConfig[] { return [...this.tiers]; } upsertEndpoint(endpointId: string, rule: PriceRule): void { this.endpoints[endpointId] = rule; } deleteEndpoint(endpointId: string): void { delete this.endpoints[endpointId]; } upsertTier(tier: TierConfig): void { const i = this.tiers.findIndex((t) => t.id === tier.id); if (i >= 0) this.tiers[i] = tier; else this.tiers.push(tier); } deleteTier(tierId: string): void { this.tiers = this.tiers.filter((t) => t.id !== tierId); } } export function quoteCall( pricing: PricingContext, tierId: string, endpointId: string, usage: CallUsage, multiplierOverride?: number, ): Quote { const tier = pricing.tiers.find(tierId); if (!tier) throw new Error(`Unknown tier: ${tierId}`); const rule = pricing.rateCard.endpoints[endpointId] ?? tier.defaultRule; if (!rule) throw new Error(`No price rule for ${tierId}/${endpointId}`); if (rule.kind === 'free') { return { endpointId, listCents: 0, totalCents: 0, breakdown: { baseCents: 0, metadataCents: 0, attachmentCents: 0 }, }; } let breakdown: Quote['breakdown']; if (rule.kind === 'fixed') { breakdown = { baseCents: rule.fixedCents, metadataCents: 0, attachmentCents: 0 }; } else { breakdown = { baseCents: rule.baseCents, metadataCents: Math.ceil(usage.metadataBytes / 1024) * rule.perKbCents, attachmentCents: Math.ceil(usage.attachmentBytes / (1024 * 1024)) * rule.perMbCents, }; } const listCents = breakdown.baseCents + breakdown.metadataCents + breakdown.attachmentCents; const multiplier = multiplierOverride ?? tier.multiplier; return { endpointId, listCents, totalCents: Math.round(listCents * multiplier), breakdown, }; }