@codelitdev/billing

中文文档
0.8.2发布于 2026/11/09MITTypeScriptES Module构建来源已验证近 30 天下载 2970

CodeLit产品供应商中性计费引擎

中文使用文档
文档目录15 节
  1. 目的
  2. 消费者准则
  3. 安装
  4. 如何融为一体
  5. 1. 说明计划a ( billing.config.ts (中文(简体) )
  6. 2. 制作引擎
  7. 3. 启动时的目录
  8. 4. 人类收费行动
  9. 5. 网络用户和工人
  10. 6. 阅读模型
  11. 7. 运营商CLI
  12. 8. 测试
  13. 此软件包不会做什么
  14. 版本
  15. 开发此 repo

@ codelitdev/ billing (英语)

CodeLit产品提供商中性计费引擎. 它运行 内部 您的 API 进程针对  PostgreSQL数据库. 这不是一个托管的收费服务.

版本 0.1.0-α.3 导弹工作流程、行动赠款、运营商服务和生命周期钩 实验. 在第二个消费者(当然Lit)证明同一合同之前没有v1分期付款.

目的

CodeLit产品通过付款机构销售自己的SaaS(组织,学校,后为其他总量). 如果没有一个共享的引擎,每个产品都倾向于发展自己的目录,取出,网络呼和调节——这些副本在容易出错的部分上存在分歧.

这个包使这些力学标准化:

  • 无法更改的版本价格目录与供应商核对
  • 离职、客户门户、取消和计划变更
  • Webhook 校验、 耐用收件箱、 检索当前项目
  • 订阅过渡、材料预测和对账租赁
  • 供应商安全公共目录/商业状态读取模型
  • 假的,多多,和柠檬斯克齐适配器 在一个合同后面

您的产品仍然拥有:

  • 谁收帐(组织、学校、.)
  • 计划名称、应享权利、配额、审判、优待、可用性
  • 计划的改变是立即的还是延长的,如何按比例分配
  • Auth, CSRF, 近代代号, HTTP 路由, UI
  • Schema 迁移、回填、加密密钥、审计存储、提醒

软件包在安装、导入或处理启动时从未运行 DDL .

消费者准则

发动机为机械. 它不会从计划名称中推断出产品政策.

你决定,然后通过:

决定你怎么告诉引擎
支付报价offers + 键 requiredOfferKeys 时间 createBilling; 准确设定, 不 "free" 内 planIds
审判资格trialDays 退出(如果不符合资格,则为0)。 要求,HMAC,学校审判留在你的桌子上
升级与降级startPlanChange({ effectiveAt, prorationMode })。软件包不给Pro/Business排序
计划怎么解锁你的权利解决者. commercialState() 只支付快照
实体生命周期getBillableEntityBillingBlockers 编号 : getPayerBillingResponsibilities;您选择了面向用户的错误
产品副作用afterProjection 编辑 引擎交易 (单位:千美元)store.getTransaction() ?? db(中文(简体) )

更改计划标记引擎所理解的:

  • effectiveAt数字 : immediately | next_billing_date
  • prorationMode数字 : prorated_immediately | do_not_bill

声明提供者 intervalChangesBillImmediately 当计费间隔改变时, 开始并开出新账, 所以 do_not_bill 跨间隔失败 plan_change_not_supported 在提供方被调用之前。 声明提供者 immediatePlanChangesOnly 无法安排更改, 因此 next_billing_date 失败的方式相同.

典型的SaaS拆分(SendLit做什么,不需要):升级 → 即时+ 按比例;降级 → 下一张发票 + 不收帐. 直到一个网约车/和解快照 实际显示新产品, commercialState().activePaidPlan 留在当前计划中 pendingPlanChange 已经设定。 您的UI应该禁用第二次更改, 并说“ 被确认” 与“ 预定” .

不许 :

  • 将应用程序Free/OSS/trial输入生成 planIds
  • 拿着应用程序 db.transaction() 穿过一个服务商呼叫,或巢 db.transaction() 在内部的根池上 afterProjection
  • 处理退出方向, payment.succeeded,或敬酒作为权利
  • 重新解释路由中的供应商 SDK 错误; 映射 BillingWorkflowError.code 仅限
  • 读取或写入计费状态 billing.store;它是发动机的内部

安装

bun add @codelitdev/[email protected]
# or: bun add @codelitdev/billing@alpha
# Dodo is an optional peer of the Dodo adapter
bun add dodopayments

公共进口( 不导入) src/:

子路径使用
@codelitdev/billing/configdefineBillingConfig
@codelitdev/billing/drizzlecreateDrizzleBillingStore
@codelitdev/billing/workflowscreateBilling (实验性)
@codelitdev/billing/operations操作员检查/ 重试/ 取消
@codelitdev/billing/providers登记、假合同类型
@codelitdev/billing/providers/dodocreateDodoBillingProvider
@codelitdev/billing/providers/lemonsqueezycreateLemonSqueezyBillingProvider
@codelitdev/billing/core状况、金钱、错误、时钟
@codelitdev/billing/catalog提供验证/ 公共目录
@codelitdev/billing/testing供应商合同+工作流程利用

CLI: 爱丽丝

bun x codelit-billing generate --config billing.config.ts
bun x codelit-billing generate --check --config billing.config.ts

如何融为一体

Your API / UI
  auth, grants, plan policy, entitlements
       │
       ▼
  createBilling  ──► catalog + state machines
                 ──► provider adapter (Dodo / fake)
                 ──► Drizzle store ──► your Postgres
                 ──► your audit + afterProjection hooks

在实践中重要的六边规则:

  • 核心从不读 process.env 或者墙壁时间。 你注射一个 Clock 和提供者选项.
  • 无法通过服务商 HTTP 呼叫进行数据库交易 。 持久退出 / webhook / 工作行弥合了这一差距.
  • 一个经过验证的网球是唤醒。 只有在 retrieveSubscription 和材料快照diff.
  • 云构成需要审计钩和回URL许可列表.

1. 说明计划a (billing.config.ts(中文(简体) )

与"Better Auth"相同的想法:软件包拥有Canonical模型;您检查生成的TypeScript,并通过细雨-kit拥有SQL.

import { defineBillingConfig } from "@codelitdev/billing/config";

export default defineBillingConfig({
    dialect: "postgresql",
    adapter: "drizzle",
    output: "./src/db/schema/billing.generated.ts",
    billableEntity: {
        modelName: "organization",
        tableImport: "./organizations",
        tableExport: "organizations",
        idColumn: "id",
        idType: "uuid",
        onDelete: "restrict",
    },
    payer: {
        modelName: "user",
        tableImport: "./auth",
        tableExport: "user",
        idColumn: "id",
        idType: "text",
        onDelete: "restrict",
    },
    planIds: ["pro", "business"],
    requiredOfferKeys: [
        "pro_month",
        "pro_year",
        "business_month",
        "business_year",
    ],
    additionalFields: {
        checkoutAttempts: {
            pendingTeamName: { type: "text", nullable: true },
        },
    },
});

planIds 为: 已支付 计划一直坚持在犬行。 免费计划或无卡试验留在您的桌子上 "free" 在这里.

然后说:

bun x codelit-billing generate --config apps/api/billing.config.ts
bun x drizzle-kit generate
bun x drizzle-kit migrate

CI 应该运行 generate --check 承诺 billing.generated.ts 无法漂移。 手编辑 SQL 用于回填( 例如种子一个 Free) billing_plan_states 行; 从不编辑生成的 TypeScript .

实例: examples/reference-product/, (中文(简体) ), examples/consumers/sendlit/。 。 。 .

2. 制作引擎

解开你自己的地窖 已键入多多选项 。 电线 Drizzle, 赠款,加密, 和产品效果.

当有多个产品共享一款多多业务时,多多将每个事件发送到每个webhook端点. 给每个产品自己的道多品牌, 创建自己的产品在这个品牌之下,并设定 brandId网络用户 brand_id 另一个品牌被存储为 ignored:它们被保存用于分解,但从未加工、再试验或隔离。 网络呼喊没有 brand_id 和以前一样处理。 见 ADR 0009 (韩语)。 。 。 .

要通过柠檬汁来销售, 做曲 createLemonSqueezyBillingProvider 从 @codelitdev/billing/providers/lemonsqueezy 与 apiKey,产品 storeId,和商店的 webhook webhookSecrets设置 checkoutProvider: "lemonsqueezy". Catalog 提供莱蒙 Squeezy 变体 ID 名称为 providerProductId。来自其他商店的 Webhooks 存储为 ignored. 试验来自变体,所以退出 trialDays; 立即适用计划修改,以及 do_not_bill 被拒绝更改到另一个间隔, 因为柠檬挤压的账单是一次性的。 关闭商店客户端设置中的计划更改: 引擎隔离一个没有开始的计划更改, 以及该订阅的后期webhooks.

import { systemClock } from "@codelitdev/billing/core";
import { createDrizzleBillingStore } from "@codelitdev/billing/drizzle";
import { createBilling } from "@codelitdev/billing/workflows";
import { createDodoBillingProvider } from "@codelitdev/billing/providers/dodo";
import { db } from "./db/client";
import * as billingSchema from "./db/billing.generated";

const clock = systemClock;
const store = createDrizzleBillingStore(db, {
    schema: billingSchema,
    clock,
    planStateDefaults: { plan: "free", rampStage: 0, rampCleanStageDays: 0 },
    checkoutApplicationFields: {
        toColumns: (fields) => ({
            pendingTeamName:
                typeof fields.pendingTeamName === "string"
                    ? fields.pendingTeamName
                    : null,
        }),
        fromRow: (row) => ({ pendingTeamName: row.pendingTeamName ?? null }),
    },
});

export const billing = createBilling({
    database: store,
    providers: [
        createDodoBillingProvider({
            apiKey: process.env.DODO_PAYMENTS_API_KEY!,
            environment: "test_mode", // or "live_mode"
            webhookSecrets: [
                {
                    version: "current",
                    secret: process.env.DODO_PAYMENTS_WEBHOOK_KEY_CURRENT!,
                },
            ],
            // Your product's Dodo brand. Other brands' webhooks are ignored.
            brandId: process.env.DODO_BRAND_ID,
            clock,
        }),
    ],
    clock,
    authorization: yourGrantPort, // consume-once BillingAuthorizationPort
    sensitiveValues: yourEncryptPort,
    hooks: {
        audit: yourAuditHook,
        lifecycle: {
            afterProjection: (input) =>
                applyProductEffects(input, store.getTransaction() ?? db),
        },
    },
    mode: "cloud", // "oss" forbids providers, offers, and catalog revision
    checkoutProvider: "dodo",
    requestedRevision: 1,
    requiredOfferKeys: [
        "pro_month",
        "pro_year",
        "business_month",
        "business_year",
    ],
    offers: yourOffers, // must match requiredOfferKeys exactly
    returnUrlValidator: (url) =>
        new URL(url).origin === new URL(process.env.WEB_CLIENT!).origin,
});

afterProjection 运行 内部 发动机交易 使用 store.getTransaction() ?? db 和做( 做) 没有 打开巢穴 db.transaction() 在根池客户端上—— 僵持状态 FOR UPDATE 作行相会. 如果您通过引擎客户端, 请在引擎客户端上写; 只有收到根时才有巢 db。 。 。 .

开放源码软件模式 : mode: "oss",空的供应商, checkoutProvider: "", (中文(简体) ), requestedRevision: null没有报价。 引擎拒绝退票; 您的产品会自动解锁付费能力 .

3. 启动时的目录

云启动验证配置形状 记录 请求的修订。 它不能阻止 Dodo 的普通API 准备状态 .

await billing.recordRequestedCatalog();
// later, from a worker / sweep — not on the hot health path
await billing.verifyRequestedCatalog();

只有在要求的修订得到核实时,才可进行离职 活动 修订。 待决或无效的请求集 checkoutAvailable: false 最后经核实的修订仍然是商业预测。 运算符放弃错误请求 abandonRequestedCatalog 返回配置后.

在创建提供者会话之前, 立即重新检查选中的产品 。 错配会冻结新的取出; 现有的订阅者会保存上次核实的映射 .

4. 人类收费行动

您的应用认证用户, 证明最近 认证 / CSRF , 不管你喜欢什么, 然后发布一个 单用途 BillingActionGrant。引擎消耗赠款 财务报告和审定财务报表 检查坚持付款人。 新的补助金不能将付款人换到现有的客户或订阅者.

const { checkoutUrl } = await billing.startCheckout({
    grant,
    entity: { kind: "organization", id: organizationId },
    payer: { id: userId, email, name },
    offerKey: "pro_month",
    catalogRevision: displayedRevision, // the revision the UI showed
    returnUrl,
    trialDays: 14, // 0 if ineligible; the package does not decide eligibility
    applicationFields: { pendingTeamName: "Acme Team" },
});

一个实体每次有一个开放式的离职,一个小时后失效. 再次开始相同报价的退票 。 如果同一付款人选择了另一个报价,例如每月一次,则该报价将取代开放式报价。 旧的退票变成了 abandoned,以及支持其过期时关闭页面的提供者。 如果它已经支付,它会变成 conflicted:它允许在没有其他情况下进入,否则第二次订阅对操作员进行隔离。 当另一个付款人的退票是开放的, startCheckout 失败 checkout_pending。 。 。 .

其他赠款行动: startPortal, (中文(简体) ), cancel,并修改计划:

await billing.startPlanChange({
    grant,
    entity: { kind: "organization", id: organizationId },
    payer: { id: userId, email, name },
    offerKey: "business_month",
    catalogRevision: displayedRevision,
    effectiveAt: "immediately", // or "next_billing_date"
    prorationMode: "prorated_immediately", // or "do_not_bill"
});

提交用户看到的目录修订; 一个 stale 修订失败 catalog_changed 并可能包括目前的公共目录.

cancel, (中文(简体) ), resumeCancellation,以及 startPlanChange 更改后从提供者读取订阅, 所以 commercialState 反映在网络呼声到来之前. cancel 付款期结束时的注销。 订阅时将保留应享权利来源,直到 paidThroughAt,以及 commercialState 报告 cancelAtPeriodEnd: true 同时,显示结束日期并提议恢复. resumeCancellation 清除计划取消并使用 cancellation 赠款。 期限一结束,提供者的网页钩或 runDeadlineBatch 取消权利。 见 ADR 0010 (英语)。 。 。 .

const state = await billing.commercialState(organizationId);
if (state.cancelAtPeriodEnd) {
    // "Pro stays active until {state.paidThroughAt}" and a resume button
}
await billing.resumeCancellation({ grant, entity, payer });

重新定向不是支付。 由webhook投影或对账产生的应享权利变化。 预定的计划更改不会重写订阅, 直至提供者的当前快照符合即将进行的尝试 .

5. 网络用户和工人

挂载每个提供者的网页 POST /webhooks/billing/<provider> 例如,在API上 /webhooks/billing/dodo。在每个产品中使用相同的路径,所以一个提供者的端点设置在产品中看起来是一样的.

保护 原始内容 躯体。 持久地插入事件后返回 2xx(包括复制件) 。 同步发射.

const ingested = await billing.ingestWebhook({
    provider: "dodo",
    raw: { body, headers },
});
res.status(ingested.duplicate ? 200 : 202).json({ accepted: true });
void billing.runWebhookInboxBatch({ workerId: `billing-${process.pid}` });

软件包不启动定时器 。 您的排程器应该打电话, 有独特的 workerId编号:

单位任务
runWebhookInboxBatch经排水核实的事件(回收+项目)
runReconciliationBatch排出卡住的退出, 计划更改, 和未调节的订阅( Q)discover: false ,然后运行队列任务
runDeadlineBatch到期未结结结账; 过期未结付应享权利
purgeExpiredSensitiveValues丢弃旧的加密取出 URL/ 重放有效载荷
verifyRequestedCatalog完成目录修订

算什么被卡住了 就可以被装扮成 createBilling({ reconciliation: { checkoutStaleAfterMs, planChangeStaleAfterMs, subscriptionStaleAfterMs } });默认时间为一分一小时. 每个未核对的订阅费用每个期间有一个供应商阅读.

多事件安全是数据库租赁,不是程序本地变种。 从不持有一个应用交易 通过一个提供者呼叫.

6. 阅读模型

普通路线不得调用提供者.

const catalog = await billing.publicCatalog();
// { revision, checkoutAvailable, offers: [{ key, plan, interval, amountMinor, currency, displayTrialDays }] }
// no provider product IDs

const commercial = await billing.commercialState(organizationId);
// activePaidPlan, subscriptionStatus, periods, pendingCheckout / pendingPlanChange, projectionVersion

// Inside your own transaction, for example when reserving quota:
await db.transaction(async (tx) => {
    const locked = await billing.commercialState(organizationId, { transaction: tx });
    // the Drizzle store locks the rows it read until tx ends
});

commercialState 是有偿访问的真相来源。 它会重新检查时钟,所以计划取消 停止计酬一次 paidThroughAt 在提供商最后活动之前 runDeadlineBatch. 决定从中解锁什么计划;不将付费状态复制到产品表中并重新确定.

以你的审判,自由计划,配额,和 canManageBilling。 。 。 . projectionVersion 是缓存密钥; 等同的 webhook 重放仅更新新鲜度, 不递增 .

关闭/ 删除/ 所有人降级前, 呼叫 getBillableEntityBillingBlockers 编号 : getPayerBillingResponsibilities 在与变异相同的交易中。 软件包报告阻塞器; 您决定了显示用户的错误 .

工作流失败 BillingWorkflowError 带有稳定代码( :catalog_changed, (中文(简体) ), payer_mismatch, (中文(简体) ), checkout_pending在应用程序中将其映射到 HTTP 。 不要坚持原始 SDK 消息 .

7. 运营商CLI

产品 CLI 调用 createOperations。每个变异需要 OperatorContext (单位:千美元)actorId边框 reason,可选票)并点击审计钩. 该包件不载运 Org/school CLI.

import { createOperations } from "@codelitdev/billing/operations";

const ops = createOperations({
    billing,
    clock,
    requestedRevision: 1,
    checkoutProvider: "dodo",
    sensitiveValues,
});

await ops.health();
await ops.inspectWebhook(providerEventId);
await ops.retryWebhook(ctx, providerEventId, "preserve_attempts"); // also drains the inbox
await ops.reconcileEntity(ctx, billableEntityId); // requires a local entitlement row
await ops.projectProviderSubscription(ctx, {
    providerSubscriptionId, // e.g. from the checkout return URL
    checkoutAttemptId, // optional; defaults to snapshot metadata
});
await ops.adoptProviderSubscription(ctx, {
    providerName: "lemonsqueezy",
    providerSubscriptionId, // started with the provider before this package
    entity: { kind: "organization", id: organizationId },
    payer: { id: userId, email },
});
await ops.requestCancellation(ctx, subscriptionId);
const payload = await ops.inspectWebhookReplay(ctx, providerEventId);

adoptProviderSubscription 接管在引擎外创建的订阅,例如产品使用此包之前的订阅. 它将供应商的客户与付款人联系起来,并预测订阅情况;之后,续约、取消、恢复和门户正常工作。 订阅的产品必须在提供者的活性目录中,为同一个实体再次采用是安全的. 见 ADR 0014 (英语)。 。 。 .

reconcileEntity 无法创建付费但未经预测的离职单的订阅。 使用 projectProviderSubscription 提供商已经持有订阅身份证明,本地权益仍然缺失.

别 UPDATE billing_webhook_events SET status = 'pending' 手边。 重试或保留尝试计数器或启动 new_budget。 。 。 .

程序: docs/runbooks.md。 。 。 .

8. 测试

import {
    runBillingProviderContract,
    createWorkflowHarness,
} from "@codelitdev/billing/testing";
  • runBillingProviderContract(adapter) ——每个适配器,包括CI中的多多与假和有门的沙盒测试对多多测试模式.
  • createWorkflowHarness() ——编目,结账,网约车订购,赠款,对账不设网络.

bun test 是决定性的。 活度多多沙盒测试停留在显式地洞门后.

此软件包不会做什么

  • 产品路线、ts-rest、MCP或UI
  • 授权、成员、公司社会责任基金或发行行动标志
  • 安装/ 导入/ 启动时自动 SQL
  • 共享计费数据库
  • 用量、税费或发票
  • 把用户从一个供应商移动到另一个供应商
  • SendLit 名声、域名或“一个拥有的自由矿石”政策

版本

  • 0.1.0-alpha.x (英语) ——实验工作流程. 公共核心/培训/提供者类型旨在保持稳定;工作流程/合同仍可改变。 安装 alpha 标记, 不 latest。 。 。 .
  • Schema-changing 软件包的放出会撞倒生成的 schema 版本. 升级:打击依赖性 generate * 审查《服务贸易总协定》 * drizzle-kit generate + 应用回填 + 在产品中迁移 + 部署相容的API/工人.
  • 建筑和推出: docs/architecture.md. 采掘业差异: docs/decisions/0001-extraction-discrepancies.md。 。 。 .

开发此 repo

bun install
bun test
bun run typecheck
bun run build
bun run test:packed   # install the tarball via public exports only

版本与发布记录

@codelitdev/billing 共发布 16 个版本,首次发布于 2026/08/31;过去 12 个月发布了 16 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.8.2latest2026/11/09今天
0.8.12026/11/018 天前
0.8.02026/11/018 天前
0.7.02026/11/018 天前
0.6.02026/11/018 天前
0.5.02026/10/3010 天前

其他发布通道:alpha → 0.1.0-alpha.4。

关于 @codelitdev/billing 的常见问题

@codelitdev/billing 是什么?

CodeLit产品供应商中性计费引擎

如何安装 @codelitdev/billing?

在项目目录运行 npm install @codelitdev/billing,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 codelit-billing,可以用 npx codelit-billing 直接运行。

@codelitdev/billing 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@codelitdev/billing 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@codelitdev/billing 可以商用吗?

@codelitdev/billing 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@codelitdev/billing 的下载量有多少?

根据 npm 官方下载统计,@codelitdev/billing 在 2026/10/10 – 2026/11/08 这 30 天内被下载 2,970 次,日均约 99 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@codelitdev/billing 还在维护吗?

最新版本 0.8.2 发布于 2026/11/09(今天);至今共发布 16 个版本,其中过去 12 个月发布了 16 个;首次发布于 2026/08/31。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。