@ingram-tech/nk-billing 0.2.1 → 0.3.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/README.md +18 -5
- package/dist/balance.d.ts +16 -6
- package/dist/balance.d.ts.map +1 -1
- package/dist/balance.js +35 -8
- package/dist/balance.js.map +1 -1
- package/dist/checkout.d.ts.map +1 -1
- package/dist/checkout.js +3 -1
- package/dist/checkout.js.map +1 -1
- package/dist/credits.d.ts +14 -2
- package/dist/credits.d.ts.map +1 -1
- package/dist/credits.js +41 -9
- package/dist/credits.js.map +1 -1
- package/dist/customers.d.ts.map +1 -1
- package/dist/customers.js +12 -2
- package/dist/customers.js.map +1 -1
- package/dist/subscription.d.ts.map +1 -1
- package/dist/subscription.js +3 -1
- package/dist/subscription.js.map +1 -1
- package/migrations/0002_billing_status_order.sql +10 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -79,14 +79,27 @@ export async function POST(request: Request) {
|
|
|
79
79
|
|
|
80
80
|
The ledger takes the DB connection by **injection** and leaves isolation to you —
|
|
81
81
|
wrap each call in `withTenant(orgId, db => spendCredits(db, …))` under RLS, or a
|
|
82
|
-
plain transaction under app-layer filtering. It owns two tables; apply
|
|
83
|
-
`migrations
|
|
84
|
-
RLS policy if your stack uses one. See [`src/credits.ts`](./src/credits.ts).
|
|
82
|
+
plain transaction under app-layer filtering. It owns two tables; apply the
|
|
83
|
+
`migrations/*.sql` files in order (Drizzle-composable fragments) and add your
|
|
84
|
+
own RLS policy if your stack uses one. See [`src/credits.ts`](./src/credits.ts).
|
|
85
|
+
|
|
86
|
+
Two semantics to know before wiring it up:
|
|
87
|
+
|
|
88
|
+
- **Entitlement gates the balance.** `spendCredits` requires an active
|
|
89
|
+
subscription or a live trial *before* it looks at credits: a one-time credit
|
|
90
|
+
pack bought by a tenant with no subscription and an expired trial is not
|
|
91
|
+
spendable. If your site sells packs standalone, gate pack checkout on
|
|
92
|
+
entitlement — or pass `activeStatuses` semantics that match your model.
|
|
93
|
+
- **Webhook ordering.** Pass `event.created` as `eventCreated` to
|
|
94
|
+
`recordSubscriptionStatus` (and apply `0002_billing_status_order.sql`) so a
|
|
95
|
+
delayed, older `customer.subscription.updated` can't overwrite the status a
|
|
96
|
+
later `deleted` event already recorded.
|
|
85
97
|
|
|
86
98
|
## Full surface
|
|
87
99
|
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
The per-export reference is the JSDoc on each export — every entry point in
|
|
101
|
+
[`src/index.ts`](./src/index.ts) and [`src/credits.ts`](./src/credits.ts) is
|
|
102
|
+
documented at the definition.
|
|
90
103
|
|
|
91
104
|
## License
|
|
92
105
|
|
package/dist/balance.d.ts
CHANGED
|
@@ -16,13 +16,23 @@ export interface Balance {
|
|
|
16
16
|
currency: string;
|
|
17
17
|
}
|
|
18
18
|
/** Read a customer's available wallet balance (Stripe's negative credit balance,
|
|
19
|
-
* sign-flipped to positive
|
|
19
|
+
* sign-flipped to positive and clamped at zero — a positive Stripe balance is
|
|
20
|
+
* an amount owed, not available credit). A deleted customer reads as zero. */
|
|
20
21
|
export declare function readBalance(customerId: string, stripe?: Stripe): Promise<Balance>;
|
|
21
|
-
/** Debit the wallet by `amountCents
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
/** Debit the wallet by `amountCents`, throwing {@link BillingError}
|
|
23
|
+
* (`insufficient_credits`) when the customer's credit doesn't cover it.
|
|
24
|
+
*
|
|
25
|
+
* Stripe does NOT reject a debit that pushes the balance positive (a positive
|
|
26
|
+
* balance is simply owed on the next invoice — which a PAYG site never
|
|
27
|
+
* issues), so the guarantee is enforced here: after the debit we check the
|
|
28
|
+
* transaction's `ending_balance`, and if the customer would owe money we
|
|
29
|
+
* reverse the debit and throw. Two concurrent overspends both apply, then
|
|
30
|
+
* both observe the positive balance and reverse — the wallet converges to its
|
|
31
|
+
* pre-race state minus at most the covered amount.
|
|
32
|
+
*
|
|
33
|
+
* Idempotency-keyed by `idempotencyTag` so retries can't double-debit — tie
|
|
34
|
+
* the tag to the action that triggered the spend (e.g. the operation's own
|
|
35
|
+
* idempotency key). */
|
|
26
36
|
export declare function debitBalance(opts: {
|
|
27
37
|
customerId: string;
|
|
28
38
|
amountCents: number;
|
package/dist/balance.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"balance.d.ts","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"balance.d.ts","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAIjC,MAAM,WAAW,OAAO;IACvB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;+EAE+E;AAC/E,wBAAsB,WAAW,CAChC,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED;;;;;;;;;;;;;wBAawB;AACxB,wBAAsB,YAAY,CACjC,IAAI,EAAE;IACL,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,EACD,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
package/dist/balance.js
CHANGED
|
@@ -9,26 +9,53 @@
|
|
|
9
9
|
* We normalise Stripe's sign convention to "amount available" (positive cents)
|
|
10
10
|
* so callers never have to remember that a credit is negative.
|
|
11
11
|
*/
|
|
12
|
+
import { BillingError } from "./errors.js";
|
|
12
13
|
import { getStripe } from "./client.js";
|
|
13
14
|
/** Read a customer's available wallet balance (Stripe's negative credit balance,
|
|
14
|
-
* sign-flipped to positive
|
|
15
|
+
* sign-flipped to positive and clamped at zero — a positive Stripe balance is
|
|
16
|
+
* an amount owed, not available credit). A deleted customer reads as zero. */
|
|
15
17
|
export async function readBalance(customerId, stripe = getStripe()) {
|
|
16
18
|
const customer = await stripe.customers.retrieve(customerId);
|
|
17
19
|
if (customer.deleted)
|
|
18
20
|
return { amount: 0, currency: "eur" };
|
|
19
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
amount: Math.max(0, -customer.balance),
|
|
23
|
+
currency: customer.currency ?? "eur",
|
|
24
|
+
};
|
|
20
25
|
}
|
|
21
|
-
/** Debit the wallet by `amountCents
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
+
/** Debit the wallet by `amountCents`, throwing {@link BillingError}
|
|
27
|
+
* (`insufficient_credits`) when the customer's credit doesn't cover it.
|
|
28
|
+
*
|
|
29
|
+
* Stripe does NOT reject a debit that pushes the balance positive (a positive
|
|
30
|
+
* balance is simply owed on the next invoice — which a PAYG site never
|
|
31
|
+
* issues), so the guarantee is enforced here: after the debit we check the
|
|
32
|
+
* transaction's `ending_balance`, and if the customer would owe money we
|
|
33
|
+
* reverse the debit and throw. Two concurrent overspends both apply, then
|
|
34
|
+
* both observe the positive balance and reverse — the wallet converges to its
|
|
35
|
+
* pre-race state minus at most the covered amount.
|
|
36
|
+
*
|
|
37
|
+
* Idempotency-keyed by `idempotencyTag` so retries can't double-debit — tie
|
|
38
|
+
* the tag to the action that triggered the spend (e.g. the operation's own
|
|
39
|
+
* idempotency key). */
|
|
26
40
|
export async function debitBalance(opts, stripe = getStripe()) {
|
|
27
|
-
|
|
41
|
+
if (!Number.isSafeInteger(opts.amountCents) || opts.amountCents <= 0) {
|
|
42
|
+
// A negative amount would silently *credit* the wallet.
|
|
43
|
+
throw new RangeError(`debitBalance amountCents must be a positive integer, got ${opts.amountCents}`);
|
|
44
|
+
}
|
|
45
|
+
const txn = await stripe.customers.createBalanceTransaction(opts.customerId, {
|
|
28
46
|
amount: opts.amountCents,
|
|
29
47
|
currency: opts.currency,
|
|
30
48
|
description: opts.description,
|
|
31
49
|
metadata: opts.metadata ?? {},
|
|
32
50
|
}, { idempotencyKey: `bal_debit_${opts.idempotencyTag}` });
|
|
51
|
+
if (txn.ending_balance > 0) {
|
|
52
|
+
await stripe.customers.createBalanceTransaction(opts.customerId, {
|
|
53
|
+
amount: -opts.amountCents,
|
|
54
|
+
currency: opts.currency,
|
|
55
|
+
description: `Reversal (insufficient balance): ${opts.description}`,
|
|
56
|
+
metadata: opts.metadata ?? {},
|
|
57
|
+
}, { idempotencyKey: `bal_debit_reversal_${opts.idempotencyTag}` });
|
|
58
|
+
throw new BillingError("insufficient_credits", "Insufficient balance for this debit.");
|
|
59
|
+
}
|
|
33
60
|
}
|
|
34
61
|
//# sourceMappingURL=balance.js.map
|
package/dist/balance.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"balance.js","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAQxC;
|
|
1
|
+
{"version":3,"file":"balance.js","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAQxC;;+EAE+E;AAC/E,MAAM,CAAC,KAAK,UAAU,WAAW,CAChC,UAAkB,EAClB,SAAiB,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC7D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC5D,OAAO;QACN,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC;QACtC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,IAAI,KAAK;KACpC,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;wBAawB;AACxB,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,IAOC,EACD,SAAiB,SAAS,EAAE;IAE5B,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,EAAE,CAAC;QACtE,wDAAwD;QACxD,MAAM,IAAI,UAAU,CACnB,4DAA4D,IAAI,CAAC,WAAW,EAAE,CAC9E,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,wBAAwB,CAC1D,IAAI,CAAC,UAAU,EACf;QACC,MAAM,EAAE,IAAI,CAAC,WAAW;QACxB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;KAC7B,EACD,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC,cAAc,EAAE,EAAE,CACtD,CAAC;IACF,IAAI,GAAG,CAAC,cAAc,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,MAAM,CAAC,SAAS,CAAC,wBAAwB,CAC9C,IAAI,CAAC,UAAU,EACf;YACC,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,WAAW,EAAE,oCAAoC,IAAI,CAAC,WAAW,EAAE;YACnE,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;SAC7B,EACD,EAAE,cAAc,EAAE,sBAAsB,IAAI,CAAC,cAAc,EAAE,EAAE,CAC/D,CAAC;QACF,MAAM,IAAI,YAAY,CACrB,sBAAsB,EACtB,sCAAsC,CACtC,CAAC;IACH,CAAC;AACF,CAAC"}
|
package/dist/checkout.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,WAAW,EAEhB,MAAM,gBAAgB,CAAC;AAGxB,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,QAAQ,EAAE,WAAW,CAAC;IACtB,mEAAmE;IACnE,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;uDACmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;oFACgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvD;4DACwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;;;gFAIgF;AAChF,wBAAsB,qBAAqB,CAC1C,IAAI,EAAE,eAAe,EACrB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,WAAW,EAEhB,MAAM,gBAAgB,CAAC;AAGxB,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,QAAQ,EAAE,WAAW,CAAC;IACtB,mEAAmE;IACnE,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;uDACmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;oFACgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvD;4DACwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;;;gFAIgF;AAChF,wBAAsB,qBAAqB,CAC1C,IAAI,EAAE,eAAe,EACrB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAuCjB;AAED,6EAA6E;AAC7E,wBAAsB,mBAAmB,CACxC,IAAI,EAAE;IACL,QAAQ,EAAE,WAAW,CAAC;IACtB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAWjB"}
|
package/dist/checkout.js
CHANGED
|
@@ -21,9 +21,11 @@ export async function createCheckoutSession(opts, stripe = getStripe()) {
|
|
|
21
21
|
const currency = opts.currency && supportedCurrencies(price).has(opts.currency)
|
|
22
22
|
? opts.currency
|
|
23
23
|
: undefined;
|
|
24
|
+
// The ref tag is spread last: the webhook side resolves the tenant from it,
|
|
25
|
+
// so caller metadata must never be able to overwrite it.
|
|
24
26
|
const metadata = {
|
|
25
|
-
[opts.customer.metadataKey]: opts.customer.id,
|
|
26
27
|
...opts.metadata,
|
|
28
|
+
[opts.customer.metadataKey]: opts.customer.id,
|
|
27
29
|
};
|
|
28
30
|
const session = await stripe.checkout.sessions.create({
|
|
29
31
|
mode: opts.mode,
|
package/dist/checkout.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkout.js","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAGN,oBAAoB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA8BpE;;;;gFAIgF;AAChF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAC1C,IAAqB,EACrB,SAAiB,SAAS,EAAE;IAE5B,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC3C,oBAAoB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC;QACjE,gBAAgB,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC;KACxC,CAAC,CAAC;IAEH,MAAM,QAAQ,GACb,IAAI,CAAC,QAAQ,IAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC7D,CAAC,CAAC,IAAI,CAAC,QAAQ;QACf,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,QAAQ,GAAG;QAChB,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;
|
|
1
|
+
{"version":3,"file":"checkout.js","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAGN,oBAAoB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA8BpE;;;;gFAIgF;AAChF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAC1C,IAAqB,EACrB,SAAiB,SAAS,EAAE;IAE5B,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC3C,oBAAoB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC;QACjE,gBAAgB,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC;KACxC,CAAC,CAAC;IAEH,MAAM,QAAQ,GACb,IAAI,CAAC,QAAQ,IAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC7D,CAAC,CAAC,IAAI,CAAC,QAAQ;QACf,CAAC,CAAC,SAAS,CAAC;IAEd,4EAA4E;IAC5E,yDAAyD;IACzD,MAAM,QAAQ,GAAG;QAChB,GAAG,IAAI,CAAC,QAAQ;QAChB,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;KAC7C,CAAC;IAEF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;QACrD,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC;QAC/D,qBAAqB,EAAE,IAAI,CAAC,mBAAmB,IAAI,IAAI;QACvD,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM;QAC7B,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,IAAI,CAAC,YAAY;YACpB,CAAC,CAAC;gBACA,iBAAiB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;gBACpC,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;aACjC;YACF,CAAC,CAAC,EAAE,CAAC;QACN,WAAW,EAAE,IAAI,CAAC,UAAU;QAC5B,UAAU,EAAE,IAAI,CAAC,SAAS;QAC1B,QAAQ;QACR,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5E,CAAC,CAAC;IAEH,IAAI,CAAC,OAAO,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1E,OAAO,OAAO,CAAC,GAAG,CAAC;AACpB,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,IAIC,EACD,SAAiB,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAC1C,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,eAAe,EACpB,MAAM,CACN,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC1D,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,UAAU,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAC;IACH,OAAO,OAAO,CAAC,GAAG,CAAC;AACpB,CAAC"}
|
package/dist/credits.d.ts
CHANGED
|
@@ -67,10 +67,13 @@ export declare function spendCredits(db: Queryable, opts: SpendOptions): Promise
|
|
|
67
67
|
export declare function requireCredits(db: Queryable, opts: SpendOptions): Promise<void>;
|
|
68
68
|
/** Return previously-spent credits — call when the work a spend paid for did not
|
|
69
69
|
* run (a dedup hit, a hard failure, an aborted enqueue). No-op for non-positive
|
|
70
|
-
* amounts.
|
|
70
|
+
* amounts. Pass `eventId` (any stable id for the failure being compensated) to
|
|
71
|
+
* make the refund idempotent — a retried failure handler then can't
|
|
72
|
+
* double-refund. Without it, retries are the caller's problem. */
|
|
71
73
|
export declare function refundCredits(db: Queryable, opts: {
|
|
72
74
|
tenantId: string;
|
|
73
75
|
amount: number;
|
|
76
|
+
eventId?: string;
|
|
74
77
|
}): Promise<void>;
|
|
75
78
|
/**
|
|
76
79
|
* Mark a Stripe event processed, idempotently. Returns false if the event was
|
|
@@ -92,12 +95,21 @@ export declare function grantCredits(db: Queryable, opts: {
|
|
|
92
95
|
stripeCustomerId?: string | null;
|
|
93
96
|
}): Promise<boolean>;
|
|
94
97
|
/** Cache a tenant's subscription status from a webhook, idempotently per event.
|
|
95
|
-
* Returns false if the event was already processed.
|
|
98
|
+
* Returns false if the event was already processed.
|
|
99
|
+
*
|
|
100
|
+
* Stripe does not guarantee delivery order, so pass the event's `created`
|
|
101
|
+
* timestamp (Unix seconds — `event.created`): a delayed older event (e.g. an
|
|
102
|
+
* `updated: active` arriving after `deleted: canceled`) is then ignored
|
|
103
|
+
* instead of resurrecting a dead subscription indefinitely. Requires migration
|
|
104
|
+
* `0002_billing_status_order.sql`; without `eventCreated` the last-write-wins
|
|
105
|
+
* legacy behavior is kept. */
|
|
96
106
|
export declare function recordSubscriptionStatus(db: Queryable, opts: {
|
|
97
107
|
tenantId: string;
|
|
98
108
|
eventId: string;
|
|
99
109
|
eventType: string;
|
|
100
110
|
status: string;
|
|
111
|
+
/** The Stripe event's `created` (Unix seconds), for out-of-order defense. */
|
|
112
|
+
eventCreated?: number;
|
|
101
113
|
stripeCustomerId?: string | null;
|
|
102
114
|
}): Promise<boolean>;
|
|
103
115
|
export interface BillingSummary {
|
package/dist/credits.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credits.d.ts","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEzE;oDACoD;AACpD,MAAM,WAAW,SAAS;IAIzB,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,OAAO,EAAE,GAChB,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;CAC1B;AAED;2DAC2D;AAC3D,MAAM,WAAW,WAAW;IAC3B,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,WAAW,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"credits.d.ts","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"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"}
|
package/dist/credits.js
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
import { BillingError } from "./errors.js";
|
|
22
22
|
import { ACTIVE_SUBSCRIPTION_STATUSES } from "./status.js";
|
|
23
23
|
export { BillingError, denyMessage } from "./errors.js";
|
|
24
|
+
// `credits_balance` is a Postgres bigint, which `pg` hands back as a string.
|
|
25
|
+
// Coerce before it leaks into arithmetic (string concatenation) or JSON.
|
|
26
|
+
const toBalance = (value) => Number(value);
|
|
24
27
|
function entitled(row, now, trialWindowMs, activeStatuses) {
|
|
25
28
|
if (row.subscription_status && activeStatuses.has(row.subscription_status)) {
|
|
26
29
|
return true;
|
|
@@ -44,6 +47,10 @@ function entitled(row, now, trialWindowMs, activeStatuses) {
|
|
|
44
47
|
*/
|
|
45
48
|
export async function spendCredits(db, opts) {
|
|
46
49
|
const activeStatuses = opts.activeStatuses ?? ACTIVE_SUBSCRIPTION_STATUSES;
|
|
50
|
+
if (!Number.isSafeInteger(opts.cost) || opts.cost < 0) {
|
|
51
|
+
// A negative cost would silently credit the tenant; NaN passes `<` checks.
|
|
52
|
+
throw new RangeError(`spendCredits cost must be a non-negative integer, got ${opts.cost}`);
|
|
53
|
+
}
|
|
47
54
|
// First touch: seed the row (with trial credits + clock when trials are on).
|
|
48
55
|
// ON CONFLICT DO NOTHING makes the trial a one-time grant.
|
|
49
56
|
await db.query(`insert into billing_credits (tenant_id, credits_balance, trial_started_at)
|
|
@@ -57,14 +64,14 @@ export async function spendCredits(db, opts) {
|
|
|
57
64
|
if (!entitled(row, Date.now(), opts.trial?.windowMs, activeStatuses)) {
|
|
58
65
|
return { ok: false, reason: "not_entitled" };
|
|
59
66
|
}
|
|
60
|
-
if (row.credits_balance < opts.cost) {
|
|
67
|
+
if (toBalance(row.credits_balance) < opts.cost) {
|
|
61
68
|
return { ok: false, reason: "insufficient_credits" };
|
|
62
69
|
}
|
|
63
70
|
const { rows: updated } = await db.query(`update billing_credits
|
|
64
71
|
set credits_balance = credits_balance - $2, updated_at = now()
|
|
65
72
|
where tenant_id = $1
|
|
66
73
|
returning credits_balance`, [opts.tenantId, opts.cost]);
|
|
67
|
-
return { ok: true, balance: updated[0]?.credits_balance ?? 0 };
|
|
74
|
+
return { ok: true, balance: toBalance(updated[0]?.credits_balance ?? 0) };
|
|
68
75
|
}
|
|
69
76
|
/** Like {@link spendCredits} but throws {@link BillingError} on a denial — use in
|
|
70
77
|
* API routes that map the error to a 402. */
|
|
@@ -75,10 +82,16 @@ export async function requireCredits(db, opts) {
|
|
|
75
82
|
}
|
|
76
83
|
/** Return previously-spent credits — call when the work a spend paid for did not
|
|
77
84
|
* run (a dedup hit, a hard failure, an aborted enqueue). No-op for non-positive
|
|
78
|
-
* amounts.
|
|
85
|
+
* amounts. Pass `eventId` (any stable id for the failure being compensated) to
|
|
86
|
+
* make the refund idempotent — a retried failure handler then can't
|
|
87
|
+
* double-refund. Without it, retries are the caller's problem. */
|
|
79
88
|
export async function refundCredits(db, opts) {
|
|
80
|
-
if (opts.amount <= 0)
|
|
89
|
+
if (!Number.isSafeInteger(opts.amount) || opts.amount <= 0)
|
|
90
|
+
return;
|
|
91
|
+
if (opts.eventId != null &&
|
|
92
|
+
!(await claimStripeEvent(db, opts.eventId, "credits.refund"))) {
|
|
81
93
|
return;
|
|
94
|
+
}
|
|
82
95
|
await db.query(`update billing_credits
|
|
83
96
|
set credits_balance = credits_balance + $2, updated_at = now()
|
|
84
97
|
where tenant_id = $1`, [opts.tenantId, opts.amount]);
|
|
@@ -111,16 +124,35 @@ export async function grantCredits(db, opts) {
|
|
|
111
124
|
return true;
|
|
112
125
|
}
|
|
113
126
|
/** Cache a tenant's subscription status from a webhook, idempotently per event.
|
|
114
|
-
* Returns false if the event was already processed.
|
|
127
|
+
* Returns false if the event was already processed.
|
|
128
|
+
*
|
|
129
|
+
* Stripe does not guarantee delivery order, so pass the event's `created`
|
|
130
|
+
* timestamp (Unix seconds — `event.created`): a delayed older event (e.g. an
|
|
131
|
+
* `updated: active` arriving after `deleted: canceled`) is then ignored
|
|
132
|
+
* instead of resurrecting a dead subscription indefinitely. Requires migration
|
|
133
|
+
* `0002_billing_status_order.sql`; without `eventCreated` the last-write-wins
|
|
134
|
+
* legacy behavior is kept. */
|
|
115
135
|
export async function recordSubscriptionStatus(db, opts) {
|
|
116
136
|
if (!(await claimStripeEvent(db, opts.eventId, opts.eventType)))
|
|
117
137
|
return false;
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
if (opts.eventCreated == null) {
|
|
139
|
+
await db.query(`insert into billing_credits (tenant_id, subscription_status, stripe_customer_id)
|
|
140
|
+
values ($1, $2, $3)
|
|
141
|
+
on conflict (tenant_id) do update set
|
|
142
|
+
subscription_status = excluded.subscription_status,
|
|
143
|
+
stripe_customer_id = coalesce(excluded.stripe_customer_id, billing_credits.stripe_customer_id),
|
|
144
|
+
updated_at = now()`, [opts.tenantId, opts.status, opts.stripeCustomerId ?? null]);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
await db.query(`insert into billing_credits (tenant_id, subscription_status, subscription_status_at, stripe_customer_id)
|
|
148
|
+
values ($1, $2, to_timestamp($3), $4)
|
|
120
149
|
on conflict (tenant_id) do update set
|
|
121
150
|
subscription_status = excluded.subscription_status,
|
|
151
|
+
subscription_status_at = excluded.subscription_status_at,
|
|
122
152
|
stripe_customer_id = coalesce(excluded.stripe_customer_id, billing_credits.stripe_customer_id),
|
|
123
|
-
updated_at = now()
|
|
153
|
+
updated_at = now()
|
|
154
|
+
where billing_credits.subscription_status_at is null
|
|
155
|
+
or excluded.subscription_status_at >= billing_credits.subscription_status_at`, [opts.tenantId, opts.status, opts.eventCreated, opts.stripeCustomerId ?? null]);
|
|
124
156
|
return true;
|
|
125
157
|
}
|
|
126
158
|
/** Read-only entitlement + balance snapshot for the billing UI. Does not create a
|
|
@@ -144,7 +176,7 @@ export async function getBillingSummary(db, opts) {
|
|
|
144
176
|
? new Date(row.trial_started_at).getTime() + opts.trialWindowMs
|
|
145
177
|
: null;
|
|
146
178
|
return {
|
|
147
|
-
creditsBalance: row.credits_balance,
|
|
179
|
+
creditsBalance: toBalance(row.credits_balance),
|
|
148
180
|
subscriptionStatus: row.subscription_status,
|
|
149
181
|
stripeCustomerId: row.stripe_customer_id,
|
|
150
182
|
entitled: entitled(row, Date.now(), opts.trialWindowMs, activeStatuses),
|
package/dist/credits.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credits.js","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,YAAY,EAAmB,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAmB,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"credits.js","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"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"}
|
package/dist/customers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"customers.d.ts","sourceRoot":"","sources":["../src/customers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC3B;oEACgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,GAAG,IAAI,CAAC;CACT;
|
|
1
|
+
{"version":3,"file":"customers.d.ts","sourceRoot":"","sources":["../src/customers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC3B;oEACgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,GAAG,IAAI,CAAC;CACT;AAQD;;gBAEgB;AAChB,wBAAsB,YAAY,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC,CAMjC;AAED;;eAEe;AACf,wBAAsB,oBAAoB,CACzC,GAAG,EAAE,WAAW,EAChB,OAAO,GAAE,eAAoB,EAC7B,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAuC1B;AAED;gEACgE;AAChE,wBAAsB,yBAAyB,CAC9C,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED;8CAC8C;AAC9C,wBAAsB,4BAA4B,CACjD,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE;IACL,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;KAChB,CAAC;CACF,EACD,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAKf"}
|
package/dist/customers.js
CHANGED
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
* baked in here — that is the framework seam.
|
|
10
10
|
*/
|
|
11
11
|
import { getStripe } from "./client.js";
|
|
12
|
+
// Stripe's search syntax delimits values with single quotes; an unescaped
|
|
13
|
+
// quote in an interpolated value would let a crafted id alter the query (it
|
|
14
|
+
// supports OR) and match another tenant's customer.
|
|
15
|
+
const escapeSearchValue = (value) => value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
12
16
|
/** The customer for `ref`, found by metadata search, or null if none exists yet.
|
|
13
17
|
* Read paths use this so merely viewing a billing page never mints an empty
|
|
14
18
|
* customer. */
|
|
15
19
|
export async function findCustomer(ref, stripe = getStripe()) {
|
|
16
20
|
const found = await stripe.customers.search({
|
|
17
|
-
query: `metadata['${ref.metadataKey}']:'${ref.id}'`,
|
|
21
|
+
query: `metadata['${escapeSearchValue(ref.metadataKey)}']:'${escapeSearchValue(ref.id)}'`,
|
|
18
22
|
limit: 1,
|
|
19
23
|
});
|
|
20
24
|
return found.data[0] ?? null;
|
|
@@ -39,7 +43,13 @@ export async function findOrCreateCustomer(ref, details = {}, stripe = getStripe
|
|
|
39
43
|
country: details.address.country ?? undefined,
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
|
-
|
|
46
|
+
// Idempotency-keyed on the ref: `customers.search` is eventually consistent
|
|
47
|
+
// (freshly created customers can be invisible for up to ~a minute), so two
|
|
48
|
+
// concurrent find-then-create calls both miss — the key makes Stripe hand
|
|
49
|
+
// both the same customer instead of minting a duplicate.
|
|
50
|
+
const customer = await stripe.customers.create(params, {
|
|
51
|
+
idempotencyKey: `cust_create_${ref.metadataKey}_${ref.id}`,
|
|
52
|
+
});
|
|
43
53
|
if (details.vatNumber) {
|
|
44
54
|
try {
|
|
45
55
|
await stripe.customers.createTaxId(customer.id, {
|
package/dist/customers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"customers.js","sourceRoot":"","sources":["../src/customers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAwBxC;;gBAEgB;AAChB,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,GAAgB,EAChB,SAAiB,SAAS,EAAE;IAE5B,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QAC3C,KAAK,EAAE,aAAa,GAAG,CAAC,WAAW,OAAO,GAAG,CAAC,EAAE,GAAG;
|
|
1
|
+
{"version":3,"file":"customers.js","sourceRoot":"","sources":["../src/customers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAwBxC,0EAA0E;AAC1E,4EAA4E;AAC5E,oDAAoD;AACpD,MAAM,iBAAiB,GAAG,CAAC,KAAa,EAAU,EAAE,CACnD,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAEnD;;gBAEgB;AAChB,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,GAAgB,EAChB,SAAiB,SAAS,EAAE;IAE5B,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QAC3C,KAAK,EAAE,aAAa,iBAAiB,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG;QACzF,KAAK,EAAE,CAAC;KACR,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC9B,CAAC;AAED;;eAEe;AACf,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACzC,GAAgB,EAChB,UAA2B,EAAE,EAC7B,SAAiB,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACjD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,MAAM,GAAgC;QAC3C,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,SAAS;QAC/B,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,SAAS;QACjC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE;KACvC,CAAC;IACF,IAAI,OAAO,CAAC,OAAO,EAAE,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACrD,MAAM,CAAC,OAAO,GAAG;YAChB,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,SAAS;YACzC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS;YACvC,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,WAAW,IAAI,SAAS;YACrD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,IAAI,SAAS;SAC7C,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,yDAAyD;IACzD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE;QACtD,cAAc,EAAE,eAAe,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,EAAE,EAAE;KAC1D,CAAC,CAAC;IAEH,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,EAAE;gBAC/C,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,OAAO,CAAC,SAAS;aACxB,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,wEAAwE;YACxE,+DAA+D;QAChE,CAAC;IACF,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED;gEACgE;AAChE,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC9C,UAAkB,EAClB,SAAiB,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC7D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AACzC,CAAC;AAED;8CAC8C;AAC9C,MAAM,CAAC,KAAK,UAAU,4BAA4B,CACjD,UAAkB,EAClB,IAQC,EACD,SAAiB,SAAS,EAAE;IAE5B,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE;QACzC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,IAAI,CAAC,OAAO;KACrB,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAIjC,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3E;oDACoD;AACpD,MAAM,WAAW,mBAAmB;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,sEAAsE;IACtE,gBAAgB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACtB;AAKD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,YAAY,GAAG,mBAAmB,CAYnF;AAED;;;sDAGsD;AACtD,wBAAsB,4BAA4B,CACjD,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAIjC,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3E;oDACoD;AACpD,MAAM,WAAW,mBAAmB;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,sEAAsE;IACtE,gBAAgB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACtB;AAKD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,YAAY,GAAG,mBAAmB,CAYnF;AAED;;;sDAGsD;AACtD,wBAAsB,4BAA4B,CACjD,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CAUrC;AAED,uFAAuF;AACvF,wBAAsB,wBAAwB,CAC7C,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAGrC"}
|
package/dist/subscription.js
CHANGED
|
@@ -34,7 +34,9 @@ export async function fetchSubscriptionForCustomer(customerId, stripe = getStrip
|
|
|
34
34
|
const subs = await stripe.subscriptions.list({
|
|
35
35
|
customer: customerId,
|
|
36
36
|
status: "all",
|
|
37
|
-
|
|
37
|
+
// Stripe's max: an active subscription older than the N most recent ones
|
|
38
|
+
// must still be found ahead of the canceled ones.
|
|
39
|
+
limit: 100,
|
|
38
40
|
expand: ["data.items.data.price"],
|
|
39
41
|
});
|
|
40
42
|
return subs.data.find((s) => isActiveStatus(s.status)) ?? subs.data[0] ?? null;
|
package/dist/subscription.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.js","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAgB3E,MAAM,MAAM,GAAG,CAAC,OAAkC,EAAe,EAAE,CAClE,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;AAEnD,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,GAAwB;IAC7D,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC/B,OAAO;QACN,cAAc,EAAE,GAAG,CAAC,EAAE;QACtB,UAAU,EAAE,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;QAC7E,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,MAAM,EAAE,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,IAAI;QAC/B,gBAAgB,EAAE,MAAM,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAClD,iBAAiB,EAAE,GAAG,CAAC,oBAAoB;QAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;KAC/B,CAAC;AACH,CAAC;AAED;;;sDAGsD;AACtD,MAAM,CAAC,KAAK,UAAU,4BAA4B,CACjD,UAAkB,EAClB,SAAiB,SAAS,EAAE;IAE5B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;QAC5C,QAAQ,EAAE,UAAU;QACpB,MAAM,EAAE,KAAK;QACb,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"subscription.js","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAgB3E,MAAM,MAAM,GAAG,CAAC,OAAkC,EAAe,EAAE,CAClE,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;AAEnD,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,GAAwB;IAC7D,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC/B,OAAO;QACN,cAAc,EAAE,GAAG,CAAC,EAAE;QACtB,UAAU,EAAE,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;QAC7E,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,MAAM,EAAE,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,IAAI;QAC/B,gBAAgB,EAAE,MAAM,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAClD,iBAAiB,EAAE,GAAG,CAAC,oBAAoB;QAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;KAC/B,CAAC;AACH,CAAC;AAED;;;sDAGsD;AACtD,MAAM,CAAC,KAAK,UAAU,4BAA4B,CACjD,UAAkB,EAClB,SAAiB,SAAS,EAAE;IAE5B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;QAC5C,QAAQ,EAAE,UAAU;QACpB,MAAM,EAAE,KAAK;QACb,yEAAyE;QACzE,kDAAkD;QAClD,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,CAAC,uBAAuB,CAAC;KACjC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAChF,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC7C,UAAkB,EAClB,SAAiB,SAAS,EAAE;IAE5B,MAAM,GAAG,GAAG,MAAM,4BAA4B,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACnE,OAAO,GAAG,CAAC,CAAC,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- @ingram-tech/nk-billing — out-of-order webhook defense.
|
|
2
|
+
--
|
|
3
|
+
-- Stripe does not guarantee event delivery order. `recordSubscriptionStatus`
|
|
4
|
+
-- stamps the Stripe event's `created` timestamp here (when the caller passes
|
|
5
|
+
-- `eventCreated`) and refuses to overwrite the cached status with an older
|
|
6
|
+
-- event, so a delayed `customer.subscription.updated` can't resurrect a
|
|
7
|
+
-- subscription that a later `customer.subscription.deleted` already canceled.
|
|
8
|
+
|
|
9
|
+
alter table billing_credits
|
|
10
|
+
add column if not exists subscription_status_at timestamptz;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-tech/nk-billing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The Ingram billing foundation: composable Stripe primitives (client, customers, prices, currency, checkout, subscriptions, webhooks) plus an injection-based Postgres credit ledger and event-dedup store.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"zod": "^4.4.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@ingram-tech/nk-dev": "0.2.
|
|
40
|
+
"@ingram-tech/nk-dev": "0.2.5",
|
|
41
41
|
"@types/node": "^26.0.1",
|
|
42
42
|
"typescript": "^6.0.3",
|
|
43
43
|
"vitest": "^4.1.9"
|