@baseworks/billing
v0.1.1
Published
Billing capability: typed HTTP client, event catalog, projections, and CLI plugin for billing-service (usage-based billing: metrics, plans, subscriptions, usage). Contract-first, versioned with the service.
Readme
@baseworks/billing
Typed client, event catalog, projections, and CLI plugin for billing-service — the per-org usage-based billing core (metrics, plans, subscriptions). Contract-first, versioned with the service.
pnpm add @baseworks/billingClient
Everything is org-scoped (X-Org-Id + identity token). Money is integer
cents throughout.
import { createBillingClient } from '@baseworks/billing'
const billing = createBillingClient({
baseUrl: process.env.BILLING_SERVICE_URL!,
token: identityJwt,
orgId: activeOrgId,
})
// 1. a billable metric
const apiCalls = await billing.createMetric({
code: 'api_calls', name: 'API calls', aggregation: 'sum', unit: 'request',
})
// 2. a plan — base fee + per-metric usage charges (all integer cents)
const pro = await billing.createPlan({
code: 'pro', name: 'Pro', currency: 'USD',
baseAmountCents: 4900, // $49.00 / month
charges: [{ metricCode: 'api_calls', unitAmountCents: 2 }], // $0.02 / call
})
// 3. subscribe a customer (customerId is a customers-service ref — validated,
// name snapshotted). Opens a one-month billing period.
const sub = await billing.createSubscription({ customerId: 'acct-42', planId: 'pro' })
await billing.listSubscriptions({ status: 'active' })
await billing.getSubscription(sub.id)
// 4. meter usage — idempotent by transactionId (re-sending is a no-op)
await billing.recordUsage({ subscriptionId: sub.id, metricCode: 'api_calls', quantity: 100, transactionId: 'req-1' })
await billing.recordUsage({ subscriptionId: sub.id, metricCode: 'api_calls', quantity: 250, transactionId: 'req-2' })
await billing.listUsage({ subscriptionId: sub.id })
// 5. rate the current period — base fee + Σ(aggregated usage × charge). No invoice.
const rating = await billing.rateSubscription(sub.id)
// → { baseAmountCents: 4900, lines: [{ metricCode: 'api_calls', quantity: 350, amountCents: 700 }], totalCents: 5600 }
// 6. close the period → finalized invoice (idempotent + resumable + locked).
// Re-running is safe: one invoice per subscription-period. --force closes the
// current period even before it ends (a real cron omits it and closes on due).
const report = await billing.closePeriods({ subscriptionId: sub.id, force: true })
// → { locked: true, closed: [{ subscriptionShortId, invoiceNumber, totalCents, status: 'closed' }], skipped: [] }
await billing.cancelSubscription(sub.id, 'downgraded')Errors throw BillingError (.status, .code): 409 code_taken,
400 unknown_metric (charge/usage references a missing metric),
404 customer_not_found, 404 plan_not_found, 404 subscription_not_found,
409 already_canceled, 409 subscription_canceled (usage on a canceled sub).
CLI plugin
import { buildBillingCommand } from '@baseworks/billing/cli'
program.addCommand(buildBillingCommand({ http, cliName: 'dtab' }))
// dtab billing | bill
// metrics|metric ls · create · get · update · archive
// plans|plan ls · create (--base, --charge metric=cents) · get · update · archive
// subscriptions|subs ls (--status) · create (--customer <ref> --plan <code>) · get · cancel
// usage record (--subscription --metric --quantity --txn) · ls · rate
// close [--subscription <ref>] [--force] [--due-in <days>]Service integration
import { PROJECTIONS, POLICIES } from '@baseworks/billing/projections'
import { MetricEvents, PlanEvents, SubscriptionEvents, UsageEvents, CloseEvents } from '@baseworks/billing/events'Projections metrics → read_metrics, plans → read_plans,
subscriptions → read_subscriptions, usage → read_usage,
closes → read_closes. Tenant = org id. Usage streams are keyed by
sha256(subscriptionId:transactionId) and close markers by
sha256(close:subscriptionId:period) — the read-model row collapses duplicates, so
dedup (and one-invoice-per-period) holds even if two runs race.
