@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.
Files changed (55) hide show
  1. package/README.md +93 -0
  2. package/dist/balance.d.ts +34 -0
  3. package/dist/balance.d.ts.map +1 -0
  4. package/dist/balance.js +34 -0
  5. package/dist/balance.js.map +1 -0
  6. package/dist/checkout.d.ts +49 -0
  7. package/dist/checkout.d.ts.map +1 -0
  8. package/dist/checkout.js +59 -0
  9. package/dist/checkout.js.map +1 -0
  10. package/dist/client.d.ts +21 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +57 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/credits.d.ts +118 -0
  15. package/dist/credits.d.ts.map +1 -0
  16. package/dist/credits.js +154 -0
  17. package/dist/credits.js.map +1 -0
  18. package/dist/currency.d.ts +30 -0
  19. package/dist/currency.d.ts.map +1 -0
  20. package/dist/currency.js +78 -0
  21. package/dist/currency.js.map +1 -0
  22. package/dist/customers.d.ts +54 -0
  23. package/dist/customers.d.ts.map +1 -0
  24. package/dist/customers.js +73 -0
  25. package/dist/customers.js.map +1 -0
  26. package/dist/errors.d.ts +18 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +24 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/index.d.ts +11 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +15 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/keys.d.ts +44 -0
  35. package/dist/keys.d.ts.map +1 -0
  36. package/dist/keys.js +64 -0
  37. package/dist/keys.js.map +1 -0
  38. package/dist/prices.d.ts +23 -0
  39. package/dist/prices.d.ts.map +1 -0
  40. package/dist/prices.js +45 -0
  41. package/dist/prices.js.map +1 -0
  42. package/dist/status.d.ts +12 -0
  43. package/dist/status.d.ts.map +1 -0
  44. package/dist/status.js +18 -0
  45. package/dist/status.js.map +1 -0
  46. package/dist/subscription.d.ts +35 -0
  47. package/dist/subscription.d.ts.map +1 -0
  48. package/dist/subscription.js +47 -0
  49. package/dist/subscription.js.map +1 -0
  50. package/dist/webhooks.d.ts +34 -0
  51. package/dist/webhooks.d.ts.map +1 -0
  52. package/dist/webhooks.js +40 -0
  53. package/dist/webhooks.js.map +1 -0
  54. package/migrations/0001_billing.sql +42 -0
  55. package/package.json +45 -0
@@ -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"}
@@ -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"}
@@ -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"}
@@ -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
@@ -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"}