@ingram-tech/nk-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/README.md +93 -0
- package/dist/balance.d.ts +34 -0
- package/dist/balance.d.ts.map +1 -0
- package/dist/balance.js +34 -0
- package/dist/balance.js.map +1 -0
- package/dist/checkout.d.ts +49 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +59 -0
- package/dist/checkout.js.map +1 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +57 -0
- package/dist/client.js.map +1 -0
- package/dist/credits.d.ts +118 -0
- package/dist/credits.d.ts.map +1 -0
- package/dist/credits.js +154 -0
- package/dist/credits.js.map +1 -0
- package/dist/currency.d.ts +30 -0
- package/dist/currency.d.ts.map +1 -0
- package/dist/currency.js +78 -0
- package/dist/currency.js.map +1 -0
- package/dist/customers.d.ts +54 -0
- package/dist/customers.d.ts.map +1 -0
- package/dist/customers.js +73 -0
- package/dist/customers.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +44 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +64 -0
- package/dist/keys.js.map +1 -0
- package/dist/prices.d.ts +23 -0
- package/dist/prices.d.ts.map +1 -0
- package/dist/prices.js +45 -0
- package/dist/prices.js.map +1 -0
- package/dist/status.d.ts +12 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +18 -0
- package/dist/status.js.map +1 -0
- package/dist/subscription.d.ts +35 -0
- package/dist/subscription.d.ts.map +1 -0
- package/dist/subscription.js +47 -0
- package/dist/subscription.js.map +1 -0
- package/dist/webhooks.d.ts +34 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +40 -0
- package/dist/webhooks.js.map +1 -0
- package/migrations/0001_billing.sql +42 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @ingram-tech/nk-billing
|
|
2
|
+
|
|
3
|
+
The Ingram billing foundation: composable **Stripe primitives** every site was
|
|
4
|
+
re-implementing, plus an optional **Postgres credit ledger** for usage metering.
|
|
5
|
+
One account-agnostic toolkit, not a billing framework that owns your flows —
|
|
6
|
+
you keep your routes, your schema, and your pricing; nk-billing removes the
|
|
7
|
+
boilerplate in between.
|
|
8
|
+
|
|
9
|
+
> Part of [nextkit](../../README.md). Stateless Stripe helpers are the main
|
|
10
|
+
> entry; the database-backed credit ledger lives behind the `/credits` subpath so
|
|
11
|
+
> a subscription-only or wallet-only site never imports a DB concept.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @ingram-tech/nk-billing stripe
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`stripe` is a direct dependency; bring your own Postgres (`pg` / nk-db) only if
|
|
20
|
+
you use the credit ledger.
|
|
21
|
+
|
|
22
|
+
## Env contract
|
|
23
|
+
|
|
24
|
+
Single-mode (most sites): `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`.
|
|
25
|
+
Dual-mode (a merchant-of-record running test beside live, e.g. cloud):
|
|
26
|
+
`STRIPE_SECRET_KEY_{TEST,LIVE}`, `STRIPE_WEBHOOK_SECRET_{TEST,LIVE}`.
|
|
27
|
+
Optional `STRIPE_API_VERSION` to pin ahead of an SDK bump.
|
|
28
|
+
|
|
29
|
+
Nothing hardcodes a price ID — prices resolve at runtime by stable Stripe
|
|
30
|
+
`lookup_key`, so the same code path works in test and live.
|
|
31
|
+
|
|
32
|
+
## The three billing models
|
|
33
|
+
|
|
34
|
+
Pick the one that matches what you sell — they compose:
|
|
35
|
+
|
|
36
|
+
- **Subscriptions** — `createCheckoutSession({ mode: "subscription" })`,
|
|
37
|
+
`createPortalSession`, `summarizeSubscription`, `fetchSubscriptionSummary`
|
|
38
|
+
(the webhook-lag self-heal read).
|
|
39
|
+
- **Stripe-side wallet** (money) — `@ingram-tech/nk-billing` `readBalance` /
|
|
40
|
+
`debitBalance`: the balance lives in Stripe, no local table.
|
|
41
|
+
- **In-app credits** (abstract units) — `@ingram-tech/nk-billing/credits`:
|
|
42
|
+
atomic `spendCredits` / `requireCredits`, `grantCredits`,
|
|
43
|
+
`recordSubscriptionStatus`, all idempotent; ships its own migration.
|
|
44
|
+
|
|
45
|
+
## Quick start — subscription checkout
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { createCheckoutSession, resolveCurrencyFromHeaders } from "@ingram-tech/nk-billing";
|
|
49
|
+
import { headers } from "next/headers";
|
|
50
|
+
|
|
51
|
+
const url = await createCheckoutSession({
|
|
52
|
+
customer: { metadataKey: "acme_org_id", id: orgId },
|
|
53
|
+
customerDetails: { name: org.name, email: user.email },
|
|
54
|
+
lookupKey: "acme_pro_monthly",
|
|
55
|
+
mode: "subscription",
|
|
56
|
+
currency: resolveCurrencyFromHeaders(await headers()),
|
|
57
|
+
successUrl: `${base}/billing?ok=1`,
|
|
58
|
+
cancelUrl: `${base}/billing`,
|
|
59
|
+
});
|
|
60
|
+
redirect(url);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick start — webhook (with credit-ledger dedup)
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { readStripeWebhook } from "@ingram-tech/nk-billing";
|
|
67
|
+
import { grantCredits, recordSubscriptionStatus } from "@ingram-tech/nk-billing/credits";
|
|
68
|
+
|
|
69
|
+
export async function POST(request: Request) {
|
|
70
|
+
const res = await readStripeWebhook(request, process.env.STRIPE_WEBHOOK_SECRET ?? "");
|
|
71
|
+
if (!res.ok) return new Response(res.message, { status: res.status });
|
|
72
|
+
// dedupe + apply inside your tenant transaction; retries are no-ops.
|
|
73
|
+
await withTenant(orgId, (db) => recordSubscriptionStatus(db, { ... }));
|
|
74
|
+
return Response.json({ received: true });
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Credit ledger & tenancy
|
|
79
|
+
|
|
80
|
+
The ledger takes the DB connection by **injection** and leaves isolation to you —
|
|
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/0001_billing.sql` (a Drizzle-composable fragment) and add your own
|
|
84
|
+
RLS policy if your stack uses one. See [`src/credits.ts`](./src/credits.ts).
|
|
85
|
+
|
|
86
|
+
## Full surface
|
|
87
|
+
|
|
88
|
+
See the [nk-billing guide](../../../nk-billing.md) for the exhaustive,
|
|
89
|
+
per-export reference and migration recipes for each consuming app.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
[MIT](../../LICENSE) © Ingram Technologies
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer-balance wallet — Stripe's own credit ledger, for pay-as-you-go sites
|
|
3
|
+
* (domains.ingram.tech). Distinct from the in-app credit ledger in
|
|
4
|
+
* `@ingram-tech/nk-billing/credits`: this stores the balance *in Stripe* as a
|
|
5
|
+
* signed integer (negative = credit available to the customer), so there is no
|
|
6
|
+
* local table to keep in sync. Pick this when the unit you sell is money;
|
|
7
|
+
* pick the credit ledger when the unit is abstract "credits".
|
|
8
|
+
*
|
|
9
|
+
* We normalise Stripe's sign convention to "amount available" (positive cents)
|
|
10
|
+
* so callers never have to remember that a credit is negative.
|
|
11
|
+
*/
|
|
12
|
+
import type Stripe from "stripe";
|
|
13
|
+
export interface Balance {
|
|
14
|
+
/** Credit available to the customer, in minor units (cents). Positive. */
|
|
15
|
+
amount: number;
|
|
16
|
+
currency: string;
|
|
17
|
+
}
|
|
18
|
+
/** Read a customer's available wallet balance (Stripe's negative credit balance,
|
|
19
|
+
* sign-flipped to positive). A deleted customer reads as zero. */
|
|
20
|
+
export declare function readBalance(customerId: string, stripe?: Stripe): Promise<Balance>;
|
|
21
|
+
/** Debit the wallet by `amountCents`. Stripe rejects a debit that would push the
|
|
22
|
+
* balance positive (the customer would owe us), giving a hard over-spend
|
|
23
|
+
* guarantee with no pre-flight check. Idempotency-keyed by `idempotencyTag` so
|
|
24
|
+
* retries can't double-debit — tie the tag to the action that triggered the
|
|
25
|
+
* spend (e.g. the operation's own idempotency key). */
|
|
26
|
+
export declare function debitBalance(opts: {
|
|
27
|
+
customerId: string;
|
|
28
|
+
amountCents: number;
|
|
29
|
+
currency: string;
|
|
30
|
+
description: string;
|
|
31
|
+
idempotencyTag: string;
|
|
32
|
+
metadata?: Record<string, string>;
|
|
33
|
+
}, stripe?: Stripe): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=balance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"balance.d.ts","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,MAAM,WAAW,OAAO;IACvB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;mEACmE;AACnE,wBAAsB,WAAW,CAChC,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED;;;;wDAIwD;AACxD,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,CAWf"}
|
package/dist/balance.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer-balance wallet — Stripe's own credit ledger, for pay-as-you-go sites
|
|
3
|
+
* (domains.ingram.tech). Distinct from the in-app credit ledger in
|
|
4
|
+
* `@ingram-tech/nk-billing/credits`: this stores the balance *in Stripe* as a
|
|
5
|
+
* signed integer (negative = credit available to the customer), so there is no
|
|
6
|
+
* local table to keep in sync. Pick this when the unit you sell is money;
|
|
7
|
+
* pick the credit ledger when the unit is abstract "credits".
|
|
8
|
+
*
|
|
9
|
+
* We normalise Stripe's sign convention to "amount available" (positive cents)
|
|
10
|
+
* so callers never have to remember that a credit is negative.
|
|
11
|
+
*/
|
|
12
|
+
import { getStripe } from "./client.js";
|
|
13
|
+
/** Read a customer's available wallet balance (Stripe's negative credit balance,
|
|
14
|
+
* sign-flipped to positive). A deleted customer reads as zero. */
|
|
15
|
+
export async function readBalance(customerId, stripe = getStripe()) {
|
|
16
|
+
const customer = await stripe.customers.retrieve(customerId);
|
|
17
|
+
if (customer.deleted)
|
|
18
|
+
return { amount: 0, currency: "eur" };
|
|
19
|
+
return { amount: -customer.balance, currency: customer.currency ?? "eur" };
|
|
20
|
+
}
|
|
21
|
+
/** Debit the wallet by `amountCents`. Stripe rejects a debit that would push the
|
|
22
|
+
* balance positive (the customer would owe us), giving a hard over-spend
|
|
23
|
+
* guarantee with no pre-flight check. Idempotency-keyed by `idempotencyTag` so
|
|
24
|
+
* retries can't double-debit — tie the tag to the action that triggered the
|
|
25
|
+
* spend (e.g. the operation's own idempotency key). */
|
|
26
|
+
export async function debitBalance(opts, stripe = getStripe()) {
|
|
27
|
+
await stripe.customers.createBalanceTransaction(opts.customerId, {
|
|
28
|
+
amount: opts.amountCents,
|
|
29
|
+
currency: opts.currency,
|
|
30
|
+
description: opts.description,
|
|
31
|
+
metadata: opts.metadata ?? {},
|
|
32
|
+
}, { idempotencyKey: `bal_debit_${opts.idempotencyTag}` });
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=balance.js.map
|
|
@@ -0,0 +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;mEACmE;AACnE,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,EAAE,MAAM,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,IAAI,KAAK,EAAE,CAAC;AAC5E,CAAC;AAED;;;;wDAIwD;AACxD,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,IAOC,EACD,SAAiB,SAAS,EAAE;IAE5B,MAAM,MAAM,CAAC,SAAS,CAAC,wBAAwB,CAC9C,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;AACH,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkout & Customer Portal sessions — the two hosted Stripe flows every
|
|
3
|
+
* self-serve site uses. {@link createCheckoutSession} covers both a recurring
|
|
4
|
+
* subscription (`mode: "subscription"`) and a one-time purchase such as a credit
|
|
5
|
+
* pack (`mode: "payment"`); {@link createPortalSession} opens the management
|
|
6
|
+
* portal.
|
|
7
|
+
*/
|
|
8
|
+
import type Stripe from "stripe";
|
|
9
|
+
import { type CustomerDetails, type CustomerRef } from "./customers.js";
|
|
10
|
+
export interface CheckoutOptions {
|
|
11
|
+
/** Identifies (and, if needed, creates) the Stripe customer. */
|
|
12
|
+
customer: CustomerRef;
|
|
13
|
+
/** Details used only when the customer is created on this call. */
|
|
14
|
+
customerDetails?: CustomerDetails;
|
|
15
|
+
/** Stable price lookup_key for the line item. */
|
|
16
|
+
lookupKey: string;
|
|
17
|
+
/** "subscription" for a recurring plan, "payment" for a one-time purchase. */
|
|
18
|
+
mode: "subscription" | "payment";
|
|
19
|
+
successUrl: string;
|
|
20
|
+
cancelUrl: string;
|
|
21
|
+
/** Line-item quantity (e.g. number of credit packs). Default 1. */
|
|
22
|
+
quantity?: number;
|
|
23
|
+
/** Presentment currency; applied only when the price actually offers it,
|
|
24
|
+
* otherwise the price's base currency is used. */
|
|
25
|
+
currency?: string;
|
|
26
|
+
/** Whether to let customers enter promo codes. Default true. */
|
|
27
|
+
allowPromotionCodes?: boolean;
|
|
28
|
+
/** Collect a tax id (e.g. EU VAT) at checkout. The customer must already have
|
|
29
|
+
* a billing address — see {@link customerHasBillingAddress}. Default false. */
|
|
30
|
+
collectTaxId?: boolean;
|
|
31
|
+
/** Stripe Checkout locale (e.g. "fr", "nl", "auto"). Default "auto". */
|
|
32
|
+
locale?: Stripe.Checkout.SessionCreateParams["locale"];
|
|
33
|
+
/** Extra metadata to stamp on the session (and, in subscription mode, the
|
|
34
|
+
* subscription) — merged with the customer ref tag. */
|
|
35
|
+
metadata?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
/** Create a Checkout Session and return its URL. The session is created in the
|
|
38
|
+
* caller's currency when the price supports it (Stripe picks the
|
|
39
|
+
* currency_options amount), else in the price's base currency. The customer ref
|
|
40
|
+
* tag is stamped on the session and — in subscription mode — on the subscription
|
|
41
|
+
* too, so `customer.subscription.*` webhooks carry it without a round-trip. */
|
|
42
|
+
export declare function createCheckoutSession(opts: CheckoutOptions, stripe?: Stripe): Promise<string>;
|
|
43
|
+
/** Open a Customer Portal session for managing/cancelling a subscription. */
|
|
44
|
+
export declare function createPortalSession(opts: {
|
|
45
|
+
customer: CustomerRef;
|
|
46
|
+
customerDetails?: CustomerDetails;
|
|
47
|
+
returnUrl: string;
|
|
48
|
+
}, stripe?: Stripe): Promise<string>;
|
|
49
|
+
//# sourceMappingURL=checkout.d.ts.map
|
|
@@ -0,0 +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,CAqCjB;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
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkout & Customer Portal sessions — the two hosted Stripe flows every
|
|
3
|
+
* self-serve site uses. {@link createCheckoutSession} covers both a recurring
|
|
4
|
+
* subscription (`mode: "subscription"`) and a one-time purchase such as a credit
|
|
5
|
+
* pack (`mode: "payment"`); {@link createPortalSession} opens the management
|
|
6
|
+
* portal.
|
|
7
|
+
*/
|
|
8
|
+
import { getStripe } from "./client.js";
|
|
9
|
+
import { findOrCreateCustomer, } from "./customers.js";
|
|
10
|
+
import { priceByLookupKey, supportedCurrencies } from "./prices.js";
|
|
11
|
+
/** Create a Checkout Session and return its URL. The session is created in the
|
|
12
|
+
* caller's currency when the price supports it (Stripe picks the
|
|
13
|
+
* currency_options amount), else in the price's base currency. The customer ref
|
|
14
|
+
* tag is stamped on the session and — in subscription mode — on the subscription
|
|
15
|
+
* too, so `customer.subscription.*` webhooks carry it without a round-trip. */
|
|
16
|
+
export async function createCheckoutSession(opts, stripe = getStripe()) {
|
|
17
|
+
const [customer, price] = await Promise.all([
|
|
18
|
+
findOrCreateCustomer(opts.customer, opts.customerDetails, stripe),
|
|
19
|
+
priceByLookupKey(opts.lookupKey, stripe),
|
|
20
|
+
]);
|
|
21
|
+
const currency = opts.currency && supportedCurrencies(price).has(opts.currency)
|
|
22
|
+
? opts.currency
|
|
23
|
+
: undefined;
|
|
24
|
+
const metadata = {
|
|
25
|
+
[opts.customer.metadataKey]: opts.customer.id,
|
|
26
|
+
...opts.metadata,
|
|
27
|
+
};
|
|
28
|
+
const session = await stripe.checkout.sessions.create({
|
|
29
|
+
mode: opts.mode,
|
|
30
|
+
customer: customer.id,
|
|
31
|
+
line_items: [{ price: price.id, quantity: opts.quantity ?? 1 }],
|
|
32
|
+
allow_promotion_codes: opts.allowPromotionCodes ?? true,
|
|
33
|
+
locale: opts.locale ?? "auto",
|
|
34
|
+
...(currency ? { currency } : {}),
|
|
35
|
+
...(opts.collectTaxId
|
|
36
|
+
? {
|
|
37
|
+
tax_id_collection: { enabled: true },
|
|
38
|
+
customer_update: { name: "auto" },
|
|
39
|
+
}
|
|
40
|
+
: {}),
|
|
41
|
+
success_url: opts.successUrl,
|
|
42
|
+
cancel_url: opts.cancelUrl,
|
|
43
|
+
metadata,
|
|
44
|
+
...(opts.mode === "subscription" ? { subscription_data: { metadata } } : {}),
|
|
45
|
+
});
|
|
46
|
+
if (!session.url)
|
|
47
|
+
throw new Error("Stripe did not return a checkout URL");
|
|
48
|
+
return session.url;
|
|
49
|
+
}
|
|
50
|
+
/** Open a Customer Portal session for managing/cancelling a subscription. */
|
|
51
|
+
export async function createPortalSession(opts, stripe = getStripe()) {
|
|
52
|
+
const customer = await findOrCreateCustomer(opts.customer, opts.customerDetails, stripe);
|
|
53
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
54
|
+
customer: customer.id,
|
|
55
|
+
return_url: opts.returnUrl,
|
|
56
|
+
});
|
|
57
|
+
return session.url;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=checkout.js.map
|
|
@@ -0,0 +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;QAC7C,GAAG,IAAI,CAAC,QAAQ;KAChB,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/client.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Stripe client — lazily constructed and memoised so the process (and the
|
|
3
|
+
* test suite) boots without keys, and the SDK is only instantiated on first use.
|
|
4
|
+
*
|
|
5
|
+
* This is the one pattern every Ingram site had hand-rolled identically (the
|
|
6
|
+
* `getStripe()` singleton); it lives here once now. Single-mode sites call
|
|
7
|
+
* {@link getStripe}; the dual-mode site (cloud.ingram.tech) calls
|
|
8
|
+
* {@link stripeFor} with a mode and gets one memoised client per mode.
|
|
9
|
+
*/
|
|
10
|
+
import Stripe from "stripe";
|
|
11
|
+
import { type StripeMode } from "./keys.js";
|
|
12
|
+
/** The single-mode Stripe client (STRIPE_SECRET_KEY). Throws a clear error if no
|
|
13
|
+
* key is set — guard read paths with {@link stripeConfigured} when billing is
|
|
14
|
+
* optional in the environment. */
|
|
15
|
+
export declare function getStripe(): Stripe;
|
|
16
|
+
/** The Stripe client for a dual-mode account (STRIPE_SECRET_KEY_{TEST,LIVE}).
|
|
17
|
+
* Throws if that mode's key is unset. */
|
|
18
|
+
export declare function stripeFor(mode: StripeMode): Stripe;
|
|
19
|
+
/** Reset the memoised clients. Test-only seam; never call in app code. */
|
|
20
|
+
export declare function resetStripeClients(): void;
|
|
21
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAexD;;mCAEmC;AACnC,wBAAgB,SAAS,IAAI,MAAM,CAOlC;AAED;0CAC0C;AAC1C,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAelD;AAED,0EAA0E;AAC1E,wBAAgB,kBAAkB,IAAI,IAAI,CAIzC"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Stripe client — lazily constructed and memoised so the process (and the
|
|
3
|
+
* test suite) boots without keys, and the SDK is only instantiated on first use.
|
|
4
|
+
*
|
|
5
|
+
* This is the one pattern every Ingram site had hand-rolled identically (the
|
|
6
|
+
* `getStripe()` singleton); it lives here once now. Single-mode sites call
|
|
7
|
+
* {@link getStripe}; the dual-mode site (cloud.ingram.tech) calls
|
|
8
|
+
* {@link stripeFor} with a mode and gets one memoised client per mode.
|
|
9
|
+
*/
|
|
10
|
+
import Stripe from "stripe";
|
|
11
|
+
import { billingEnv } from "./keys.js";
|
|
12
|
+
let single = null;
|
|
13
|
+
const byMode = {};
|
|
14
|
+
function construct(key) {
|
|
15
|
+
const { apiVersion } = billingEnv();
|
|
16
|
+
// Cast: the SDK types apiVersion as a literal union it ships with; we accept
|
|
17
|
+
// any pinned string from the env so a site can pin ahead of an SDK bump.
|
|
18
|
+
const config = apiVersion
|
|
19
|
+
? { apiVersion }
|
|
20
|
+
: undefined;
|
|
21
|
+
return new Stripe(key, config);
|
|
22
|
+
}
|
|
23
|
+
/** The single-mode Stripe client (STRIPE_SECRET_KEY). Throws a clear error if no
|
|
24
|
+
* key is set — guard read paths with {@link stripeConfigured} when billing is
|
|
25
|
+
* optional in the environment. */
|
|
26
|
+
export function getStripe() {
|
|
27
|
+
if (!single) {
|
|
28
|
+
const key = process.env.STRIPE_SECRET_KEY;
|
|
29
|
+
if (!key)
|
|
30
|
+
throw new Error("STRIPE_SECRET_KEY is not set");
|
|
31
|
+
single = construct(key);
|
|
32
|
+
}
|
|
33
|
+
return single;
|
|
34
|
+
}
|
|
35
|
+
/** The Stripe client for a dual-mode account (STRIPE_SECRET_KEY_{TEST,LIVE}).
|
|
36
|
+
* Throws if that mode's key is unset. */
|
|
37
|
+
export function stripeFor(mode) {
|
|
38
|
+
const cached = byMode[mode];
|
|
39
|
+
if (cached)
|
|
40
|
+
return cached;
|
|
41
|
+
const key = mode === "live"
|
|
42
|
+
? process.env.STRIPE_SECRET_KEY_LIVE
|
|
43
|
+
: process.env.STRIPE_SECRET_KEY_TEST;
|
|
44
|
+
if (!key) {
|
|
45
|
+
throw new Error(`STRIPE_SECRET_KEY_${mode.toUpperCase()} is not set (dual-mode billing)`);
|
|
46
|
+
}
|
|
47
|
+
const client = construct(key);
|
|
48
|
+
byMode[mode] = client;
|
|
49
|
+
return client;
|
|
50
|
+
}
|
|
51
|
+
/** Reset the memoised clients. Test-only seam; never call in app code. */
|
|
52
|
+
export function resetStripeClients() {
|
|
53
|
+
single = null;
|
|
54
|
+
delete byMode.test;
|
|
55
|
+
delete byMode.live;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAmB,MAAM,WAAW,CAAC;AAExD,IAAI,MAAM,GAAkB,IAAI,CAAC;AACjC,MAAM,MAAM,GAAwC,EAAE,CAAC;AAEvD,SAAS,SAAS,CAAC,GAAW;IAC7B,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAAC;IACpC,6EAA6E;IAC7E,yEAAyE;IACzE,MAAM,MAAM,GAAG,UAAU;QACxB,CAAC,CAAE,EAAE,UAAU,EAA8C;QAC7D,CAAC,CAAC,SAAS,CAAC;IACb,OAAO,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAChC,CAAC;AAED;;mCAEmC;AACnC,MAAM,UAAU,SAAS;IACxB,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QAC1C,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAC1D,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC;AAED;0CAC0C;AAC1C,MAAM,UAAU,SAAS,CAAC,IAAgB;IACzC,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,GAAG,GACR,IAAI,KAAK,MAAM;QACd,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB;QACpC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;IACvC,IAAI,CAAC,GAAG,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACd,qBAAqB,IAAI,CAAC,WAAW,EAAE,iCAAiC,CACxE,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9B,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC;IACtB,OAAO,MAAM,CAAC;AACf,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,kBAAkB;IACjC,MAAM,GAAG,IAAI,CAAC;IACd,OAAO,MAAM,CAAC,IAAI,CAAC;IACnB,OAAO,MAAM,CAAC,IAAI,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-app credit ledger — the stateful half of nk-billing, for sites that
|
|
3
|
+
* meter usage in abstract "credits" rather than money (integrain). It follows
|
|
4
|
+
* nextkit's Django-app model: this package owns its tables and ships their
|
|
5
|
+
* migration (`migrations/0001_billing.sql`), and takes the database connection by
|
|
6
|
+
* **injection** so it never reaches into the consuming site's schema.
|
|
7
|
+
*
|
|
8
|
+
* Tenancy is the *caller's* concern, deliberately. Every function takes a
|
|
9
|
+
* `Queryable` (a `pg` client / pool, or nk-db's query interface) and the caller
|
|
10
|
+
* wraps the call in whatever isolation it uses — `withTenant(orgId, db => …)`
|
|
11
|
+
* under RLS, or a plain transaction under app-layer filtering. That is why the
|
|
12
|
+
* spend path is exposed as `spendCredits(db, …)` you call inside your own
|
|
13
|
+
* transaction, mirroring how a site charges atomically with the row it inserts.
|
|
14
|
+
*
|
|
15
|
+
* The ledger is keyed by an opaque `tenantId` — use whatever the site bills per
|
|
16
|
+
* (org id, user id). The trial grant is folded into the first spend: the first
|
|
17
|
+
* touch for a tenant lazily creates its row with the trial credits and stamps the
|
|
18
|
+
* trial clock, so a brand-new workspace gets its trial without a separate
|
|
19
|
+
* provisioning step.
|
|
20
|
+
*/
|
|
21
|
+
import { type DenyReason } from "./errors.js";
|
|
22
|
+
export { BillingError, type DenyReason, denyMessage } from "./errors.js";
|
|
23
|
+
/** The minimal database surface the ledger needs — satisfied by a `pg`
|
|
24
|
+
* Pool/PoolClient and by nk-db's query helpers. */
|
|
25
|
+
export interface Queryable {
|
|
26
|
+
query<R = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<{
|
|
27
|
+
rows: R[];
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/** Trial config for the lazy first-spend grant. Omit to disable trials (a tenant
|
|
31
|
+
* with no subscription is then simply never entitled). */
|
|
32
|
+
export interface TrialConfig {
|
|
33
|
+
/** Credits seeded on the tenant's first spend. */
|
|
34
|
+
credits: number;
|
|
35
|
+
/** How long the trial lasts from that first spend, in milliseconds. */
|
|
36
|
+
windowMs: number;
|
|
37
|
+
}
|
|
38
|
+
export type SpendResult = {
|
|
39
|
+
ok: true;
|
|
40
|
+
balance: number;
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
reason: DenyReason;
|
|
44
|
+
};
|
|
45
|
+
export interface SpendOptions {
|
|
46
|
+
tenantId: string;
|
|
47
|
+
/** Credits this action costs. The site keeps its own action→cost table. */
|
|
48
|
+
cost: number;
|
|
49
|
+
/** Enable the lazy trial grant on first spend. */
|
|
50
|
+
trial?: TrialConfig;
|
|
51
|
+
/** Override which subscription statuses count as entitled. */
|
|
52
|
+
activeStatuses?: ReadonlySet<string>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Atomically charge `cost` credits to a tenant **within the caller's
|
|
56
|
+
* transaction**. Serialised by `SELECT … FOR UPDATE` on the tenant's row so
|
|
57
|
+
* concurrent spends can't both pass the balance check and overspend. Grants the
|
|
58
|
+
* trial on first touch when `trial` is set. Returns the outcome — does not throw
|
|
59
|
+
* — so callers can branch (use {@link requireCredits} when you want a throw).
|
|
60
|
+
*
|
|
61
|
+
* The caller MUST run this inside a transaction scoped to the tenant; otherwise
|
|
62
|
+
* the row lock spans no surrounding work and the atomicity guarantee is weaker.
|
|
63
|
+
*/
|
|
64
|
+
export declare function spendCredits(db: Queryable, opts: SpendOptions): Promise<SpendResult>;
|
|
65
|
+
/** Like {@link spendCredits} but throws {@link BillingError} on a denial — use in
|
|
66
|
+
* API routes that map the error to a 402. */
|
|
67
|
+
export declare function requireCredits(db: Queryable, opts: SpendOptions): Promise<void>;
|
|
68
|
+
/** Return previously-spent credits — call when the work a spend paid for did not
|
|
69
|
+
* run (a dedup hit, a hard failure, an aborted enqueue). No-op for non-positive
|
|
70
|
+
* amounts. */
|
|
71
|
+
export declare function refundCredits(db: Queryable, opts: {
|
|
72
|
+
tenantId: string;
|
|
73
|
+
amount: number;
|
|
74
|
+
}): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Mark a Stripe event processed, idempotently. Returns false if the event was
|
|
77
|
+
* already claimed, so the caller skips re-applying it. Run inside the same
|
|
78
|
+
* transaction as the state change it guards, so a webhook retry can never apply
|
|
79
|
+
* the change twice. Backed by `billing_stripe_events`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function claimStripeEvent(db: Queryable, eventId: string, type: string): Promise<boolean>;
|
|
82
|
+
/** Add credits to a tenant, idempotently per Stripe event — the dedupe and the
|
|
83
|
+
* balance increment share the caller's transaction, so a webhook retry never
|
|
84
|
+
* double-credits. Returns false (no-op) if the event was already processed.
|
|
85
|
+
* Pass a synthetic `eventId` (e.g. `subgrant:<sub>:<periodStart>`) to grant a
|
|
86
|
+
* subscription's bundled credits once per billing period. */
|
|
87
|
+
export declare function grantCredits(db: Queryable, opts: {
|
|
88
|
+
tenantId: string;
|
|
89
|
+
eventId: string;
|
|
90
|
+
eventType: string;
|
|
91
|
+
amount: number;
|
|
92
|
+
stripeCustomerId?: string | null;
|
|
93
|
+
}): Promise<boolean>;
|
|
94
|
+
/** Cache a tenant's subscription status from a webhook, idempotently per event.
|
|
95
|
+
* Returns false if the event was already processed. */
|
|
96
|
+
export declare function recordSubscriptionStatus(db: Queryable, opts: {
|
|
97
|
+
tenantId: string;
|
|
98
|
+
eventId: string;
|
|
99
|
+
eventType: string;
|
|
100
|
+
status: string;
|
|
101
|
+
stripeCustomerId?: string | null;
|
|
102
|
+
}): Promise<boolean>;
|
|
103
|
+
export interface BillingSummary {
|
|
104
|
+
creditsBalance: number;
|
|
105
|
+
subscriptionStatus: string | null;
|
|
106
|
+
stripeCustomerId: string | null;
|
|
107
|
+
entitled: boolean;
|
|
108
|
+
/** Trial window end (epoch ms) when in trial and unsubscribed, else null. */
|
|
109
|
+
trialEndsAt: number | null;
|
|
110
|
+
}
|
|
111
|
+
/** Read-only entitlement + balance snapshot for the billing UI. Does not create a
|
|
112
|
+
* row or grant the trial (that happens on the first spend). */
|
|
113
|
+
export declare function getBillingSummary(db: Queryable, opts: {
|
|
114
|
+
tenantId: string;
|
|
115
|
+
trialWindowMs?: number;
|
|
116
|
+
activeStatuses?: ReadonlySet<string>;
|
|
117
|
+
}): Promise<BillingSummary>;
|
|
118
|
+
//# sourceMappingURL=credits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credits.d.ts","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"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"}
|