@wtfalch/billing
v0.1.1
Published
Products, plans and subscriptions: a flat monthly plan per organisation plus metered usage rated from @wtfalch/ledger, converted to @wtfalch/payments' minor units at charge time. Net of MVA.
Readme
@wtfalch/billing
Products, plans and subscriptions: a flat monthly plan per organisation plus
metered usage rated from @wtfalch/ledger,
converted to @wtfalch/payments'
minor units at charge time. Amounts are net of MVA (Norwegian VAT) --
@wtfalch/invoicing adds MVA, this
package never computes it.
The library shape: one published package, no server, no database this
package owns a credential to. The host supplies a Database (the same one
wired into @wtfalch/ledger, since rating a period reads ledger's own
tables) and calls this package's plain functions.
Install
pnpm add @wtfalch/billing @wtfalch/ledger @wtfalch/payments@wtfalch/authz is an optional peer, scaffolded per the estate's usual
convention -- see src/catalogue.ts.
Usage
import { migrate as migrateBilling } from '@wtfalch/billing';
import { migrate as migrateLedger } from '@wtfalch/ledger';
// once, at startup, against the host's own Database:
await migrateLedger(db);
await migrateBilling(db);import { createPlan, createProduct, setPlanMeter } from '@wtfalch/billing';
const product = await createProduct(db, {
sellerOrganisationId: estateOrgId,
key: 'estate',
name: 'Estate plan',
});
const plan = await createPlan(db, {
productId: product.id,
key: 'pro',
name: 'Pro',
currency: 'NOK',
priceMicros: '299000000', // 299.00 NOK/month, net of MVA
});
// AI usage is billed at cost, plus a 10% markup, above a 50 NOK allowance:
await setPlanMeter(db, {
planId: plan.id,
meter: 'ai',
includedMicros: '50000000',
markupBp: 1000,
});import { createSubscription, createCharge, chargeToPaymentAmount } from '@wtfalch/billing';
const subscription = await createSubscription(db, {
planId: plan.id,
buyerOrganisationId: customerOrgId,
trialDays: 14,
});
// after the trial ends and the subscription is active (see below):
const charge = await createCharge(db, subscription.id, subscription.currentPeriodStart);
const amount = chargeToPaymentAmount(charge); // { currency: 'NOK', value: 29900 }
// hand `amount` to @wtfalch/payments' createPayment / chargeRecurringAgreementimport { activateSubscription, markPastDue, recordDunningAttempt } from '@wtfalch/billing';
// a payment provider's webhook reports the charge failed:
await recordDunningAttempt(db, { subscriptionId: subscription.id, succeeded: false, nextRetryAt: tomorrow });
await markPastDue(db, subscription.id);
// ...and later, a retry succeeds:
await recordDunningAttempt(db, { subscriptionId: subscription.id, succeeded: true });
await activateSubscription(db, subscription.id);Public surface
- Products and plans:
createProduct,getProduct,listProducts;createPlan,getPlan,setPlanMeter,listPlanMeters. - Subscriptions:
createSubscription,getSubscription, and the state machine --activateSubscription,markPastDue,cancelSubscription(trial → active → past_due ⇄ active, any non-cancelled status →cancelled, which is terminal). - Dunning:
recordDunningAttempt,dueDunningRetries-- a log and a query, not a scheduler. A@wtfalch/jobshandler sweepsdueDunningRetries, attempts a charge, and callsrecordDunningAttemptplusactivateSubscription/markPastDueitself; this package runs no schedule of its own. - Rating:
ratePeriod(read-only) andcreateCharge(rates and persists, idempotent per subscription/period). - Money:
minorUnitDecimals,toMinorUnits-- the one place ledger's integer micros become payments' integer minor units, rounding once, half up, per charge line. - Migrations:
migrate. - Authorization:
catalogue,resourceModule,checkBillingManage,checkBillingRead(billing:configure,billing:read).
What this package does not do
- Rate usage itself.
files,aiandvaletalready turn their own usage into money before it reaches a ledger row (files_rates/metering.ts,offerings.ts,priceBook); this package reads that already-priced usage from@wtfalch/ledgerand only adds an optional allowance/markup "on top" per plan. Duplicating those rate tables here would be a fourth copy of the same pricing. - Compute MVA.
@wtfalch/invoicingdoes. - Take the payment.
@wtfalch/paymentsdoes -- this package hands it aMoneyamount in minor units viachargeToPaymentAmount. - Run a schedule.
dueDunningRetriesis a query a@wtfalch/jobshandler calls; nothing here ticks on its own.
Status
v1 built: products, plans (with optional per-meter allowance/markup),
subscriptions (trial/active/past_due/cancelled, with dunning logging and a
due-retries query), period rating against @wtfalch/ledger usage, and the
micros→minor-units conversion. @wtfalch/ledger and @wtfalch/payments are
both merged on main in their own repos but not yet published to npm --
see docs/adr/0012.
Not published to npm itself -- publishing needs the user's npm 2FA, and in
any case waits on both of those.
