@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/dist/credits.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
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 { BillingError } from "./errors.js";
|
|
22
|
+
import { ACTIVE_SUBSCRIPTION_STATUSES } from "./status.js";
|
|
23
|
+
export { BillingError, denyMessage } from "./errors.js";
|
|
24
|
+
function entitled(row, now, trialWindowMs, activeStatuses) {
|
|
25
|
+
if (row.subscription_status && activeStatuses.has(row.subscription_status)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (trialWindowMs != null && row.trial_started_at) {
|
|
29
|
+
const started = new Date(row.trial_started_at).getTime();
|
|
30
|
+
if (Number.isFinite(started) && now - started <= trialWindowMs)
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Atomically charge `cost` credits to a tenant **within the caller's
|
|
37
|
+
* transaction**. Serialised by `SELECT … FOR UPDATE` on the tenant's row so
|
|
38
|
+
* concurrent spends can't both pass the balance check and overspend. Grants the
|
|
39
|
+
* trial on first touch when `trial` is set. Returns the outcome — does not throw
|
|
40
|
+
* — so callers can branch (use {@link requireCredits} when you want a throw).
|
|
41
|
+
*
|
|
42
|
+
* The caller MUST run this inside a transaction scoped to the tenant; otherwise
|
|
43
|
+
* the row lock spans no surrounding work and the atomicity guarantee is weaker.
|
|
44
|
+
*/
|
|
45
|
+
export async function spendCredits(db, opts) {
|
|
46
|
+
const activeStatuses = opts.activeStatuses ?? ACTIVE_SUBSCRIPTION_STATUSES;
|
|
47
|
+
// First touch: seed the row (with trial credits + clock when trials are on).
|
|
48
|
+
// ON CONFLICT DO NOTHING makes the trial a one-time grant.
|
|
49
|
+
await db.query(`insert into billing_credits (tenant_id, credits_balance, trial_started_at)
|
|
50
|
+
values ($1, $2, ${opts.trial ? "now()" : "null"})
|
|
51
|
+
on conflict (tenant_id) do nothing`, [opts.tenantId, opts.trial?.credits ?? 0]);
|
|
52
|
+
const { rows } = await db.query(`select credits_balance, subscription_status, trial_started_at, stripe_customer_id
|
|
53
|
+
from billing_credits where tenant_id = $1 for update`, [opts.tenantId]);
|
|
54
|
+
const row = rows[0];
|
|
55
|
+
if (!row)
|
|
56
|
+
return { ok: false, reason: "not_entitled" };
|
|
57
|
+
if (!entitled(row, Date.now(), opts.trial?.windowMs, activeStatuses)) {
|
|
58
|
+
return { ok: false, reason: "not_entitled" };
|
|
59
|
+
}
|
|
60
|
+
if (row.credits_balance < opts.cost) {
|
|
61
|
+
return { ok: false, reason: "insufficient_credits" };
|
|
62
|
+
}
|
|
63
|
+
const { rows: updated } = await db.query(`update billing_credits
|
|
64
|
+
set credits_balance = credits_balance - $2, updated_at = now()
|
|
65
|
+
where tenant_id = $1
|
|
66
|
+
returning credits_balance`, [opts.tenantId, opts.cost]);
|
|
67
|
+
return { ok: true, balance: updated[0]?.credits_balance ?? 0 };
|
|
68
|
+
}
|
|
69
|
+
/** Like {@link spendCredits} but throws {@link BillingError} on a denial — use in
|
|
70
|
+
* API routes that map the error to a 402. */
|
|
71
|
+
export async function requireCredits(db, opts) {
|
|
72
|
+
const result = await spendCredits(db, opts);
|
|
73
|
+
if (!result.ok)
|
|
74
|
+
throw new BillingError(result.reason);
|
|
75
|
+
}
|
|
76
|
+
/** Return previously-spent credits — call when the work a spend paid for did not
|
|
77
|
+
* run (a dedup hit, a hard failure, an aborted enqueue). No-op for non-positive
|
|
78
|
+
* amounts. */
|
|
79
|
+
export async function refundCredits(db, opts) {
|
|
80
|
+
if (opts.amount <= 0)
|
|
81
|
+
return;
|
|
82
|
+
await db.query(`update billing_credits
|
|
83
|
+
set credits_balance = credits_balance + $2, updated_at = now()
|
|
84
|
+
where tenant_id = $1`, [opts.tenantId, opts.amount]);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Mark a Stripe event processed, idempotently. Returns false if the event was
|
|
88
|
+
* already claimed, so the caller skips re-applying it. Run inside the same
|
|
89
|
+
* transaction as the state change it guards, so a webhook retry can never apply
|
|
90
|
+
* the change twice. Backed by `billing_stripe_events`.
|
|
91
|
+
*/
|
|
92
|
+
export async function claimStripeEvent(db, eventId, type) {
|
|
93
|
+
const { rows } = await db.query(`insert into billing_stripe_events (event_id, type) values ($1, $2)
|
|
94
|
+
on conflict (event_id) do nothing returning event_id`, [eventId, type]);
|
|
95
|
+
return rows.length > 0;
|
|
96
|
+
}
|
|
97
|
+
/** Add credits to a tenant, idempotently per Stripe event — the dedupe and the
|
|
98
|
+
* balance increment share the caller's transaction, so a webhook retry never
|
|
99
|
+
* double-credits. Returns false (no-op) if the event was already processed.
|
|
100
|
+
* Pass a synthetic `eventId` (e.g. `subgrant:<sub>:<periodStart>`) to grant a
|
|
101
|
+
* subscription's bundled credits once per billing period. */
|
|
102
|
+
export async function grantCredits(db, opts) {
|
|
103
|
+
if (!(await claimStripeEvent(db, opts.eventId, opts.eventType)))
|
|
104
|
+
return false;
|
|
105
|
+
await db.query(`insert into billing_credits (tenant_id, credits_balance, stripe_customer_id)
|
|
106
|
+
values ($1, $2, $3)
|
|
107
|
+
on conflict (tenant_id) do update set
|
|
108
|
+
credits_balance = billing_credits.credits_balance + excluded.credits_balance,
|
|
109
|
+
stripe_customer_id = coalesce(excluded.stripe_customer_id, billing_credits.stripe_customer_id),
|
|
110
|
+
updated_at = now()`, [opts.tenantId, opts.amount, opts.stripeCustomerId ?? null]);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
/** Cache a tenant's subscription status from a webhook, idempotently per event.
|
|
114
|
+
* Returns false if the event was already processed. */
|
|
115
|
+
export async function recordSubscriptionStatus(db, opts) {
|
|
116
|
+
if (!(await claimStripeEvent(db, opts.eventId, opts.eventType)))
|
|
117
|
+
return false;
|
|
118
|
+
await db.query(`insert into billing_credits (tenant_id, subscription_status, stripe_customer_id)
|
|
119
|
+
values ($1, $2, $3)
|
|
120
|
+
on conflict (tenant_id) do update set
|
|
121
|
+
subscription_status = excluded.subscription_status,
|
|
122
|
+
stripe_customer_id = coalesce(excluded.stripe_customer_id, billing_credits.stripe_customer_id),
|
|
123
|
+
updated_at = now()`, [opts.tenantId, opts.status, opts.stripeCustomerId ?? null]);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/** Read-only entitlement + balance snapshot for the billing UI. Does not create a
|
|
127
|
+
* row or grant the trial (that happens on the first spend). */
|
|
128
|
+
export async function getBillingSummary(db, opts) {
|
|
129
|
+
const activeStatuses = opts.activeStatuses ?? ACTIVE_SUBSCRIPTION_STATUSES;
|
|
130
|
+
const { rows } = await db.query(`select credits_balance, subscription_status, trial_started_at, stripe_customer_id
|
|
131
|
+
from billing_credits where tenant_id = $1`, [opts.tenantId]);
|
|
132
|
+
const row = rows[0];
|
|
133
|
+
if (!row) {
|
|
134
|
+
return {
|
|
135
|
+
creditsBalance: 0,
|
|
136
|
+
subscriptionStatus: null,
|
|
137
|
+
stripeCustomerId: null,
|
|
138
|
+
entitled: false,
|
|
139
|
+
trialEndsAt: null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const subscribed = !!row.subscription_status && activeStatuses.has(row.subscription_status);
|
|
143
|
+
const trialEndsAt = !subscribed && opts.trialWindowMs != null && row.trial_started_at
|
|
144
|
+
? new Date(row.trial_started_at).getTime() + opts.trialWindowMs
|
|
145
|
+
: null;
|
|
146
|
+
return {
|
|
147
|
+
creditsBalance: row.credits_balance,
|
|
148
|
+
subscriptionStatus: row.subscription_status,
|
|
149
|
+
stripeCustomerId: row.stripe_customer_id,
|
|
150
|
+
entitled: entitled(row, Date.now(), opts.trialWindowMs, activeStatuses),
|
|
151
|
+
trialEndsAt,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=credits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credits.js","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentment currency. We sell in two — EUR (the home/base currency) and USD —
|
|
3
|
+
* chosen by where the request comes from. A Stripe Price carries the EUR base
|
|
4
|
+
* `unit_amount` plus a USD entry in `currency_options`; checkout passes the
|
|
5
|
+
* resolved currency and Stripe charges the matching amount.
|
|
6
|
+
*
|
|
7
|
+
* Currency resolution reads Vercel's edge geolocation header
|
|
8
|
+
* (`x-vercel-ip-country`). To stay framework-agnostic this module never imports
|
|
9
|
+
* `next/headers`; the caller passes a `Headers` object (e.g. `await headers()` in
|
|
10
|
+
* a Next.js Server Component, or `request.headers` in a route handler).
|
|
11
|
+
*/
|
|
12
|
+
/** The two presentment currencies we offer. */
|
|
13
|
+
export type BillingCurrency = "usd" | "eur";
|
|
14
|
+
/** Used when the caller's region is unknown — and the base on the Stripe prices. */
|
|
15
|
+
export declare const DEFAULT_CURRENCY: BillingCurrency;
|
|
16
|
+
/** EU/EEA country codes served in euros (closer to EUR than USD even where the
|
|
17
|
+
* local currency isn't the euro — we only offer the two). Everyone else: USD. */
|
|
18
|
+
export declare const EUR_COUNTRIES: ReadonlySet<string>;
|
|
19
|
+
/** Map an ISO-3166 alpha-2 country code to a presentment currency: EU/EEA → EUR,
|
|
20
|
+
* any other identified country → USD, and no signal (absent header / local dev)
|
|
21
|
+
* → the home base currency. */
|
|
22
|
+
export declare function currencyForCountry(country: string | null | undefined): BillingCurrency;
|
|
23
|
+
/** Resolve the caller's presentment currency from a `Headers` object via Vercel's
|
|
24
|
+
* `x-vercel-ip-country` (set at the edge on every request). Falls back to the
|
|
25
|
+
* base currency off-Vercel where the header is absent. */
|
|
26
|
+
export declare function resolveCurrencyFromHeaders(headers: Headers): BillingCurrency;
|
|
27
|
+
/** Format a Stripe minor-unit amount (cents) in `currency` for display. Drops the
|
|
28
|
+
* trailing ".00" on round figures. */
|
|
29
|
+
export declare function formatAmount(minorUnits: number, currency: BillingCurrency): string;
|
|
30
|
+
//# sourceMappingURL=currency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"currency.d.ts","sourceRoot":"","sources":["../src/currency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,KAAK,CAAC;AAE5C,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,EAAE,eAAuB,CAAC;AAEvD;kFACkF;AAClF,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,MAAM,CAsC5C,CAAC;AAEH;;gCAEgC;AAChC,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAChC,eAAe,CAGjB;AAED;;2DAE2D;AAC3D,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,CAE5E;AAED;uCACuC;AACvC,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,MAAM,CAMlF"}
|
package/dist/currency.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentment currency. We sell in two — EUR (the home/base currency) and USD —
|
|
3
|
+
* chosen by where the request comes from. A Stripe Price carries the EUR base
|
|
4
|
+
* `unit_amount` plus a USD entry in `currency_options`; checkout passes the
|
|
5
|
+
* resolved currency and Stripe charges the matching amount.
|
|
6
|
+
*
|
|
7
|
+
* Currency resolution reads Vercel's edge geolocation header
|
|
8
|
+
* (`x-vercel-ip-country`). To stay framework-agnostic this module never imports
|
|
9
|
+
* `next/headers`; the caller passes a `Headers` object (e.g. `await headers()` in
|
|
10
|
+
* a Next.js Server Component, or `request.headers` in a route handler).
|
|
11
|
+
*/
|
|
12
|
+
/** Used when the caller's region is unknown — and the base on the Stripe prices. */
|
|
13
|
+
export const DEFAULT_CURRENCY = "eur";
|
|
14
|
+
/** EU/EEA country codes served in euros (closer to EUR than USD even where the
|
|
15
|
+
* local currency isn't the euro — we only offer the two). Everyone else: USD. */
|
|
16
|
+
export const EUR_COUNTRIES = new Set([
|
|
17
|
+
// Eurozone
|
|
18
|
+
"AT",
|
|
19
|
+
"BE",
|
|
20
|
+
"HR",
|
|
21
|
+
"CY",
|
|
22
|
+
"EE",
|
|
23
|
+
"FI",
|
|
24
|
+
"FR",
|
|
25
|
+
"DE",
|
|
26
|
+
"GR",
|
|
27
|
+
"IE",
|
|
28
|
+
"IT",
|
|
29
|
+
"LV",
|
|
30
|
+
"LT",
|
|
31
|
+
"LU",
|
|
32
|
+
"MT",
|
|
33
|
+
"NL",
|
|
34
|
+
"PT",
|
|
35
|
+
"SK",
|
|
36
|
+
"SI",
|
|
37
|
+
"ES",
|
|
38
|
+
// Rest of EU (non-euro)
|
|
39
|
+
"BG",
|
|
40
|
+
"CZ",
|
|
41
|
+
"DK",
|
|
42
|
+
"HU",
|
|
43
|
+
"PL",
|
|
44
|
+
"RO",
|
|
45
|
+
"SE",
|
|
46
|
+
// EEA + microstates that use the euro
|
|
47
|
+
"IS",
|
|
48
|
+
"LI",
|
|
49
|
+
"NO",
|
|
50
|
+
"MC",
|
|
51
|
+
"SM",
|
|
52
|
+
"VA",
|
|
53
|
+
"AD",
|
|
54
|
+
]);
|
|
55
|
+
/** Map an ISO-3166 alpha-2 country code to a presentment currency: EU/EEA → EUR,
|
|
56
|
+
* any other identified country → USD, and no signal (absent header / local dev)
|
|
57
|
+
* → the home base currency. */
|
|
58
|
+
export function currencyForCountry(country) {
|
|
59
|
+
if (!country)
|
|
60
|
+
return DEFAULT_CURRENCY;
|
|
61
|
+
return EUR_COUNTRIES.has(country.toUpperCase()) ? "eur" : "usd";
|
|
62
|
+
}
|
|
63
|
+
/** Resolve the caller's presentment currency from a `Headers` object via Vercel's
|
|
64
|
+
* `x-vercel-ip-country` (set at the edge on every request). Falls back to the
|
|
65
|
+
* base currency off-Vercel where the header is absent. */
|
|
66
|
+
export function resolveCurrencyFromHeaders(headers) {
|
|
67
|
+
return currencyForCountry(headers.get("x-vercel-ip-country"));
|
|
68
|
+
}
|
|
69
|
+
/** Format a Stripe minor-unit amount (cents) in `currency` for display. Drops the
|
|
70
|
+
* trailing ".00" on round figures. */
|
|
71
|
+
export function formatAmount(minorUnits, currency) {
|
|
72
|
+
return new Intl.NumberFormat(currency === "eur" ? "en-IE" : "en-US", {
|
|
73
|
+
style: "currency",
|
|
74
|
+
currency: currency.toUpperCase(),
|
|
75
|
+
minimumFractionDigits: minorUnits % 100 === 0 ? 0 : 2,
|
|
76
|
+
}).format(minorUnits / 100);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=currency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"currency.js","sourceRoot":"","sources":["../src/currency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAoB,KAAK,CAAC;AAEvD;kFACkF;AAClF,MAAM,CAAC,MAAM,aAAa,GAAwB,IAAI,GAAG,CAAC;IACzD,WAAW;IACX,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,wBAAwB;IACxB,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,sCAAsC;IACtC,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;CACJ,CAAC,CAAC;AAEH;;gCAEgC;AAChC,MAAM,UAAU,kBAAkB,CACjC,OAAkC;IAElC,IAAI,CAAC,OAAO;QAAE,OAAO,gBAAgB,CAAC;IACtC,OAAO,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AACjE,CAAC;AAED;;2DAE2D;AAC3D,MAAM,UAAU,0BAA0B,CAAC,OAAgB;IAC1D,OAAO,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;uCACuC;AACvC,MAAM,UAAU,YAAY,CAAC,UAAkB,EAAE,QAAyB;IACzE,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE;QACpE,KAAK,EAAE,UAAU;QACjB,QAAQ,EAAE,QAAQ,CAAC,WAAW,EAAE;QAChC,qBAAqB,EAAE,UAAU,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KACrD,CAAC,CAAC,MAAM,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer resolution. Every Ingram site keys its Stripe customer to a local
|
|
3
|
+
* entity (an org or a user) by writing that id into the customer's `metadata`,
|
|
4
|
+
* then finds it back by metadata search — no local `stripe_customer_id` column is
|
|
5
|
+
* required just to locate the customer (though sites cache it for speed).
|
|
6
|
+
*
|
|
7
|
+
* The metadata key differs per site (`financica_organization_id`,
|
|
8
|
+
* `integrain_org_id`, …), so it is always passed in explicitly via
|
|
9
|
+
* {@link CustomerRef} rather than baked in here — that is the framework seam.
|
|
10
|
+
*/
|
|
11
|
+
import type Stripe from "stripe";
|
|
12
|
+
/** Identifies a Stripe customer by a metadata tag → local entity id. */
|
|
13
|
+
export interface CustomerRef {
|
|
14
|
+
/** The Stripe `metadata` key the local id is stored under, e.g.
|
|
15
|
+
* "integrain_org_id". Choose one per site and use it everywhere. */
|
|
16
|
+
metadataKey: string;
|
|
17
|
+
/** The local entity id (org id, user id, …). */
|
|
18
|
+
id: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CustomerDetails {
|
|
21
|
+
name?: string | null;
|
|
22
|
+
email?: string | null;
|
|
23
|
+
/** EU VAT number; recorded as an `eu_vat` tax id (best-effort, non-fatal). */
|
|
24
|
+
vatNumber?: string | null;
|
|
25
|
+
address?: {
|
|
26
|
+
line1?: string | null;
|
|
27
|
+
city?: string | null;
|
|
28
|
+
postal_code?: string | null;
|
|
29
|
+
country?: string | null;
|
|
30
|
+
} | null;
|
|
31
|
+
}
|
|
32
|
+
/** The customer for `ref`, found by metadata search, or null if none exists yet.
|
|
33
|
+
* Read paths use this so merely viewing a billing page never mints an empty
|
|
34
|
+
* customer. */
|
|
35
|
+
export declare function findCustomer(ref: CustomerRef, stripe?: Stripe): Promise<Stripe.Customer | null>;
|
|
36
|
+
/** Like {@link findCustomer} but creates the customer on first use (checkout).
|
|
37
|
+
* The local id is always written into metadata so {@link findCustomer} can find
|
|
38
|
+
* it back. */
|
|
39
|
+
export declare function findOrCreateCustomer(ref: CustomerRef, details?: CustomerDetails, stripe?: Stripe): Promise<Stripe.Customer>;
|
|
40
|
+
/** Whether a customer has a billing address on file. Stripe requires one before
|
|
41
|
+
* `tax_id_collection` can be enabled on a Checkout session. */
|
|
42
|
+
export declare function customerHasBillingAddress(customerId: string, stripe?: Stripe): Promise<boolean>;
|
|
43
|
+
/** Update a customer's name and billing address — used when details were missing
|
|
44
|
+
* and got collected just before checkout. */
|
|
45
|
+
export declare function updateCustomerBillingAddress(customerId: string, opts: {
|
|
46
|
+
name?: string | null;
|
|
47
|
+
address: {
|
|
48
|
+
line1: string;
|
|
49
|
+
city: string;
|
|
50
|
+
postal_code: string;
|
|
51
|
+
country: string;
|
|
52
|
+
};
|
|
53
|
+
}, stripe?: Stripe): Promise<void>;
|
|
54
|
+
//# sourceMappingURL=customers.d.ts.map
|
|
@@ -0,0 +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;yEACqE;IACrE,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;AAED;;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,CAiC1B;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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer resolution. Every Ingram site keys its Stripe customer to a local
|
|
3
|
+
* entity (an org or a user) by writing that id into the customer's `metadata`,
|
|
4
|
+
* then finds it back by metadata search — no local `stripe_customer_id` column is
|
|
5
|
+
* required just to locate the customer (though sites cache it for speed).
|
|
6
|
+
*
|
|
7
|
+
* The metadata key differs per site (`financica_organization_id`,
|
|
8
|
+
* `integrain_org_id`, …), so it is always passed in explicitly via
|
|
9
|
+
* {@link CustomerRef} rather than baked in here — that is the framework seam.
|
|
10
|
+
*/
|
|
11
|
+
import { getStripe } from "./client.js";
|
|
12
|
+
/** The customer for `ref`, found by metadata search, or null if none exists yet.
|
|
13
|
+
* Read paths use this so merely viewing a billing page never mints an empty
|
|
14
|
+
* customer. */
|
|
15
|
+
export async function findCustomer(ref, stripe = getStripe()) {
|
|
16
|
+
const found = await stripe.customers.search({
|
|
17
|
+
query: `metadata['${ref.metadataKey}']:'${ref.id}'`,
|
|
18
|
+
limit: 1,
|
|
19
|
+
});
|
|
20
|
+
return found.data[0] ?? null;
|
|
21
|
+
}
|
|
22
|
+
/** Like {@link findCustomer} but creates the customer on first use (checkout).
|
|
23
|
+
* The local id is always written into metadata so {@link findCustomer} can find
|
|
24
|
+
* it back. */
|
|
25
|
+
export async function findOrCreateCustomer(ref, details = {}, stripe = getStripe()) {
|
|
26
|
+
const existing = await findCustomer(ref, stripe);
|
|
27
|
+
if (existing)
|
|
28
|
+
return existing;
|
|
29
|
+
const params = {
|
|
30
|
+
name: details.name ?? undefined,
|
|
31
|
+
email: details.email ?? undefined,
|
|
32
|
+
metadata: { [ref.metadataKey]: ref.id },
|
|
33
|
+
};
|
|
34
|
+
if (details.address?.line1 || details.address?.city) {
|
|
35
|
+
params.address = {
|
|
36
|
+
line1: details.address.line1 ?? undefined,
|
|
37
|
+
city: details.address.city ?? undefined,
|
|
38
|
+
postal_code: details.address.postal_code ?? undefined,
|
|
39
|
+
country: details.address.country ?? undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const customer = await stripe.customers.create(params);
|
|
43
|
+
if (details.vatNumber) {
|
|
44
|
+
try {
|
|
45
|
+
await stripe.customers.createTaxId(customer.id, {
|
|
46
|
+
type: "eu_vat",
|
|
47
|
+
value: details.vatNumber,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Non-fatal: the VAT number may be in a format Stripe rejects. We don't
|
|
52
|
+
// fail customer creation over a tax id we can re-attach later.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return customer;
|
|
56
|
+
}
|
|
57
|
+
/** Whether a customer has a billing address on file. Stripe requires one before
|
|
58
|
+
* `tax_id_collection` can be enabled on a Checkout session. */
|
|
59
|
+
export async function customerHasBillingAddress(customerId, stripe = getStripe()) {
|
|
60
|
+
const customer = await stripe.customers.retrieve(customerId);
|
|
61
|
+
if (customer.deleted)
|
|
62
|
+
return false;
|
|
63
|
+
return Boolean(customer.address?.line1);
|
|
64
|
+
}
|
|
65
|
+
/** Update a customer's name and billing address — used when details were missing
|
|
66
|
+
* and got collected just before checkout. */
|
|
67
|
+
export async function updateCustomerBillingAddress(customerId, opts, stripe = getStripe()) {
|
|
68
|
+
await stripe.customers.update(customerId, {
|
|
69
|
+
...(opts.name ? { name: opts.name } : {}),
|
|
70
|
+
address: opts.address,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=customers.js.map
|
|
@@ -0,0 +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;QACnD,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,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEvD,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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one error type the credit ledger throws when a charge can't proceed.
|
|
3
|
+
* API routes turn it into a 402 Payment Required; server actions / chat map it to
|
|
4
|
+
* a user-facing message. Kept here (not in `credits.ts`) so the error shape can be
|
|
5
|
+
* imported without pulling in the Postgres-facing ledger code.
|
|
6
|
+
*/
|
|
7
|
+
/** Why a charge was denied. */
|
|
8
|
+
export type DenyReason = "not_entitled" | "insufficient_credits";
|
|
9
|
+
/** Default user-facing copy for a denial — short, actionable, points at the fix.
|
|
10
|
+
* Sites can ignore this and write their own from the {@link DenyReason}. */
|
|
11
|
+
export declare function denyMessage(reason: DenyReason): string;
|
|
12
|
+
/** Thrown by the ledger's `requireCredits` when an entity can't pay for an
|
|
13
|
+
* action. Carries the machine-readable {@link DenyReason} for status mapping. */
|
|
14
|
+
export declare class BillingError extends Error {
|
|
15
|
+
readonly reason: DenyReason;
|
|
16
|
+
constructor(reason: DenyReason, message?: string);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+BAA+B;AAC/B,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,sBAAsB,CAAC;AAEjE;6EAC6E;AAC7E,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAItD;AAED;kFACkF;AAClF,qBAAa,YAAa,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;gBAChB,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,MAA4B;CAKrE"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one error type the credit ledger throws when a charge can't proceed.
|
|
3
|
+
* API routes turn it into a 402 Payment Required; server actions / chat map it to
|
|
4
|
+
* a user-facing message. Kept here (not in `credits.ts`) so the error shape can be
|
|
5
|
+
* imported without pulling in the Postgres-facing ledger code.
|
|
6
|
+
*/
|
|
7
|
+
/** Default user-facing copy for a denial — short, actionable, points at the fix.
|
|
8
|
+
* Sites can ignore this and write their own from the {@link DenyReason}. */
|
|
9
|
+
export function denyMessage(reason) {
|
|
10
|
+
return reason === "not_entitled"
|
|
11
|
+
? "This workspace needs an active subscription. Subscribe in Settings → Billing."
|
|
12
|
+
: "You're out of credits. Buy a credit pack in Settings → Billing.";
|
|
13
|
+
}
|
|
14
|
+
/** Thrown by the ledger's `requireCredits` when an entity can't pay for an
|
|
15
|
+
* action. Carries the machine-readable {@link DenyReason} for status mapping. */
|
|
16
|
+
export class BillingError extends Error {
|
|
17
|
+
reason;
|
|
18
|
+
constructor(reason, message = denyMessage(reason)) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "BillingError";
|
|
21
|
+
this.reason = reason;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;6EAC6E;AAC7E,MAAM,UAAU,WAAW,CAAC,MAAkB;IAC7C,OAAO,MAAM,KAAK,cAAc;QAC/B,CAAC,CAAC,+EAA+E;QACjF,CAAC,CAAC,iEAAiE,CAAC;AACtE,CAAC;AAED;kFACkF;AAClF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC7B,MAAM,CAAa;IAC5B,YAAY,MAAkB,EAAE,UAAkB,WAAW,CAAC,MAAM,CAAC;QACpE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;CACD"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { type BillingEnv, billingEnv, type StripeMode, stripeConfigured, stripeModeConfigured, } from "./keys.js";
|
|
2
|
+
export { getStripe, resetStripeClients, stripeFor } from "./client.js";
|
|
3
|
+
export { type CustomerDetails, type CustomerRef, customerHasBillingAddress, findCustomer, findOrCreateCustomer, updateCustomerBillingAddress, } from "./customers.js";
|
|
4
|
+
export { displayAmount, priceByLookupKey, supportedCurrencies } from "./prices.js";
|
|
5
|
+
export { type BillingCurrency, currencyForCountry, DEFAULT_CURRENCY, EUR_COUNTRIES, formatAmount, resolveCurrencyFromHeaders, } from "./currency.js";
|
|
6
|
+
export { type CheckoutOptions, createCheckoutSession, createPortalSession, } from "./checkout.js";
|
|
7
|
+
export { ACTIVE_SUBSCRIPTION_STATUSES, fetchSubscriptionForCustomer, fetchSubscriptionSummary, isActiveStatus, type SubscriptionSummary, summarizeSubscription, } from "./subscription.js";
|
|
8
|
+
export { readStripeWebhook, verifyStripeWebhook, type WebhookResult, } from "./webhooks.js";
|
|
9
|
+
export { type Balance, debitBalance, readBalance } from "./balance.js";
|
|
10
|
+
export { BillingError, type DenyReason, denyMessage } from "./errors.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACN,KAAK,UAAU,EACf,UAAU,EACV,KAAK,UAAU,EACf,gBAAgB,EAChB,oBAAoB,GACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,yBAAyB,EACzB,YAAY,EACZ,oBAAoB,EACpB,4BAA4B,GAC5B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EACN,KAAK,eAAe,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,0BAA0B,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,KAAK,eAAe,EACpB,qBAAqB,EACrB,mBAAmB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,4BAA4B,EAC5B,4BAA4B,EAC5B,wBAAwB,EACxB,cAAc,EACd,KAAK,mBAAmB,EACxB,qBAAqB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,aAAa,GAClB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The Ingram billing foundation. The stateless Stripe primitives are the main
|
|
2
|
+
// entry; the Postgres credit ledger + event-dedup store live behind the
|
|
3
|
+
// "@ingram-tech/nk-billing/credits" subpath so a site that only does
|
|
4
|
+
// subscriptions or a Stripe-side wallet never imports a database concept.
|
|
5
|
+
export { billingEnv, stripeConfigured, stripeModeConfigured, } from "./keys.js";
|
|
6
|
+
export { getStripe, resetStripeClients, stripeFor } from "./client.js";
|
|
7
|
+
export { customerHasBillingAddress, findCustomer, findOrCreateCustomer, updateCustomerBillingAddress, } from "./customers.js";
|
|
8
|
+
export { displayAmount, priceByLookupKey, supportedCurrencies } from "./prices.js";
|
|
9
|
+
export { currencyForCountry, DEFAULT_CURRENCY, EUR_COUNTRIES, formatAmount, resolveCurrencyFromHeaders, } from "./currency.js";
|
|
10
|
+
export { createCheckoutSession, createPortalSession, } from "./checkout.js";
|
|
11
|
+
export { ACTIVE_SUBSCRIPTION_STATUSES, fetchSubscriptionForCustomer, fetchSubscriptionSummary, isActiveStatus, summarizeSubscription, } from "./subscription.js";
|
|
12
|
+
export { readStripeWebhook, verifyStripeWebhook, } from "./webhooks.js";
|
|
13
|
+
export { debitBalance, readBalance } from "./balance.js";
|
|
14
|
+
export { BillingError, denyMessage } from "./errors.js";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,wEAAwE;AACxE,qEAAqE;AACrE,0EAA0E;AAE1E,OAAO,EAEN,UAAU,EAEV,gBAAgB,EAChB,oBAAoB,GACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAGN,yBAAyB,EACzB,YAAY,EACZ,oBAAoB,EACpB,4BAA4B,GAC5B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAEN,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,0BAA0B,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EAEN,qBAAqB,EACrB,mBAAmB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,4BAA4B,EAC5B,4BAA4B,EAC5B,wBAAwB,EACxB,cAAc,EAEd,qBAAqB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,iBAAiB,EACjB,mBAAmB,GAEnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAgB,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,YAAY,EAAmB,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment contract for @ingram-tech/nk-billing.
|
|
3
|
+
*
|
|
4
|
+
* Following the "each package owns its own env validation" pattern (see
|
|
5
|
+
* @ingram-tech/nk-db and @ingram-tech/nk-auth). Env vars are external input, so
|
|
6
|
+
* we parse them with Zod (per docs/code-style.md) rather than reading
|
|
7
|
+
* `process.env` ad hoc.
|
|
8
|
+
*
|
|
9
|
+
* Two deployment shapes are supported, picked per call site, never both at once:
|
|
10
|
+
*
|
|
11
|
+
* 1. Single-mode (the default — financica / integrain / thornhill): one
|
|
12
|
+
* account, one key. `getStripe()` reads STRIPE_SECRET_KEY and
|
|
13
|
+
* `webhookSecret()` reads STRIPE_WEBHOOK_SECRET.
|
|
14
|
+
*
|
|
15
|
+
* 2. Dual-mode (cloud.ingram.tech, which is its own merchant of record and
|
|
16
|
+
* runs the Stripe sandbox beside live): `stripeFor("test"|"live")` reads
|
|
17
|
+
* STRIPE_SECRET_KEY_{TEST,LIVE} and `webhookSecret(mode)` reads
|
|
18
|
+
* STRIPE_WEBHOOK_SECRET_{TEST,LIVE}.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here hardcodes a price ID. Prices are resolved at runtime by their
|
|
21
|
+
* stable Stripe `lookup_key` (set on the Price in the infra Pulumi program), so
|
|
22
|
+
* the same key resolves in test and live even though their price IDs differ.
|
|
23
|
+
*/
|
|
24
|
+
/** Which Stripe account a dual-mode site is talking to. */
|
|
25
|
+
export type StripeMode = "test" | "live";
|
|
26
|
+
export interface BillingEnv {
|
|
27
|
+
secretKey?: string;
|
|
28
|
+
webhookSecret?: string;
|
|
29
|
+
testSecretKey?: string;
|
|
30
|
+
liveSecretKey?: string;
|
|
31
|
+
testWebhookSecret?: string;
|
|
32
|
+
liveWebhookSecret?: string;
|
|
33
|
+
apiVersion?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Read and validate the billing env vars at once. Every field is optional —
|
|
36
|
+
* billing degrades gracefully when unconfigured (see {@link stripeConfigured})
|
|
37
|
+
* rather than throwing at import time. */
|
|
38
|
+
export declare const billingEnv: (env?: NodeJS.ProcessEnv) => BillingEnv;
|
|
39
|
+
/** Whether single-mode billing is configured (the secret key is present). Use to
|
|
40
|
+
* open dev/local flows when no key is set, e.g. a no-op paywall. */
|
|
41
|
+
export declare const stripeConfigured: (env?: NodeJS.ProcessEnv) => boolean;
|
|
42
|
+
/** Whether the given dual-mode account is configured. */
|
|
43
|
+
export declare const stripeModeConfigured: (mode: StripeMode, env?: NodeJS.ProcessEnv) => boolean;
|
|
44
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAezC,MAAM,WAAW,UAAU;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;2CAE2C;AAC3C,eAAO,MAAM,UAAU,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,UAkBjE,CAAC;AAEF;qEACqE;AACrE,eAAO,MAAM,gBAAgB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OACzC,CAAC;AAEhC,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,GAChC,MAAM,UAAU,EAChB,MAAK,MAAM,CAAC,UAAwB,KAClC,OACgF,CAAC"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment contract for @ingram-tech/nk-billing.
|
|
3
|
+
*
|
|
4
|
+
* Following the "each package owns its own env validation" pattern (see
|
|
5
|
+
* @ingram-tech/nk-db and @ingram-tech/nk-auth). Env vars are external input, so
|
|
6
|
+
* we parse them with Zod (per docs/code-style.md) rather than reading
|
|
7
|
+
* `process.env` ad hoc.
|
|
8
|
+
*
|
|
9
|
+
* Two deployment shapes are supported, picked per call site, never both at once:
|
|
10
|
+
*
|
|
11
|
+
* 1. Single-mode (the default — financica / integrain / thornhill): one
|
|
12
|
+
* account, one key. `getStripe()` reads STRIPE_SECRET_KEY and
|
|
13
|
+
* `webhookSecret()` reads STRIPE_WEBHOOK_SECRET.
|
|
14
|
+
*
|
|
15
|
+
* 2. Dual-mode (cloud.ingram.tech, which is its own merchant of record and
|
|
16
|
+
* runs the Stripe sandbox beside live): `stripeFor("test"|"live")` reads
|
|
17
|
+
* STRIPE_SECRET_KEY_{TEST,LIVE} and `webhookSecret(mode)` reads
|
|
18
|
+
* STRIPE_WEBHOOK_SECRET_{TEST,LIVE}.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here hardcodes a price ID. Prices are resolved at runtime by their
|
|
21
|
+
* stable Stripe `lookup_key` (set on the Price in the infra Pulumi program), so
|
|
22
|
+
* the same key resolves in test and live even though their price IDs differ.
|
|
23
|
+
*/
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
const schema = z.object({
|
|
26
|
+
// Single-mode.
|
|
27
|
+
STRIPE_SECRET_KEY: z.string().min(1).optional(),
|
|
28
|
+
STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(),
|
|
29
|
+
// Dual-mode.
|
|
30
|
+
STRIPE_SECRET_KEY_TEST: z.string().min(1).optional(),
|
|
31
|
+
STRIPE_SECRET_KEY_LIVE: z.string().min(1).optional(),
|
|
32
|
+
STRIPE_WEBHOOK_SECRET_TEST: z.string().min(1).optional(),
|
|
33
|
+
STRIPE_WEBHOOK_SECRET_LIVE: z.string().min(1).optional(),
|
|
34
|
+
// Optional pin; omitted → the stripe SDK's bundled API version.
|
|
35
|
+
STRIPE_API_VERSION: z.string().min(1).optional(),
|
|
36
|
+
});
|
|
37
|
+
/** Read and validate the billing env vars at once. Every field is optional —
|
|
38
|
+
* billing degrades gracefully when unconfigured (see {@link stripeConfigured})
|
|
39
|
+
* rather than throwing at import time. */
|
|
40
|
+
export const billingEnv = (env = process.env) => {
|
|
41
|
+
const result = schema.safeParse(env);
|
|
42
|
+
if (!result.success) {
|
|
43
|
+
const issues = result.error.issues
|
|
44
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
45
|
+
.join(", ");
|
|
46
|
+
throw new Error(`@ingram-tech/nk-billing: invalid environment — ${issues}`);
|
|
47
|
+
}
|
|
48
|
+
const d = result.data;
|
|
49
|
+
return {
|
|
50
|
+
secretKey: d.STRIPE_SECRET_KEY,
|
|
51
|
+
webhookSecret: d.STRIPE_WEBHOOK_SECRET,
|
|
52
|
+
testSecretKey: d.STRIPE_SECRET_KEY_TEST,
|
|
53
|
+
liveSecretKey: d.STRIPE_SECRET_KEY_LIVE,
|
|
54
|
+
testWebhookSecret: d.STRIPE_WEBHOOK_SECRET_TEST,
|
|
55
|
+
liveWebhookSecret: d.STRIPE_WEBHOOK_SECRET_LIVE,
|
|
56
|
+
apiVersion: d.STRIPE_API_VERSION,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/** Whether single-mode billing is configured (the secret key is present). Use to
|
|
60
|
+
* open dev/local flows when no key is set, e.g. a no-op paywall. */
|
|
61
|
+
export const stripeConfigured = (env = process.env) => Boolean(env.STRIPE_SECRET_KEY);
|
|
62
|
+
/** Whether the given dual-mode account is configured. */
|
|
63
|
+
export const stripeModeConfigured = (mode, env = process.env) => Boolean(mode === "live" ? env.STRIPE_SECRET_KEY_LIVE : env.STRIPE_SECRET_KEY_TEST);
|
|
64
|
+
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IACvB,eAAe;IACf,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC/C,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACnD,aAAa;IACb,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACpD,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACpD,0BAA0B,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxD,0BAA0B,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxD,gEAAgE;IAChE,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAChD,CAAC,CAAC;AAYH;;2CAE2C;AAC3C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAc,EAAE;IAC9E,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM;aAChC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;aAC3D,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,kDAAkD,MAAM,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;IACtB,OAAO;QACN,SAAS,EAAE,CAAC,CAAC,iBAAiB;QAC9B,aAAa,EAAE,CAAC,CAAC,qBAAqB;QACtC,aAAa,EAAE,CAAC,CAAC,sBAAsB;QACvC,aAAa,EAAE,CAAC,CAAC,sBAAsB;QACvC,iBAAiB,EAAE,CAAC,CAAC,0BAA0B;QAC/C,iBAAiB,EAAE,CAAC,CAAC,0BAA0B;QAC/C,UAAU,EAAE,CAAC,CAAC,kBAAkB;KAChC,CAAC;AACH,CAAC,CAAC;AAEF;qEACqE;AACrE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAW,EAAE,CACjF,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AAEhC,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CACnC,IAAgB,EAChB,MAAyB,OAAO,CAAC,GAAG,EAC1B,EAAE,CACZ,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC"}
|