@absolutejs/billing 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ledger.d.ts +56 -0
- package/dist/ledger.js +54 -0
- package/dist/ledger.js.map +10 -0
- package/package.json +8 -3
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** One priced, metered event, ready to persist. */
|
|
2
|
+
export type LedgerEntry = {
|
|
3
|
+
/** Charge in integer sub-units of the plan's denomination (see
|
|
4
|
+
* `Plan.denomination`) — never a float. */
|
|
5
|
+
amount: number;
|
|
6
|
+
/** What the customer is billed in the product's own unit. */
|
|
7
|
+
credits: number;
|
|
8
|
+
/** Product-level grouping ("chat", "voice"), not the vendor's. */
|
|
9
|
+
feature?: string | null;
|
|
10
|
+
model?: string;
|
|
11
|
+
/** "llm" | "tts" | "embedding" | whatever the product meters. */
|
|
12
|
+
operation: string;
|
|
13
|
+
provider: string;
|
|
14
|
+
/** Idempotency handle for at-least-once callers. */
|
|
15
|
+
requestId?: string;
|
|
16
|
+
/** Null for system/background work with nobody to bill. */
|
|
17
|
+
tenant?: string | null;
|
|
18
|
+
};
|
|
19
|
+
export type LedgerCommit = {
|
|
20
|
+
/** Append the row AND debit `entry.credits` from the tenant's balance in a
|
|
21
|
+
* single atomic unit. Called with tenant null for unattributed work, where
|
|
22
|
+
* there is nothing to debit. */
|
|
23
|
+
commit: (entry: LedgerEntry) => Promise<void>;
|
|
24
|
+
/** Fold the entry into a derived daily aggregate. Best-effort by contract:
|
|
25
|
+
* this module swallows its failures. */
|
|
26
|
+
rollup?: (entry: LedgerEntry) => Promise<void>;
|
|
27
|
+
/** Total sub-units charged since `since`, for the spend cap. */
|
|
28
|
+
spentSince?: (since: Date) => Promise<number>;
|
|
29
|
+
};
|
|
30
|
+
export type UsageLedgerOptions = {
|
|
31
|
+
/** Sub-units per credit. A peg of 1_000 with micros means 1 credit =
|
|
32
|
+
* $0.001. Required for credit conversion; omit to bill in raw amounts. */
|
|
33
|
+
creditPegSubUnits?: number;
|
|
34
|
+
/** Reported when a rollup fails. The charge already succeeded. */
|
|
35
|
+
onRollupError?: (error: unknown, entry: LedgerEntry) => void;
|
|
36
|
+
store: LedgerCommit;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Credits for a charge. Ceiling, not rounding: a product that sells credits
|
|
40
|
+
* must never hand out a fraction it cannot deduct, and rounding down means
|
|
41
|
+
* the smallest calls are free — which is how a "cheap" endpoint becomes an
|
|
42
|
+
* unmetered one.
|
|
43
|
+
*/
|
|
44
|
+
export declare const creditsFor: (amount: number, pegSubUnits: number) => number;
|
|
45
|
+
export type UsageLedger = {
|
|
46
|
+
/** Price-agnostic: hand it an amount already in sub-units. Returns what was
|
|
47
|
+
* written, including the credits it derived. */
|
|
48
|
+
record: (entry: Omit<LedgerEntry, "credits"> & {
|
|
49
|
+
credits?: number;
|
|
50
|
+
}) => Promise<LedgerEntry>;
|
|
51
|
+
/** True once spend since `since` reaches `capSubUnits`. Fails OPEN — a
|
|
52
|
+
* broken cap must not take down paid features, and the per-provider
|
|
53
|
+
* budgets are still in front. */
|
|
54
|
+
overCap: (capSubUnits: number, since: Date) => Promise<boolean>;
|
|
55
|
+
};
|
|
56
|
+
export declare const createUsageLedger: (options: UsageLedgerOptions) => UsageLedger;
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/ledger.ts
|
|
18
|
+
var creditsFor = (amount, pegSubUnits) => {
|
|
19
|
+
if (pegSubUnits <= 0)
|
|
20
|
+
return 0;
|
|
21
|
+
return Math.ceil(amount / pegSubUnits);
|
|
22
|
+
};
|
|
23
|
+
var createUsageLedger = (options) => {
|
|
24
|
+
const { creditPegSubUnits, onRollupError, store } = options;
|
|
25
|
+
return {
|
|
26
|
+
overCap: async (capSubUnits, since) => {
|
|
27
|
+
if (!store.spentSince)
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
return await store.spentSince(since) >= capSubUnits;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
record: async (input) => {
|
|
36
|
+
const credits = input.credits ?? (creditPegSubUnits === undefined ? 0 : creditsFor(input.amount, creditPegSubUnits));
|
|
37
|
+
const entry = { ...input, credits };
|
|
38
|
+
await store.commit(entry);
|
|
39
|
+
if (store.rollup) {
|
|
40
|
+
await store.rollup(entry).catch((error) => {
|
|
41
|
+
onRollupError?.(error, entry);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return entry;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export {
|
|
49
|
+
creditsFor,
|
|
50
|
+
createUsageLedger
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
//# debugId=0E1FB04E107E92A964756E2164756E21
|
|
54
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/ledger.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"// The layer between a meter and an invoice: every metered event gets priced,\n// converted to the customer's credit unit, written to an append-only ledger,\n// debited against a balance, and folded into a daily rollup that a spend cap\n// can read. Every app billing a metered API rebuilds this, and each one\n// rediscovers the same three traps:\n//\n// - Money in floats. Summing float ledger rows drifts; one real deployment\n// lost 2 cents across 25,740 rows before anyone noticed. Amounts here are\n// integer sub-units (see Plan.denomination) and are only ever added.\n// - The debit and the ledger row diverging. If the row is written but the\n// balance is not debited, a customer gets free usage; the reverse\n// over-charges. They must land together, so the store commits them as one\n// unit — this module never splits them.\n// - The rollup being treated as truth. It is a derived index, rebuildable\n// from the ledger, so a rollup failure must never fail the charge.\n//\n// Storage stays the host's business (Postgres, ClickHouse, anything). This\n// owns the policy and the arithmetic, which is the part that is identical\n// everywhere — and the part that is worth getting wrong only once.\n\n/** One priced, metered event, ready to persist. */\nexport type LedgerEntry = {\n /** Charge in integer sub-units of the plan's denomination (see\n * `Plan.denomination`) — never a float. */\n amount: number;\n /** What the customer is billed in the product's own unit. */\n credits: number;\n /** Product-level grouping (\"chat\", \"voice\"), not the vendor's. */\n feature?: string | null;\n model?: string;\n /** \"llm\" | \"tts\" | \"embedding\" | whatever the product meters. */\n operation: string;\n provider: string;\n /** Idempotency handle for at-least-once callers. */\n requestId?: string;\n /** Null for system/background work with nobody to bill. */\n tenant?: string | null;\n};\n\nexport type LedgerCommit = {\n /** Append the row AND debit `entry.credits` from the tenant's balance in a\n * single atomic unit. Called with tenant null for unattributed work, where\n * there is nothing to debit. */\n commit: (entry: LedgerEntry) => Promise<void>;\n /** Fold the entry into a derived daily aggregate. Best-effort by contract:\n * this module swallows its failures. */\n rollup?: (entry: LedgerEntry) => Promise<void>;\n /** Total sub-units charged since `since`, for the spend cap. */\n spentSince?: (since: Date) => Promise<number>;\n};\n\nexport type UsageLedgerOptions = {\n /** Sub-units per credit. A peg of 1_000 with micros means 1 credit =\n * $0.001. Required for credit conversion; omit to bill in raw amounts. */\n creditPegSubUnits?: number;\n /** Reported when a rollup fails. The charge already succeeded. */\n onRollupError?: (error: unknown, entry: LedgerEntry) => void;\n store: LedgerCommit;\n};\n\n/**\n * Credits for a charge. Ceiling, not rounding: a product that sells credits\n * must never hand out a fraction it cannot deduct, and rounding down means\n * the smallest calls are free — which is how a \"cheap\" endpoint becomes an\n * unmetered one.\n */\nexport const creditsFor = (amount: number, pegSubUnits: number) => {\n if (pegSubUnits <= 0) return 0;\n\n return Math.ceil(amount / pegSubUnits);\n};\n\nexport type UsageLedger = {\n /** Price-agnostic: hand it an amount already in sub-units. Returns what was\n * written, including the credits it derived. */\n record: (\n entry: Omit<LedgerEntry, \"credits\"> & { credits?: number },\n ) => Promise<LedgerEntry>;\n /** True once spend since `since` reaches `capSubUnits`. Fails OPEN — a\n * broken cap must not take down paid features, and the per-provider\n * budgets are still in front. */\n overCap: (capSubUnits: number, since: Date) => Promise<boolean>;\n};\n\nexport const createUsageLedger = (options: UsageLedgerOptions): UsageLedger => {\n const { creditPegSubUnits, onRollupError, store } = options;\n\n return {\n overCap: async (capSubUnits, since) => {\n if (!store.spentSince) return false;\n try {\n return (await store.spentSince(since)) >= capSubUnits;\n } catch {\n return false;\n }\n },\n record: async (input) => {\n const credits =\n input.credits ??\n (creditPegSubUnits === undefined\n ? 0\n : creditsFor(input.amount, creditPegSubUnits));\n const entry: LedgerEntry = { ...input, credits };\n // The charge is the thing that must not be lost; it is awaited and its\n // failure propagates to the caller.\n await store.commit(entry);\n // The rollup is a derived index — rebuildable from the ledger — so its\n // failure is reported, never raised.\n if (store.rollup) {\n await store.rollup(entry).catch((error: unknown) => {\n onRollupError?.(error, entry);\n });\n }\n\n return entry;\n },\n };\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAkEO,IAAM,aAAa,CAAC,QAAgB,gBAAwB;AAAA,EACjE,IAAI,eAAe;AAAA,IAAG,OAAO;AAAA,EAE7B,OAAO,KAAK,KAAK,SAAS,WAAW;AAAA;AAehC,IAAM,oBAAoB,CAAC,YAA6C;AAAA,EAC7E,QAAQ,mBAAmB,eAAe,UAAU;AAAA,EAEpD,OAAO;AAAA,IACL,SAAS,OAAO,aAAa,UAAU;AAAA,MACrC,IAAI,CAAC,MAAM;AAAA,QAAY,OAAO;AAAA,MAC9B,IAAI;AAAA,QACF,OAAQ,MAAM,MAAM,WAAW,KAAK,KAAM;AAAA,QAC1C,MAAM;AAAA,QACN,OAAO;AAAA;AAAA;AAAA,IAGX,QAAQ,OAAO,UAAU;AAAA,MACvB,MAAM,UACJ,MAAM,YACL,sBAAsB,YACnB,IACA,WAAW,MAAM,QAAQ,iBAAiB;AAAA,MAChD,MAAM,QAAqB,KAAK,OAAO,QAAQ;AAAA,MAG/C,MAAM,MAAM,OAAO,KAAK;AAAA,MAGxB,IAAI,MAAM,QAAQ;AAAA,QAChB,MAAM,MAAM,OAAO,KAAK,EAAE,MAAM,CAAC,UAAmB;AAAA,UAClD,gBAAgB,OAAO,KAAK;AAAA,SAC7B;AAAA,MACH;AAAA,MAEA,OAAO;AAAA;AAAA,EAEX;AAAA;",
|
|
8
|
+
"debugId": "0E1FB04E107E92A964756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/billing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Cost-model substrate for the AbsoluteJS PaaS. createPlan declares a priced product (base fee + per-dimension unit prices + tiered / free-tier rules); computeInvoice turns a @absolutejs/metering Usage snapshot into Invoice line items in integer micros (no float drift). Pluggable invoice sinks (Stripe / etc.) live in @absolutejs/billing-adapters/*.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,12 @@
|
|
|
27
27
|
"import": "./dist/manifest.js",
|
|
28
28
|
"default": "./dist/manifest.js"
|
|
29
29
|
},
|
|
30
|
-
"./manifest.json": "./dist/manifest.json"
|
|
30
|
+
"./manifest.json": "./dist/manifest.json",
|
|
31
|
+
"./ledger": {
|
|
32
|
+
"default": "./dist/ledger.js",
|
|
33
|
+
"import": "./dist/ledger.js",
|
|
34
|
+
"types": "./dist/ledger.d.ts"
|
|
35
|
+
}
|
|
31
36
|
},
|
|
32
37
|
"publishConfig": {
|
|
33
38
|
"access": "public"
|
|
@@ -37,7 +42,7 @@
|
|
|
37
42
|
"README.md"
|
|
38
43
|
],
|
|
39
44
|
"scripts": {
|
|
40
|
-
"build": "rm -rf dist && bun build src/index.ts src/manifest.ts --root ./src --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json && absolute-manifest emit",
|
|
45
|
+
"build": "rm -rf dist && bun build src/index.ts src/ledger.ts src/manifest.ts --root ./src --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json && absolute-manifest emit",
|
|
41
46
|
"test": "bun test tests/",
|
|
42
47
|
"typecheck": "tsc --noEmit",
|
|
43
48
|
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|