@absolutejs/billing 0.1.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/LICENSE +93 -0
- package/README.md +92 -0
- package/dist/index.d.ts +174 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +10 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
**Licensor:** Alex Kahn
|
|
4
|
+
|
|
5
|
+
**Licensed Work:** @absolutejs/billing (https://github.com/absolutejs/billing)
|
|
6
|
+
|
|
7
|
+
**Change Date:** May 31, 2030
|
|
8
|
+
|
|
9
|
+
**Change License:** Apache License, Version 2.0
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Terms
|
|
14
|
+
|
|
15
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
16
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
17
|
+
Licensor may make an Additional Use Grant, permitting limited production use.
|
|
18
|
+
|
|
19
|
+
### Additional Use Grant
|
|
20
|
+
|
|
21
|
+
You may use the Licensed Work in production, provided your use does not include
|
|
22
|
+
any of the following:
|
|
23
|
+
|
|
24
|
+
1. **Offering a Competing Service.** You may not offer the Licensed Work, or
|
|
25
|
+
any derivative or substantial portion of it, to third parties as a hosted or
|
|
26
|
+
managed metering, rating, or usage-based-billing service that competes with
|
|
27
|
+
hosted SaaS-billing offerings (including, but not limited to, Metronome,
|
|
28
|
+
Orb, Lago, Stripe Billing, m3ter, Chargebee, Recurly, Maxio, Zuora, Sage
|
|
29
|
+
Intacct Subscriptions, OctaneAI Billing, Togai, or any similar hosted
|
|
30
|
+
offering whose primary value to its users is metering, rating, and invoicing
|
|
31
|
+
usage-based products). This includes any product whose primary value to its
|
|
32
|
+
users is the functionality the Licensed Work provides.
|
|
33
|
+
|
|
34
|
+
2. **Resale or Redistribution as a Standalone Product.** You may not sell,
|
|
35
|
+
license, or distribute the Licensed Work, or any derivative or fork of it,
|
|
36
|
+
as a standalone commercial product.
|
|
37
|
+
|
|
38
|
+
3. **Removal of Attribution.** Any derivative work, fork, or redistribution of
|
|
39
|
+
the Licensed Work must prominently credit AbsoluteJS and include a link to
|
|
40
|
+
the original project repository (https://github.com/absolutejs/billing).
|
|
41
|
+
|
|
42
|
+
For clarity, the following uses are expressly permitted:
|
|
43
|
+
|
|
44
|
+
- Using the Licensed Work to compute invoices for your own customers, internal
|
|
45
|
+
cost dashboards, finance reports, or SaaS products (whether commercial or
|
|
46
|
+
non-commercial), so long as the Licensed Work itself is not the primary
|
|
47
|
+
product you are selling.
|
|
48
|
+
- Using the Licensed Work as a dependency in commercial software you build and
|
|
49
|
+
sell, as long as the software is not itself a competing hosted billing
|
|
50
|
+
service of the kind described in clause 1.
|
|
51
|
+
- Providing consulting, development, or professional services to clients using
|
|
52
|
+
the Licensed Work.
|
|
53
|
+
- Forking and modifying the Licensed Work for your own internal use, provided
|
|
54
|
+
attribution is maintained.
|
|
55
|
+
|
|
56
|
+
### Change Date and Change License
|
|
57
|
+
|
|
58
|
+
On the Change Date specified above, or on such other date as the Licensor may
|
|
59
|
+
specify by written notice, the Licensed Work will be made available under the
|
|
60
|
+
Change License (Apache License, Version 2.0). Until the Change Date, the terms
|
|
61
|
+
of this Business Source License 1.1 apply.
|
|
62
|
+
|
|
63
|
+
### Trademark
|
|
64
|
+
|
|
65
|
+
This license does not grant you any rights to use the "AbsoluteJS" or
|
|
66
|
+
"@absolutejs" name, logo, or any related trademarks. Forks and derivative works
|
|
67
|
+
must not be named or branded in a manner that suggests endorsement by or
|
|
68
|
+
affiliation with AbsoluteJS or the Licensor.
|
|
69
|
+
|
|
70
|
+
### Notices
|
|
71
|
+
|
|
72
|
+
You must not remove or obscure any licensing, copyright, or other notices
|
|
73
|
+
included in the Licensed Work.
|
|
74
|
+
|
|
75
|
+
### No Warranty
|
|
76
|
+
|
|
77
|
+
THE LICENSED WORK IS PROVIDED "AS IS". THE LICENSOR HEREBY DISCLAIMS ALL
|
|
78
|
+
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
|
|
79
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO
|
|
80
|
+
EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
|
|
81
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
|
|
82
|
+
IN CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE
|
|
83
|
+
LICENSED WORK.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Contact
|
|
88
|
+
|
|
89
|
+
For commercial licensing inquiries or additional permissions, contact:
|
|
90
|
+
|
|
91
|
+
- **Alex Kahn**
|
|
92
|
+
- alexkahndev@gmail.com
|
|
93
|
+
- alexkahndev.github.io
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# `@absolutejs/billing`
|
|
2
|
+
|
|
3
|
+
> Cost-model substrate for the AbsoluteJS PaaS.
|
|
4
|
+
|
|
5
|
+
`@absolutejs/billing` is the pure-function layer between
|
|
6
|
+
`@absolutejs/metering` (which collects usage events) and an
|
|
7
|
+
invoicing backend (Stripe, QuickBooks, an internal billing engine).
|
|
8
|
+
|
|
9
|
+
It does two things:
|
|
10
|
+
|
|
11
|
+
1. **`createPlan(...)`** — declares a priced product: optional flat
|
|
12
|
+
base fee, per-dimension unit prices, optional graduated tiers,
|
|
13
|
+
optional per-dimension free allowances.
|
|
14
|
+
2. **`computeInvoice({ plan, period, tenant, usage })`** — pure
|
|
15
|
+
function that turns a `Usage` snapshot into an `Invoice` with
|
|
16
|
+
line items and a total.
|
|
17
|
+
|
|
18
|
+
All money math is done in integer **micros** (1 micro = 1/1,000,000
|
|
19
|
+
of a currency unit — the same denomination Stripe stores prices in
|
|
20
|
+
internally). Float drift is structurally impossible: a $0.0002
|
|
21
|
+
per-request price is `200` and rounding policy is explicit.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createPlan, computeInvoice, formatMicros } from '@absolutejs/billing';
|
|
25
|
+
|
|
26
|
+
const plan = createPlan({
|
|
27
|
+
name: 'pro',
|
|
28
|
+
currency: 'usd',
|
|
29
|
+
basePriceMicros: 20_000_000, // $20/mo
|
|
30
|
+
pricedDimensions: {
|
|
31
|
+
requests: { perUnitMicros: 200, freeTier: 1_000_000 },
|
|
32
|
+
cpuMs: { perUnitMicros: 50, unit: 1000, freeTier: 60_000 * 60 * 10 },
|
|
33
|
+
bytesEgress: { perUnitMicros: 100, unit: 1024 * 1024, freeTier: 100 * 1024 * 1024 },
|
|
34
|
+
hibernationGbSeconds: { perUnitMicros: 5 },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const invoice = computeInvoice({
|
|
39
|
+
plan,
|
|
40
|
+
tenant: 'acme',
|
|
41
|
+
period: { start, end },
|
|
42
|
+
usage, // a Usage from @absolutejs/metering
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log(formatMicros(invoice.totalMicros, invoice.currency));
|
|
46
|
+
// "27.50 USD"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Pricing shapes
|
|
50
|
+
|
|
51
|
+
A `PricedDimension` is one of three:
|
|
52
|
+
|
|
53
|
+
- **Flat per-unit** — `{ perUnitMicros: 200, unit: 1 }`
|
|
54
|
+
- **Tiered (graduated)** — `{ tiers: [{ upTo: 1_000_000, perUnitMicros: 200 }, { upTo: Infinity, perUnitMicros: 100 }] }`
|
|
55
|
+
- **Custom** — `{ price: (chargedQuantity) => micros }` (escape hatch for surge / caps / non-monotonic pricing)
|
|
56
|
+
|
|
57
|
+
Optional knobs:
|
|
58
|
+
|
|
59
|
+
- **`freeTier`** — units subtracted before pricing
|
|
60
|
+
- **`unit`** — divisor so `bytesEgress` priced as MB ↔ `unit:
|
|
61
|
+
1024*1024`
|
|
62
|
+
- **`label`** — invoice line-item display name
|
|
63
|
+
|
|
64
|
+
Plan-level knobs:
|
|
65
|
+
|
|
66
|
+
- **`basePriceMicros`** — flat fee per period
|
|
67
|
+
- **`minimumChargeMicros`** — floor; an adjustment line item fills
|
|
68
|
+
any gap
|
|
69
|
+
- **`rounding`** — `'truncate'` (default; sub-cent → $0.00) or
|
|
70
|
+
`'round-half-up'`
|
|
71
|
+
- **`currency`** — display label; not converted
|
|
72
|
+
- **`metadata`** — arbitrary keys that flow through to the invoice
|
|
73
|
+
|
|
74
|
+
## Why pure?
|
|
75
|
+
|
|
76
|
+
The control plane needs to:
|
|
77
|
+
|
|
78
|
+
- **Preview** an upcoming invoice before the period closes
|
|
79
|
+
- **Re-price** a past period under a new plan ("what would this
|
|
80
|
+
customer have paid on the proposed enterprise tier?")
|
|
81
|
+
- **Dry-run** plan changes before publishing them
|
|
82
|
+
|
|
83
|
+
A pure cost-model function makes all three trivial — no Stripe SDK,
|
|
84
|
+
no side effects, no IO. The Stripe push (or QuickBooks export, or
|
|
85
|
+
mailed-PDF generator) lives outside this package, in
|
|
86
|
+
`@absolutejs/billing-adapters/*`.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
BSL-1.1 with named carveout against hosted SaaS billing platforms
|
|
91
|
+
(Metronome, Orb, Lago, Stripe Billing, m3ter, Chargebee). See
|
|
92
|
+
`LICENSE`. Change date: **2030-05-31** → Apache 2.0.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @absolutejs/billing — cost-model substrate for the AbsoluteJS PaaS.
|
|
3
|
+
*
|
|
4
|
+
* Two pieces:
|
|
5
|
+
*
|
|
6
|
+
* - `createPlan(...)` — declarative pricing config: optional flat
|
|
7
|
+
* base fee + per-dimension unit prices, with optional graduated
|
|
8
|
+
* tiers and free-tier allowances per dimension.
|
|
9
|
+
*
|
|
10
|
+
* - `computeInvoice({ plan, period, tenant, usage, currency? })`
|
|
11
|
+
* — pure function that turns a `@absolutejs/metering`-shaped
|
|
12
|
+
* `Usage` snapshot (or any record of metered numbers) into an
|
|
13
|
+
* `Invoice` of line items + total. All money math is done in
|
|
14
|
+
* integer **micros** (1 micro = 1/1,000,000 of a currency unit
|
|
15
|
+
* — the same denomination Stripe uses internally) so float
|
|
16
|
+
* drift is structurally impossible.
|
|
17
|
+
*
|
|
18
|
+
* Invoice sinks (push to Stripe, post to QuickBooks, mail a PDF)
|
|
19
|
+
* live OUTSIDE this package, in `@absolutejs/billing-adapters/*`.
|
|
20
|
+
* Keeping the substrate pure means the control plane can preview
|
|
21
|
+
* invoices, run dry-run "would-charge" projections, and replay an
|
|
22
|
+
* old usage snapshot through a new plan without touching any
|
|
23
|
+
* vendor SDK.
|
|
24
|
+
*/
|
|
25
|
+
/** Integer micros — 1,000,000 micros = 1 unit of the currency. */
|
|
26
|
+
export type Micros = number;
|
|
27
|
+
/**
|
|
28
|
+
* Round a fractional micros value to an integer. The substrate uses
|
|
29
|
+
* **truncation** (banker's-style would surprise callers expecting
|
|
30
|
+
* "$0.0009 → $0.00" not "$0.0009 → $0.001"). Plans override per-plan.
|
|
31
|
+
*/
|
|
32
|
+
export type Rounding = 'truncate' | 'round-half-up';
|
|
33
|
+
/**
|
|
34
|
+
* One step in a graduated-tier price table. `upTo` is the inclusive
|
|
35
|
+
* upper bound (in metered units, NOT micros) for this band.
|
|
36
|
+
* `perUnitMicros` is what the customer pays per single metered unit
|
|
37
|
+
* within this band. The last entry must have `upTo: Infinity` to
|
|
38
|
+
* cover any overflow.
|
|
39
|
+
*/
|
|
40
|
+
export type PricingTier = {
|
|
41
|
+
upTo: number;
|
|
42
|
+
perUnitMicros: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Per-dimension pricing. Three shapes:
|
|
46
|
+
*
|
|
47
|
+
* - Flat per-unit: `{ perUnitMicros: 200, unit: 1024 * 1024 }`
|
|
48
|
+
* charges 200 micros ($0.0002) per MB of usage.
|
|
49
|
+
*
|
|
50
|
+
* - Tiered: `{ tiers: [...], unit: 1 }` charges per the first
|
|
51
|
+
* matching `PricingTier` band.
|
|
52
|
+
*
|
|
53
|
+
* - Custom: `{ price: (quantity) => micros, unit: 1 }` — escape
|
|
54
|
+
* hatch for surge / caps / non-monotonic pricing. The substrate
|
|
55
|
+
* stays pure; you ship whatever function you want.
|
|
56
|
+
*
|
|
57
|
+
* `freeTier` is subtracted from the metered quantity BEFORE pricing
|
|
58
|
+
* — the conventional "first N units free" rule.
|
|
59
|
+
*
|
|
60
|
+
* `unit` is the metered-unit denominator: 1 means "price per single
|
|
61
|
+
* metered unit", 1024*1024 means "price per MB when quantity is in
|
|
62
|
+
* bytes." Default 1.
|
|
63
|
+
*
|
|
64
|
+
* `label` overrides the line-item display name.
|
|
65
|
+
*/
|
|
66
|
+
export type PricedDimension = {
|
|
67
|
+
label?: string;
|
|
68
|
+
freeTier?: number;
|
|
69
|
+
unit?: number;
|
|
70
|
+
} & ({
|
|
71
|
+
perUnitMicros: number;
|
|
72
|
+
tiers?: never;
|
|
73
|
+
price?: never;
|
|
74
|
+
} | {
|
|
75
|
+
tiers: PricingTier[];
|
|
76
|
+
perUnitMicros?: never;
|
|
77
|
+
price?: never;
|
|
78
|
+
} | {
|
|
79
|
+
price: (chargedQuantity: number) => Micros;
|
|
80
|
+
perUnitMicros?: never;
|
|
81
|
+
tiers?: never;
|
|
82
|
+
});
|
|
83
|
+
export type Plan = {
|
|
84
|
+
/** Human label for the invoice (`'pro'`, `'enterprise'`, etc.). */
|
|
85
|
+
name: string;
|
|
86
|
+
/** Optional flat base fee charged once per invoice period. */
|
|
87
|
+
basePriceMicros?: Micros;
|
|
88
|
+
/**
|
|
89
|
+
* Dimensions priced from usage. Keys must match keys on the
|
|
90
|
+
* `usage` record passed to `computeInvoice`. Anything not listed
|
|
91
|
+
* is ignored.
|
|
92
|
+
*/
|
|
93
|
+
pricedDimensions: Record<string, PricedDimension>;
|
|
94
|
+
/** Default currency for invoices generated from this plan. */
|
|
95
|
+
currency?: string;
|
|
96
|
+
/** Rounding strategy applied per line item. Default `'truncate'`. */
|
|
97
|
+
rounding?: Rounding;
|
|
98
|
+
/**
|
|
99
|
+
* Minimum charge (in micros) — if the computed total is below
|
|
100
|
+
* this floor, the invoice total is raised to the floor and a
|
|
101
|
+
* single `'minimum-charge-adjustment'` line item captures the
|
|
102
|
+
* difference. Defaults to 0 (no floor).
|
|
103
|
+
*/
|
|
104
|
+
minimumChargeMicros?: Micros;
|
|
105
|
+
/** Arbitrary plan-level metadata that flows through to invoices. */
|
|
106
|
+
metadata?: Record<string, string>;
|
|
107
|
+
};
|
|
108
|
+
export declare const createPlan: (plan: Plan) => Plan;
|
|
109
|
+
export type LineItem = {
|
|
110
|
+
/**
|
|
111
|
+
* Stable key for the line item. For priced dimensions it's the
|
|
112
|
+
* usage-record key (`'requests'`, `'cpuMs'`, etc.). For the base
|
|
113
|
+
* fee it's `'base'`. For minimum-charge top-up it's
|
|
114
|
+
* `'minimum-charge-adjustment'`.
|
|
115
|
+
*/
|
|
116
|
+
key: string;
|
|
117
|
+
/** Human-readable label. */
|
|
118
|
+
label: string;
|
|
119
|
+
/** Metered units BEFORE applying free tier. 0 for the base fee. */
|
|
120
|
+
quantity: number;
|
|
121
|
+
/** Metered units AFTER applying free tier (what's actually charged). */
|
|
122
|
+
chargedQuantity: number;
|
|
123
|
+
/** Free-tier units subtracted from `quantity`. */
|
|
124
|
+
freeTier?: number;
|
|
125
|
+
/** Charge for this line in integer micros. */
|
|
126
|
+
amountMicros: Micros;
|
|
127
|
+
/**
|
|
128
|
+
* Tier-by-tier breakdown when graduated pricing was used. Each
|
|
129
|
+
* entry: `{ tierIndex, unitsInTier, perUnitMicros, amountMicros }`.
|
|
130
|
+
*/
|
|
131
|
+
tierBreakdown?: Array<{
|
|
132
|
+
tierIndex: number;
|
|
133
|
+
unitsInTier: number;
|
|
134
|
+
perUnitMicros: number;
|
|
135
|
+
amountMicros: Micros;
|
|
136
|
+
}>;
|
|
137
|
+
};
|
|
138
|
+
export type InvoicePeriod = {
|
|
139
|
+
/** Inclusive period start (`Date.now()` ms). */
|
|
140
|
+
start: number;
|
|
141
|
+
/** Exclusive period end. */
|
|
142
|
+
end: number;
|
|
143
|
+
};
|
|
144
|
+
export type Invoice = {
|
|
145
|
+
tenant: string;
|
|
146
|
+
plan: string;
|
|
147
|
+
currency: string;
|
|
148
|
+
period: InvoicePeriod;
|
|
149
|
+
lineItems: LineItem[];
|
|
150
|
+
/** Sum of all `lineItems[].amountMicros`. */
|
|
151
|
+
totalMicros: Micros;
|
|
152
|
+
/** Convenience: `totalMicros / 1_000_000` as a number. */
|
|
153
|
+
totalUnits: number;
|
|
154
|
+
/** Plan-level metadata copied through unchanged. */
|
|
155
|
+
metadata?: Record<string, string>;
|
|
156
|
+
};
|
|
157
|
+
export type ComputeInvoiceInput = {
|
|
158
|
+
plan: Plan;
|
|
159
|
+
tenant: string;
|
|
160
|
+
period: InvoicePeriod;
|
|
161
|
+
/** Metered numbers keyed by the same names as `plan.pricedDimensions`. */
|
|
162
|
+
usage: Record<string, number>;
|
|
163
|
+
/** Override the plan's currency (e.g. for tenant-local invoicing). */
|
|
164
|
+
currency?: string;
|
|
165
|
+
};
|
|
166
|
+
export declare const computeInvoice: ({ plan, tenant, period, usage, currency }: ComputeInvoiceInput) => Invoice;
|
|
167
|
+
/**
|
|
168
|
+
* Format an integer micros amount as a human currency string. Pure
|
|
169
|
+
* — no Intl side effects. For locales / advanced formatting, pipe
|
|
170
|
+
* through `Intl.NumberFormat` yourself.
|
|
171
|
+
*/
|
|
172
|
+
export declare const formatMicros: (amount: Micros, currency: string, { minorUnits }?: {
|
|
173
|
+
minorUnits?: number;
|
|
174
|
+
}) => string;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
var roundMicros = (value, rounding) => {
|
|
4
|
+
if (rounding === "truncate")
|
|
5
|
+
return Math.trunc(value);
|
|
6
|
+
return Math.round(value);
|
|
7
|
+
};
|
|
8
|
+
var createPlan = (plan) => {
|
|
9
|
+
for (const [key, dim] of Object.entries(plan.pricedDimensions)) {
|
|
10
|
+
if (dim.tiers !== undefined) {
|
|
11
|
+
if (dim.tiers.length === 0) {
|
|
12
|
+
throw new Error(`billing: dimension '${key}' has no tiers`);
|
|
13
|
+
}
|
|
14
|
+
const last = dim.tiers[dim.tiers.length - 1];
|
|
15
|
+
if (last !== undefined && Number.isFinite(last.upTo)) {
|
|
16
|
+
throw new Error(`billing: dimension '${key}' final tier must have upTo: Infinity`);
|
|
17
|
+
}
|
|
18
|
+
let prev = 0;
|
|
19
|
+
for (let i = 0;i < dim.tiers.length; i += 1) {
|
|
20
|
+
const tier = dim.tiers[i];
|
|
21
|
+
if (tier.upTo < prev) {
|
|
22
|
+
throw new Error(`billing: dimension '${key}' tier #${i} upTo (${tier.upTo}) must be >= previous (${prev})`);
|
|
23
|
+
}
|
|
24
|
+
prev = tier.upTo;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return plan;
|
|
29
|
+
};
|
|
30
|
+
var computeDimension = ({
|
|
31
|
+
quantity,
|
|
32
|
+
dim,
|
|
33
|
+
rounding
|
|
34
|
+
}) => {
|
|
35
|
+
const free = dim.freeTier ?? 0;
|
|
36
|
+
const charged = Math.max(0, quantity - free);
|
|
37
|
+
const unit = dim.unit ?? 1;
|
|
38
|
+
const chargedUnits = unit === 1 ? charged : charged / unit;
|
|
39
|
+
if (dim.perUnitMicros !== undefined) {
|
|
40
|
+
const amountMicros = roundMicros(chargedUnits * dim.perUnitMicros, rounding);
|
|
41
|
+
return { amountMicros, chargedQuantity: charged };
|
|
42
|
+
}
|
|
43
|
+
if (dim.price !== undefined) {
|
|
44
|
+
const amountMicros = roundMicros(dim.price(charged), rounding);
|
|
45
|
+
return { amountMicros, chargedQuantity: charged };
|
|
46
|
+
}
|
|
47
|
+
const tierBreakdown = [];
|
|
48
|
+
let remaining = chargedUnits;
|
|
49
|
+
let bandFloor = 0;
|
|
50
|
+
let totalMicros = 0;
|
|
51
|
+
for (let i = 0;i < dim.tiers.length && remaining > 0; i += 1) {
|
|
52
|
+
const tier = dim.tiers[i];
|
|
53
|
+
const bandWidth = tier.upTo - bandFloor;
|
|
54
|
+
const unitsInTier = Math.min(remaining, bandWidth);
|
|
55
|
+
if (unitsInTier > 0) {
|
|
56
|
+
const tierMicros = roundMicros(unitsInTier * tier.perUnitMicros, rounding);
|
|
57
|
+
tierBreakdown.push({
|
|
58
|
+
amountMicros: tierMicros,
|
|
59
|
+
perUnitMicros: tier.perUnitMicros,
|
|
60
|
+
tierIndex: i,
|
|
61
|
+
unitsInTier
|
|
62
|
+
});
|
|
63
|
+
totalMicros += tierMicros;
|
|
64
|
+
}
|
|
65
|
+
remaining -= unitsInTier;
|
|
66
|
+
bandFloor = tier.upTo;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
amountMicros: totalMicros,
|
|
70
|
+
chargedQuantity: charged,
|
|
71
|
+
tierBreakdown
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
var computeInvoice = ({
|
|
75
|
+
plan,
|
|
76
|
+
tenant,
|
|
77
|
+
period,
|
|
78
|
+
usage,
|
|
79
|
+
currency
|
|
80
|
+
}) => {
|
|
81
|
+
const rounding = plan.rounding ?? "truncate";
|
|
82
|
+
const lineItems = [];
|
|
83
|
+
if (plan.basePriceMicros !== undefined && plan.basePriceMicros > 0) {
|
|
84
|
+
lineItems.push({
|
|
85
|
+
amountMicros: plan.basePriceMicros,
|
|
86
|
+
chargedQuantity: 1,
|
|
87
|
+
key: "base",
|
|
88
|
+
label: `${plan.name} base fee`,
|
|
89
|
+
quantity: 1
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
for (const [key, dim] of Object.entries(plan.pricedDimensions)) {
|
|
93
|
+
const quantity = usage[key] ?? 0;
|
|
94
|
+
if (!Number.isFinite(quantity) || quantity < 0)
|
|
95
|
+
continue;
|
|
96
|
+
const result = computeDimension({ dim, quantity, rounding });
|
|
97
|
+
if (result.amountMicros === 0 && result.chargedQuantity === 0)
|
|
98
|
+
continue;
|
|
99
|
+
const item = {
|
|
100
|
+
amountMicros: result.amountMicros,
|
|
101
|
+
chargedQuantity: result.chargedQuantity,
|
|
102
|
+
key,
|
|
103
|
+
label: dim.label ?? key,
|
|
104
|
+
quantity
|
|
105
|
+
};
|
|
106
|
+
if (dim.freeTier !== undefined)
|
|
107
|
+
item.freeTier = dim.freeTier;
|
|
108
|
+
if (result.tierBreakdown !== undefined && result.tierBreakdown.length > 0) {
|
|
109
|
+
item.tierBreakdown = result.tierBreakdown;
|
|
110
|
+
}
|
|
111
|
+
lineItems.push(item);
|
|
112
|
+
}
|
|
113
|
+
let totalMicros = lineItems.reduce((sum, item) => sum + item.amountMicros, 0);
|
|
114
|
+
const floor = plan.minimumChargeMicros ?? 0;
|
|
115
|
+
if (floor > 0 && totalMicros < floor) {
|
|
116
|
+
const gap = floor - totalMicros;
|
|
117
|
+
lineItems.push({
|
|
118
|
+
amountMicros: gap,
|
|
119
|
+
chargedQuantity: 1,
|
|
120
|
+
key: "minimum-charge-adjustment",
|
|
121
|
+
label: "Minimum charge adjustment",
|
|
122
|
+
quantity: 1
|
|
123
|
+
});
|
|
124
|
+
totalMicros = floor;
|
|
125
|
+
}
|
|
126
|
+
const invoice = {
|
|
127
|
+
currency: currency ?? plan.currency ?? "usd",
|
|
128
|
+
lineItems,
|
|
129
|
+
period,
|
|
130
|
+
plan: plan.name,
|
|
131
|
+
tenant,
|
|
132
|
+
totalMicros,
|
|
133
|
+
totalUnits: totalMicros / 1e6
|
|
134
|
+
};
|
|
135
|
+
if (plan.metadata !== undefined)
|
|
136
|
+
invoice.metadata = plan.metadata;
|
|
137
|
+
return invoice;
|
|
138
|
+
};
|
|
139
|
+
var formatMicros = (amount, currency, { minorUnits = 2 } = {}) => {
|
|
140
|
+
const sign = amount < 0 ? "-" : "";
|
|
141
|
+
const abs = Math.abs(amount);
|
|
142
|
+
const wholeMicrosPerMinor = 10 ** (6 - minorUnits);
|
|
143
|
+
const minorTotal = Math.round(abs / wholeMicrosPerMinor);
|
|
144
|
+
const divisor = 10 ** minorUnits;
|
|
145
|
+
const whole = Math.trunc(minorTotal / divisor);
|
|
146
|
+
const upper = currency.toUpperCase();
|
|
147
|
+
if (minorUnits === 0)
|
|
148
|
+
return `${sign}${whole} ${upper}`;
|
|
149
|
+
const fraction = minorTotal % divisor;
|
|
150
|
+
const fractionStr = fraction.toString().padStart(minorUnits, "0");
|
|
151
|
+
return `${sign}${whole}.${fractionStr} ${upper}`;
|
|
152
|
+
};
|
|
153
|
+
export {
|
|
154
|
+
formatMicros,
|
|
155
|
+
createPlan,
|
|
156
|
+
computeInvoice
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
//# debugId=1E2BA78C0466F20664756E2164756E21
|
|
160
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @absolutejs/billing — cost-model substrate for the AbsoluteJS PaaS.\n *\n * Two pieces:\n *\n * - `createPlan(...)` — declarative pricing config: optional flat\n * base fee + per-dimension unit prices, with optional graduated\n * tiers and free-tier allowances per dimension.\n *\n * - `computeInvoice({ plan, period, tenant, usage, currency? })`\n * — pure function that turns a `@absolutejs/metering`-shaped\n * `Usage` snapshot (or any record of metered numbers) into an\n * `Invoice` of line items + total. All money math is done in\n * integer **micros** (1 micro = 1/1,000,000 of a currency unit\n * — the same denomination Stripe uses internally) so float\n * drift is structurally impossible.\n *\n * Invoice sinks (push to Stripe, post to QuickBooks, mail a PDF)\n * live OUTSIDE this package, in `@absolutejs/billing-adapters/*`.\n * Keeping the substrate pure means the control plane can preview\n * invoices, run dry-run \"would-charge\" projections, and replay an\n * old usage snapshot through a new plan without touching any\n * vendor SDK.\n */\n\n// =============================================================================\n// Money primitives\n// =============================================================================\n\n/** Integer micros — 1,000,000 micros = 1 unit of the currency. */\nexport type Micros = number;\n\n/**\n * Round a fractional micros value to an integer. The substrate uses\n * **truncation** (banker's-style would surprise callers expecting\n * \"$0.0009 → $0.00\" not \"$0.0009 → $0.001\"). Plans override per-plan.\n */\nexport type Rounding = 'truncate' | 'round-half-up';\n\nconst roundMicros = (value: number, rounding: Rounding): Micros => {\n\tif (rounding === 'truncate') return Math.trunc(value);\n\treturn Math.round(value);\n};\n\n// =============================================================================\n// Pricing config\n// =============================================================================\n\n/**\n * One step in a graduated-tier price table. `upTo` is the inclusive\n * upper bound (in metered units, NOT micros) for this band.\n * `perUnitMicros` is what the customer pays per single metered unit\n * within this band. The last entry must have `upTo: Infinity` to\n * cover any overflow.\n */\nexport type PricingTier = {\n\tupTo: number;\n\tperUnitMicros: number;\n};\n\n/**\n * Per-dimension pricing. Three shapes:\n *\n * - Flat per-unit: `{ perUnitMicros: 200, unit: 1024 * 1024 }`\n * charges 200 micros ($0.0002) per MB of usage.\n *\n * - Tiered: `{ tiers: [...], unit: 1 }` charges per the first\n * matching `PricingTier` band.\n *\n * - Custom: `{ price: (quantity) => micros, unit: 1 }` — escape\n * hatch for surge / caps / non-monotonic pricing. The substrate\n * stays pure; you ship whatever function you want.\n *\n * `freeTier` is subtracted from the metered quantity BEFORE pricing\n * — the conventional \"first N units free\" rule.\n *\n * `unit` is the metered-unit denominator: 1 means \"price per single\n * metered unit\", 1024*1024 means \"price per MB when quantity is in\n * bytes.\" Default 1.\n *\n * `label` overrides the line-item display name.\n */\nexport type PricedDimension = {\n\tlabel?: string;\n\tfreeTier?: number;\n\tunit?: number;\n} & (\n\t| { perUnitMicros: number; tiers?: never; price?: never }\n\t| { tiers: PricingTier[]; perUnitMicros?: never; price?: never }\n\t| {\n\t\t\tprice: (chargedQuantity: number) => Micros;\n\t\t\tperUnitMicros?: never;\n\t\t\ttiers?: never;\n\t }\n);\n\nexport type Plan = {\n\t/** Human label for the invoice (`'pro'`, `'enterprise'`, etc.). */\n\tname: string;\n\t/** Optional flat base fee charged once per invoice period. */\n\tbasePriceMicros?: Micros;\n\t/**\n\t * Dimensions priced from usage. Keys must match keys on the\n\t * `usage` record passed to `computeInvoice`. Anything not listed\n\t * is ignored.\n\t */\n\tpricedDimensions: Record<string, PricedDimension>;\n\t/** Default currency for invoices generated from this plan. */\n\tcurrency?: string;\n\t/** Rounding strategy applied per line item. Default `'truncate'`. */\n\trounding?: Rounding;\n\t/**\n\t * Minimum charge (in micros) — if the computed total is below\n\t * this floor, the invoice total is raised to the floor and a\n\t * single `'minimum-charge-adjustment'` line item captures the\n\t * difference. Defaults to 0 (no floor).\n\t */\n\tminimumChargeMicros?: Micros;\n\t/** Arbitrary plan-level metadata that flows through to invoices. */\n\tmetadata?: Record<string, string>;\n};\n\nexport const createPlan = (plan: Plan): Plan => {\n\tfor (const [key, dim] of Object.entries(plan.pricedDimensions)) {\n\t\tif (dim.tiers !== undefined) {\n\t\t\tif (dim.tiers.length === 0) {\n\t\t\t\tthrow new Error(`billing: dimension '${key}' has no tiers`);\n\t\t\t}\n\t\t\tconst last = dim.tiers[dim.tiers.length - 1];\n\t\t\tif (last !== undefined && Number.isFinite(last.upTo)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`billing: dimension '${key}' final tier must have upTo: Infinity`\n\t\t\t\t);\n\t\t\t}\n\t\t\tlet prev = 0;\n\t\t\tfor (let i = 0; i < dim.tiers.length; i += 1) {\n\t\t\t\tconst tier = dim.tiers[i]!;\n\t\t\t\tif (tier.upTo < prev) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`billing: dimension '${key}' tier #${i} upTo (${tier.upTo}) must be >= previous (${prev})`\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tprev = tier.upTo;\n\t\t\t}\n\t\t}\n\t}\n\treturn plan;\n};\n\n// =============================================================================\n// Invoice shape\n// =============================================================================\n\nexport type LineItem = {\n\t/**\n\t * Stable key for the line item. For priced dimensions it's the\n\t * usage-record key (`'requests'`, `'cpuMs'`, etc.). For the base\n\t * fee it's `'base'`. For minimum-charge top-up it's\n\t * `'minimum-charge-adjustment'`.\n\t */\n\tkey: string;\n\t/** Human-readable label. */\n\tlabel: string;\n\t/** Metered units BEFORE applying free tier. 0 for the base fee. */\n\tquantity: number;\n\t/** Metered units AFTER applying free tier (what's actually charged). */\n\tchargedQuantity: number;\n\t/** Free-tier units subtracted from `quantity`. */\n\tfreeTier?: number;\n\t/** Charge for this line in integer micros. */\n\tamountMicros: Micros;\n\t/**\n\t * Tier-by-tier breakdown when graduated pricing was used. Each\n\t * entry: `{ tierIndex, unitsInTier, perUnitMicros, amountMicros }`.\n\t */\n\ttierBreakdown?: Array<{\n\t\ttierIndex: number;\n\t\tunitsInTier: number;\n\t\tperUnitMicros: number;\n\t\tamountMicros: Micros;\n\t}>;\n};\n\nexport type InvoicePeriod = {\n\t/** Inclusive period start (`Date.now()` ms). */\n\tstart: number;\n\t/** Exclusive period end. */\n\tend: number;\n};\n\nexport type Invoice = {\n\ttenant: string;\n\tplan: string;\n\tcurrency: string;\n\tperiod: InvoicePeriod;\n\tlineItems: LineItem[];\n\t/** Sum of all `lineItems[].amountMicros`. */\n\ttotalMicros: Micros;\n\t/** Convenience: `totalMicros / 1_000_000` as a number. */\n\ttotalUnits: number;\n\t/** Plan-level metadata copied through unchanged. */\n\tmetadata?: Record<string, string>;\n};\n\n// =============================================================================\n// Pricing math\n// =============================================================================\n\ntype ComputeDimensionInput = {\n\tquantity: number;\n\tdim: PricedDimension;\n\trounding: Rounding;\n};\n\ntype ComputeDimensionResult = {\n\tamountMicros: Micros;\n\tchargedQuantity: number;\n\ttierBreakdown?: LineItem['tierBreakdown'];\n};\n\nconst computeDimension = ({\n\tquantity,\n\tdim,\n\trounding\n}: ComputeDimensionInput): ComputeDimensionResult => {\n\tconst free = dim.freeTier ?? 0;\n\tconst charged = Math.max(0, quantity - free);\n\tconst unit = dim.unit ?? 1;\n\tconst chargedUnits = unit === 1 ? charged : charged / unit;\n\n\tif (dim.perUnitMicros !== undefined) {\n\t\tconst amountMicros = roundMicros(\n\t\t\tchargedUnits * dim.perUnitMicros,\n\t\t\trounding\n\t\t);\n\t\treturn { amountMicros, chargedQuantity: charged };\n\t}\n\n\tif (dim.price !== undefined) {\n\t\tconst amountMicros = roundMicros(dim.price(charged), rounding);\n\t\treturn { amountMicros, chargedQuantity: charged };\n\t}\n\n\t// Tiered pricing — walk tiers, allocate chargedUnits into bands.\n\tconst tierBreakdown: NonNullable<LineItem['tierBreakdown']> = [];\n\tlet remaining = chargedUnits;\n\tlet bandFloor = 0;\n\tlet totalMicros = 0;\n\tfor (let i = 0; i < dim.tiers!.length && remaining > 0; i += 1) {\n\t\tconst tier = dim.tiers![i]!;\n\t\tconst bandWidth = tier.upTo - bandFloor;\n\t\tconst unitsInTier = Math.min(remaining, bandWidth);\n\t\tif (unitsInTier > 0) {\n\t\t\tconst tierMicros = roundMicros(\n\t\t\t\tunitsInTier * tier.perUnitMicros,\n\t\t\t\trounding\n\t\t\t);\n\t\t\ttierBreakdown.push({\n\t\t\t\tamountMicros: tierMicros,\n\t\t\t\tperUnitMicros: tier.perUnitMicros,\n\t\t\t\ttierIndex: i,\n\t\t\t\tunitsInTier\n\t\t\t});\n\t\t\ttotalMicros += tierMicros;\n\t\t}\n\t\tremaining -= unitsInTier;\n\t\tbandFloor = tier.upTo;\n\t}\n\treturn {\n\t\tamountMicros: totalMicros,\n\t\tchargedQuantity: charged,\n\t\ttierBreakdown\n\t};\n};\n\n// =============================================================================\n// computeInvoice — pure\n// =============================================================================\n\nexport type ComputeInvoiceInput = {\n\tplan: Plan;\n\ttenant: string;\n\tperiod: InvoicePeriod;\n\t/** Metered numbers keyed by the same names as `plan.pricedDimensions`. */\n\tusage: Record<string, number>;\n\t/** Override the plan's currency (e.g. for tenant-local invoicing). */\n\tcurrency?: string;\n};\n\nexport const computeInvoice = ({\n\tplan,\n\ttenant,\n\tperiod,\n\tusage,\n\tcurrency\n}: ComputeInvoiceInput): Invoice => {\n\tconst rounding = plan.rounding ?? 'truncate';\n\tconst lineItems: LineItem[] = [];\n\n\tif (plan.basePriceMicros !== undefined && plan.basePriceMicros > 0) {\n\t\tlineItems.push({\n\t\t\tamountMicros: plan.basePriceMicros,\n\t\t\tchargedQuantity: 1,\n\t\t\tkey: 'base',\n\t\t\tlabel: `${plan.name} base fee`,\n\t\t\tquantity: 1\n\t\t});\n\t}\n\n\tfor (const [key, dim] of Object.entries(plan.pricedDimensions)) {\n\t\tconst quantity = usage[key] ?? 0;\n\t\tif (!Number.isFinite(quantity) || quantity < 0) continue;\n\t\tconst result = computeDimension({ dim, quantity, rounding });\n\t\tif (result.amountMicros === 0 && result.chargedQuantity === 0) continue;\n\t\tconst item: LineItem = {\n\t\t\tamountMicros: result.amountMicros,\n\t\t\tchargedQuantity: result.chargedQuantity,\n\t\t\tkey,\n\t\t\tlabel: dim.label ?? key,\n\t\t\tquantity\n\t\t};\n\t\tif (dim.freeTier !== undefined) item.freeTier = dim.freeTier;\n\t\tif (result.tierBreakdown !== undefined && result.tierBreakdown.length > 0) {\n\t\t\titem.tierBreakdown = result.tierBreakdown;\n\t\t}\n\t\tlineItems.push(item);\n\t}\n\n\tlet totalMicros = lineItems.reduce(\n\t\t(sum, item) => sum + item.amountMicros,\n\t\t0\n\t);\n\n\tconst floor = plan.minimumChargeMicros ?? 0;\n\tif (floor > 0 && totalMicros < floor) {\n\t\tconst gap = floor - totalMicros;\n\t\tlineItems.push({\n\t\t\tamountMicros: gap,\n\t\t\tchargedQuantity: 1,\n\t\t\tkey: 'minimum-charge-adjustment',\n\t\t\tlabel: 'Minimum charge adjustment',\n\t\t\tquantity: 1\n\t\t});\n\t\ttotalMicros = floor;\n\t}\n\n\tconst invoice: Invoice = {\n\t\tcurrency: currency ?? plan.currency ?? 'usd',\n\t\tlineItems,\n\t\tperiod,\n\t\tplan: plan.name,\n\t\ttenant,\n\t\ttotalMicros,\n\t\ttotalUnits: totalMicros / 1_000_000\n\t};\n\tif (plan.metadata !== undefined) invoice.metadata = plan.metadata;\n\treturn invoice;\n};\n\n// =============================================================================\n// Display helpers\n// =============================================================================\n\n/**\n * Format an integer micros amount as a human currency string. Pure\n * — no Intl side effects. For locales / advanced formatting, pipe\n * through `Intl.NumberFormat` yourself.\n */\nexport const formatMicros = (\n\tamount: Micros,\n\tcurrency: string,\n\t{ minorUnits = 2 }: { minorUnits?: number } = {}\n): string => {\n\tconst sign = amount < 0 ? '-' : '';\n\tconst abs = Math.abs(amount);\n\tconst wholeMicrosPerMinor = 10 ** (6 - minorUnits);\n\tconst minorTotal = Math.round(abs / wholeMicrosPerMinor);\n\tconst divisor = 10 ** minorUnits;\n\tconst whole = Math.trunc(minorTotal / divisor);\n\tconst upper = currency.toUpperCase();\n\tif (minorUnits === 0) return `${sign}${whole} ${upper}`;\n\tconst fraction = minorTotal % divisor;\n\tconst fractionStr = fraction.toString().padStart(minorUnits, '0');\n\treturn `${sign}${whole}.${fractionStr} ${upper}`;\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "1E2BA78C0466F20664756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/billing",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/absolutejs/billing.git"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"license": "BSL-1.1",
|
|
14
|
+
"author": "Alex Kahn",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"files": ["dist", "README.md"],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json",
|
|
28
|
+
"test": "bun test tests/",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
31
|
+
"check:package": "bun run typecheck && bun run build && bun run test",
|
|
32
|
+
"release": "bun run format && bun run check:package && bun publish"
|
|
33
|
+
},
|
|
34
|
+
"keywords": ["absolutejs", "billing", "cost-model", "metering", "invoicing", "saas", "pricing"],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"bun-types": "^1.3.14"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/bun": "^1.3.14",
|
|
40
|
+
"prettier": "^3.8.3",
|
|
41
|
+
"typescript": "^5.9.0"
|
|
42
|
+
}
|
|
43
|
+
}
|