@fonderie/billing 5.1.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-BvYwTSIF.d.ts → index-CBhthuMn.d.ts} +41 -2
- package/dist/{index-CjwZwEVR.d.cts → index-CS1QagwE.d.cts} +41 -2
- package/dist/index.cjs +150 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +149 -7
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js.map +1 -1
- package/dist/types.cjs +12 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +6 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,11 @@ interface IBillingEvent {
|
|
|
9
9
|
interface INormalizedSubscription {
|
|
10
10
|
subscriberType: SubscriberType;
|
|
11
11
|
subscriberId: string;
|
|
12
|
+
/** Plan name from the price nickname (legacy fallback; prefer resolving via priceLookupKey/priceId). */
|
|
12
13
|
plan: string;
|
|
14
|
+
/** The subscription item's price lookup_key + id — for config-based plan attribution (§16.3). */
|
|
15
|
+
priceLookupKey: string | null;
|
|
16
|
+
priceId: string | null;
|
|
13
17
|
status: string;
|
|
14
18
|
providerCustomerId: string;
|
|
15
19
|
providerSubscriptionId: string;
|
|
@@ -19,6 +23,16 @@ interface INormalizedSubscription {
|
|
|
19
23
|
trialEndsAt: Date | null;
|
|
20
24
|
interval: 'month' | 'year';
|
|
21
25
|
}
|
|
26
|
+
interface IResolvedPrice {
|
|
27
|
+
priceId: string;
|
|
28
|
+
lookupKey: string | null;
|
|
29
|
+
unitAmount: number;
|
|
30
|
+
currency: string;
|
|
31
|
+
interval: 'month' | 'year';
|
|
32
|
+
nickname: string | null;
|
|
33
|
+
productId: string;
|
|
34
|
+
active: boolean;
|
|
35
|
+
}
|
|
22
36
|
interface IBillingProvider {
|
|
23
37
|
name: string;
|
|
24
38
|
createCustomer(opts: {
|
|
@@ -40,6 +54,8 @@ interface IBillingProvider {
|
|
|
40
54
|
}): Promise<{
|
|
41
55
|
url: string;
|
|
42
56
|
}>;
|
|
57
|
+
resolvePriceById(priceId: string): Promise<IResolvedPrice | null>;
|
|
58
|
+
resolvePricesByLookupKey(lookupKeys: string[]): Promise<Map<string, IResolvedPrice>>;
|
|
43
59
|
updateSubscription(opts: {
|
|
44
60
|
subscriptionId: string;
|
|
45
61
|
priceId: string;
|
|
@@ -67,8 +83,30 @@ interface ICounterBackend {
|
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
interface IBillingPlanPrice {
|
|
70
|
-
|
|
86
|
+
/** Stable Stripe Price lookup_key, e.g. "pro_monthly". Preferred reference. */
|
|
87
|
+
lookupKey?: string;
|
|
88
|
+
/** Stripe price id. Used for hydration and as a lookup_key fallback. */
|
|
71
89
|
priceId?: string;
|
|
90
|
+
/**
|
|
91
|
+
* @deprecated Display amount in cents. When pricing hydration is on and the
|
|
92
|
+
* price resolves from Stripe, the live amount wins; this is the fallback only.
|
|
93
|
+
*/
|
|
94
|
+
amount?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read-through pricing: amount/currency come from Stripe (source of truth) rather
|
|
98
|
+
* than the duplicated `amount` above. Gated by `hydration` (kill-switch, §16.9).
|
|
99
|
+
* See packages/billing/docs/pricing-hydration.md.
|
|
100
|
+
*/
|
|
101
|
+
interface IBillingPricingConfig {
|
|
102
|
+
/** Kill-switch. When false (default), use the deprecated hardcoded amount/USD path. */
|
|
103
|
+
hydration?: boolean;
|
|
104
|
+
/** Fresh-cache TTL. Default 300_000 (5m). */
|
|
105
|
+
cacheTtlMs?: number;
|
|
106
|
+
/** Serve last-cached price on a transient resolution miss (lookup_key transfer). Default 3_600_000 (1h). */
|
|
107
|
+
transferGraceMs?: number;
|
|
108
|
+
/** Max age to serve stale price during a Stripe outage before giving up. Default 86_400_000 (24h). */
|
|
109
|
+
maxStaleMs?: number;
|
|
72
110
|
}
|
|
73
111
|
interface IBillingPlanDefaults {
|
|
74
112
|
warnAt?: number;
|
|
@@ -100,6 +138,7 @@ interface IBillingConfig {
|
|
|
100
138
|
backend?: RateLimitBackendConfig;
|
|
101
139
|
};
|
|
102
140
|
notifications?: IBillingNotificationsConfig;
|
|
141
|
+
pricing?: IBillingPricingConfig;
|
|
103
142
|
}
|
|
104
143
|
declare const MESSAGE_KEYS: {
|
|
105
144
|
readonly limitWarning: "billing.limit-warning";
|
|
@@ -113,4 +152,4 @@ declare function requirePlan(plans: string | string[], store: IStoreAdapter, ctx
|
|
|
113
152
|
|
|
114
153
|
declare function withBilling(store: IStoreAdapter, config: IBillingConfig, backend: ICounterBackend): Middleware;
|
|
115
154
|
|
|
116
|
-
export { type BillingMessageKey as B, type IBillingConfig as I, MESSAGE_KEYS as M, type RateLimitBackendConfig as R, type IBillingProvider as a, type
|
|
155
|
+
export { type BillingMessageKey as B, type IBillingConfig as I, MESSAGE_KEYS as M, type RateLimitBackendConfig as R, type IBillingProvider as a, type IResolvedPrice as b, type IBillingEvent as c, type ICounterBackend as d, type IBillingPlan as e, type IBillingNotificationsConfig as f, type IBillingPlanDefaults as g, type IBillingPlanPrice as h, type IBillingPricingConfig as i, requirePlan as r, withBilling as w };
|
|
@@ -9,7 +9,11 @@ interface IBillingEvent {
|
|
|
9
9
|
interface INormalizedSubscription {
|
|
10
10
|
subscriberType: SubscriberType;
|
|
11
11
|
subscriberId: string;
|
|
12
|
+
/** Plan name from the price nickname (legacy fallback; prefer resolving via priceLookupKey/priceId). */
|
|
12
13
|
plan: string;
|
|
14
|
+
/** The subscription item's price lookup_key + id — for config-based plan attribution (§16.3). */
|
|
15
|
+
priceLookupKey: string | null;
|
|
16
|
+
priceId: string | null;
|
|
13
17
|
status: string;
|
|
14
18
|
providerCustomerId: string;
|
|
15
19
|
providerSubscriptionId: string;
|
|
@@ -19,6 +23,16 @@ interface INormalizedSubscription {
|
|
|
19
23
|
trialEndsAt: Date | null;
|
|
20
24
|
interval: 'month' | 'year';
|
|
21
25
|
}
|
|
26
|
+
interface IResolvedPrice {
|
|
27
|
+
priceId: string;
|
|
28
|
+
lookupKey: string | null;
|
|
29
|
+
unitAmount: number;
|
|
30
|
+
currency: string;
|
|
31
|
+
interval: 'month' | 'year';
|
|
32
|
+
nickname: string | null;
|
|
33
|
+
productId: string;
|
|
34
|
+
active: boolean;
|
|
35
|
+
}
|
|
22
36
|
interface IBillingProvider {
|
|
23
37
|
name: string;
|
|
24
38
|
createCustomer(opts: {
|
|
@@ -40,6 +54,8 @@ interface IBillingProvider {
|
|
|
40
54
|
}): Promise<{
|
|
41
55
|
url: string;
|
|
42
56
|
}>;
|
|
57
|
+
resolvePriceById(priceId: string): Promise<IResolvedPrice | null>;
|
|
58
|
+
resolvePricesByLookupKey(lookupKeys: string[]): Promise<Map<string, IResolvedPrice>>;
|
|
43
59
|
updateSubscription(opts: {
|
|
44
60
|
subscriptionId: string;
|
|
45
61
|
priceId: string;
|
|
@@ -67,8 +83,30 @@ interface ICounterBackend {
|
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
interface IBillingPlanPrice {
|
|
70
|
-
|
|
86
|
+
/** Stable Stripe Price lookup_key, e.g. "pro_monthly". Preferred reference. */
|
|
87
|
+
lookupKey?: string;
|
|
88
|
+
/** Stripe price id. Used for hydration and as a lookup_key fallback. */
|
|
71
89
|
priceId?: string;
|
|
90
|
+
/**
|
|
91
|
+
* @deprecated Display amount in cents. When pricing hydration is on and the
|
|
92
|
+
* price resolves from Stripe, the live amount wins; this is the fallback only.
|
|
93
|
+
*/
|
|
94
|
+
amount?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read-through pricing: amount/currency come from Stripe (source of truth) rather
|
|
98
|
+
* than the duplicated `amount` above. Gated by `hydration` (kill-switch, §16.9).
|
|
99
|
+
* See packages/billing/docs/pricing-hydration.md.
|
|
100
|
+
*/
|
|
101
|
+
interface IBillingPricingConfig {
|
|
102
|
+
/** Kill-switch. When false (default), use the deprecated hardcoded amount/USD path. */
|
|
103
|
+
hydration?: boolean;
|
|
104
|
+
/** Fresh-cache TTL. Default 300_000 (5m). */
|
|
105
|
+
cacheTtlMs?: number;
|
|
106
|
+
/** Serve last-cached price on a transient resolution miss (lookup_key transfer). Default 3_600_000 (1h). */
|
|
107
|
+
transferGraceMs?: number;
|
|
108
|
+
/** Max age to serve stale price during a Stripe outage before giving up. Default 86_400_000 (24h). */
|
|
109
|
+
maxStaleMs?: number;
|
|
72
110
|
}
|
|
73
111
|
interface IBillingPlanDefaults {
|
|
74
112
|
warnAt?: number;
|
|
@@ -100,6 +138,7 @@ interface IBillingConfig {
|
|
|
100
138
|
backend?: RateLimitBackendConfig;
|
|
101
139
|
};
|
|
102
140
|
notifications?: IBillingNotificationsConfig;
|
|
141
|
+
pricing?: IBillingPricingConfig;
|
|
103
142
|
}
|
|
104
143
|
declare const MESSAGE_KEYS: {
|
|
105
144
|
readonly limitWarning: "billing.limit-warning";
|
|
@@ -113,4 +152,4 @@ declare function requirePlan(plans: string | string[], store: IStoreAdapter, ctx
|
|
|
113
152
|
|
|
114
153
|
declare function withBilling(store: IStoreAdapter, config: IBillingConfig, backend: ICounterBackend): Middleware;
|
|
115
154
|
|
|
116
|
-
export { type BillingMessageKey as B, type IBillingConfig as I, MESSAGE_KEYS as M, type RateLimitBackendConfig as R, type IBillingProvider as a, type
|
|
155
|
+
export { type BillingMessageKey as B, type IBillingConfig as I, MESSAGE_KEYS as M, type RateLimitBackendConfig as R, type IBillingProvider as a, type IResolvedPrice as b, type IBillingEvent as c, type ICounterBackend as d, type IBillingPlan as e, type IBillingNotificationsConfig as f, type IBillingPlanDefaults as g, type IBillingPlanPrice as h, type IBillingPricingConfig as i, requirePlan as r, withBilling as w };
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
BILLING_INTERVAL: () => BILLING_INTERVAL,
|
|
23
24
|
BillingModule: () => BillingModule,
|
|
24
25
|
DBCounterBackend: () => DBCounterBackend,
|
|
25
26
|
MESSAGE_KEYS: () => MESSAGE_KEYS,
|
|
@@ -86,6 +87,54 @@ var recordUsageSchema = import_zod.z.object({
|
|
|
86
87
|
quantity: import_zod.z.number().min(0).optional()
|
|
87
88
|
});
|
|
88
89
|
|
|
90
|
+
// src/services/price-cache.ts
|
|
91
|
+
var PriceCache = class {
|
|
92
|
+
ttl;
|
|
93
|
+
grace;
|
|
94
|
+
maxStale;
|
|
95
|
+
byId = /* @__PURE__ */ new Map();
|
|
96
|
+
inflight = /* @__PURE__ */ new Map();
|
|
97
|
+
constructor(opts = {}) {
|
|
98
|
+
this.ttl = opts.ttlMs ?? 3e5;
|
|
99
|
+
this.grace = opts.graceMs ?? 36e5;
|
|
100
|
+
this.maxStale = opts.maxStaleMs ?? 864e5;
|
|
101
|
+
}
|
|
102
|
+
async byPriceId(priceId, provider) {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const hit = this.byId.get(priceId);
|
|
105
|
+
if (hit && now - hit.at < this.ttl) return { price: hit.price, stale: false };
|
|
106
|
+
let fresh;
|
|
107
|
+
try {
|
|
108
|
+
fresh = await this.single(priceId, () => provider.resolvePriceById(priceId));
|
|
109
|
+
} catch {
|
|
110
|
+
if (hit && now - hit.at < this.maxStale) return { price: hit.price, stale: true };
|
|
111
|
+
return { price: null, stale: true };
|
|
112
|
+
}
|
|
113
|
+
if (fresh) {
|
|
114
|
+
this.byId.set(priceId, { price: fresh, at: now });
|
|
115
|
+
return { price: fresh, stale: false };
|
|
116
|
+
}
|
|
117
|
+
if (hit && now - hit.at < this.grace) return { price: hit.price, stale: true };
|
|
118
|
+
return { price: null, stale: true };
|
|
119
|
+
}
|
|
120
|
+
invalidate(priceId) {
|
|
121
|
+
if (priceId) this.byId.delete(priceId);
|
|
122
|
+
else this.byId.clear();
|
|
123
|
+
}
|
|
124
|
+
/** Warm the cache with prices already resolved elsewhere (e.g. boot guard). */
|
|
125
|
+
prime(prices) {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
for (const p of prices) this.byId.set(p.priceId, { price: p, at: now });
|
|
128
|
+
}
|
|
129
|
+
single(key, run) {
|
|
130
|
+
const existing = this.inflight.get(key);
|
|
131
|
+
if (existing) return existing;
|
|
132
|
+
const p = run().finally(() => this.inflight.delete(key));
|
|
133
|
+
this.inflight.set(key, p);
|
|
134
|
+
return p;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
89
138
|
// src/controllers/plan.controller.ts
|
|
90
139
|
var import_core = require("@fonderie/core");
|
|
91
140
|
|
|
@@ -96,6 +145,18 @@ function getPlans(config) {
|
|
|
96
145
|
function getPlanByName(name, config) {
|
|
97
146
|
return config.plans.find((p) => p.name.toLowerCase() === name.toLowerCase()) ?? null;
|
|
98
147
|
}
|
|
148
|
+
function resolvePlanNameByPrice(price, plans) {
|
|
149
|
+
const find = (pred) => plans.find((pl) => pred(pl.monthly) || pred(pl.yearly))?.name ?? null;
|
|
150
|
+
if (price.lookupKey) {
|
|
151
|
+
const m = find((p) => p?.lookupKey === price.lookupKey);
|
|
152
|
+
if (m) return m;
|
|
153
|
+
}
|
|
154
|
+
if (price.priceId) {
|
|
155
|
+
const m = find((p) => p?.priceId === price.priceId);
|
|
156
|
+
if (m) return m;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
99
160
|
async function syncPlansToDB(config, store) {
|
|
100
161
|
const plans = config.plans;
|
|
101
162
|
if (plans.length === 0) return;
|
|
@@ -314,12 +375,41 @@ function toUsageRecordDTO(record) {
|
|
|
314
375
|
}
|
|
315
376
|
|
|
316
377
|
// src/controllers/plan.controller.ts
|
|
317
|
-
function
|
|
378
|
+
async function hydratePricing(dto, plan, config, cache) {
|
|
379
|
+
try {
|
|
380
|
+
let stale = false;
|
|
381
|
+
const resolve = async (priceId) => {
|
|
382
|
+
if (!priceId) return null;
|
|
383
|
+
const r = await cache.byPriceId(priceId, config.provider);
|
|
384
|
+
if (r.stale) stale = true;
|
|
385
|
+
return r.price;
|
|
386
|
+
};
|
|
387
|
+
const [m, y] = await Promise.all([resolve(plan.monthlyPriceId), resolve(plan.yearlyPriceId)]);
|
|
388
|
+
if (m && y && m.currency !== y.currency) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`[billing] plan "${plan.name}": monthly/yearly currency mismatch (${m.currency} vs ${y.currency})`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (m) dto.pricing.monthly = m.unitAmount;
|
|
394
|
+
if (y) dto.pricing.yearly = y.unitAmount;
|
|
395
|
+
const currency = m?.currency ?? y?.currency;
|
|
396
|
+
if (currency) dto.pricing.currency = currency.toUpperCase();
|
|
397
|
+
if (stale) dto.pricingStale = true;
|
|
398
|
+
} catch (err) {
|
|
399
|
+
console.error(`[billing] pricing hydration failed for "${plan.name}":`, err.message);
|
|
400
|
+
dto.pricingStale = true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function planController(store, config, cache) {
|
|
318
404
|
const plans = new PlanModel(store);
|
|
405
|
+
const hydrate = config.pricing?.hydration === true;
|
|
319
406
|
return {
|
|
320
407
|
async list(_ctx) {
|
|
321
408
|
const list = await plans.list();
|
|
322
409
|
const dtos = list.map(toPlanDTO);
|
|
410
|
+
if (hydrate) {
|
|
411
|
+
await Promise.all(dtos.map((dto, i) => hydratePricing(dto, list[i], config, cache)));
|
|
412
|
+
}
|
|
323
413
|
return (0, import_core.setApiResponse)(import_core.HTTP.OK, "PLAN_LIST", `Retrieved ${list.length} workspace plans`, {
|
|
324
414
|
plans: dtos
|
|
325
415
|
});
|
|
@@ -330,8 +420,10 @@ function planController(store) {
|
|
|
330
420
|
if (!id) return (0, import_core.setApiResponse)(import_core.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Plan ID required");
|
|
331
421
|
const plan = await plans.findById(id);
|
|
332
422
|
if (!plan) return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Plan not found");
|
|
423
|
+
const dto = toPlanDTO(plan);
|
|
424
|
+
if (hydrate) await hydratePricing(dto, plan, config, cache);
|
|
333
425
|
return (0, import_core.setApiResponse)(import_core.HTTP.OK, "PLAN_FETCHED", "Plan retrieved successfully.", {
|
|
334
|
-
plan:
|
|
426
|
+
plan: dto
|
|
335
427
|
});
|
|
336
428
|
},
|
|
337
429
|
async create(ctx) {
|
|
@@ -764,7 +856,7 @@ function usageController(store) {
|
|
|
764
856
|
|
|
765
857
|
// src/controllers/webhook.controller.ts
|
|
766
858
|
var import_core5 = require("@fonderie/core");
|
|
767
|
-
function webhookController(store, config) {
|
|
859
|
+
function webhookController(store, config, priceCache) {
|
|
768
860
|
const subscriptions = new SubscriptionModel(store);
|
|
769
861
|
return {
|
|
770
862
|
async handle(ctx) {
|
|
@@ -786,11 +878,15 @@ function webhookController(store, config) {
|
|
|
786
878
|
} catch {
|
|
787
879
|
return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "INVALID_REQUEST", "Invalid webhook signature");
|
|
788
880
|
}
|
|
881
|
+
if (priceCache && (event.type.startsWith("price.") || event.type.startsWith("product."))) {
|
|
882
|
+
priceCache.invalidate();
|
|
883
|
+
}
|
|
789
884
|
if (event.subscription) {
|
|
885
|
+
const plan = event.type === "customer.subscription.deleted" ? event.subscription.plan : resolvePlanNameByPrice(event.subscription, config.plans) ?? event.subscription.plan;
|
|
790
886
|
await subscriptions.upsert({
|
|
791
887
|
subscriberType: event.subscription.subscriberType,
|
|
792
888
|
subscriberId: event.subscription.subscriberId,
|
|
793
|
-
plan
|
|
889
|
+
plan,
|
|
794
890
|
interval: event.subscription.interval,
|
|
795
891
|
status: event.subscription.status,
|
|
796
892
|
providerCustomerId: event.subscription.providerCustomerId,
|
|
@@ -808,11 +904,16 @@ function webhookController(store, config) {
|
|
|
808
904
|
|
|
809
905
|
// src/routes.ts
|
|
810
906
|
function buildBillingRoutes(store, config) {
|
|
811
|
-
const
|
|
907
|
+
const priceCache = new PriceCache({
|
|
908
|
+
ttlMs: config.pricing?.cacheTtlMs,
|
|
909
|
+
graceMs: config.pricing?.transferGraceMs,
|
|
910
|
+
maxStaleMs: config.pricing?.maxStaleMs
|
|
911
|
+
});
|
|
912
|
+
const plan = planController(store, config, priceCache);
|
|
812
913
|
const subscription = subscriptionController(store);
|
|
813
914
|
const checkout = checkoutController(store, config);
|
|
814
915
|
const usage = usageController(store);
|
|
815
|
-
const webhook = webhookController(store, config);
|
|
916
|
+
const webhook = webhookController(store, config, priceCache);
|
|
816
917
|
return [
|
|
817
918
|
// Plans — public read-only
|
|
818
919
|
["GET", "/plans", plan.list],
|
|
@@ -1035,6 +1136,9 @@ var BillingModule = class {
|
|
|
1035
1136
|
}
|
|
1036
1137
|
};
|
|
1037
1138
|
|
|
1139
|
+
// src/types.ts
|
|
1140
|
+
var BILLING_INTERVAL = { MONTH: "month", YEAR: "year" };
|
|
1141
|
+
|
|
1038
1142
|
// src/providers/stripe.ts
|
|
1039
1143
|
var _client = null;
|
|
1040
1144
|
async function getClient(secretKey) {
|
|
@@ -1055,6 +1159,8 @@ function normalizeSubscription(sub) {
|
|
|
1055
1159
|
subscriberType: sub.metadata?.["subscriberType"] ?? "workspace",
|
|
1056
1160
|
subscriberId: sub.metadata?.["subscriberId"] ?? "",
|
|
1057
1161
|
plan: item?.price.nickname ?? "unknown",
|
|
1162
|
+
priceLookupKey: item?.price.lookup_key ?? null,
|
|
1163
|
+
priceId: item?.price.id ?? null,
|
|
1058
1164
|
status: sub.status,
|
|
1059
1165
|
providerCustomerId: sub.customer,
|
|
1060
1166
|
providerSubscriptionId: sub.id,
|
|
@@ -1062,7 +1168,19 @@ function normalizeSubscription(sub) {
|
|
|
1062
1168
|
currentPeriodEnd: periodEnd ? new Date(periodEnd * 1e3) : /* @__PURE__ */ new Date(),
|
|
1063
1169
|
cancelAtPeriodEnd: sub.cancel_at_period_end,
|
|
1064
1170
|
trialEndsAt: sub.trial_end ? new Date(sub.trial_end * 1e3) : null,
|
|
1065
|
-
interval: item?.price.recurring?.interval ===
|
|
1171
|
+
interval: item?.price.recurring?.interval === BILLING_INTERVAL.YEAR ? BILLING_INTERVAL.YEAR : BILLING_INTERVAL.MONTH
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function toResolvedPrice(p) {
|
|
1175
|
+
return {
|
|
1176
|
+
priceId: p.id,
|
|
1177
|
+
lookupKey: p.lookup_key ?? null,
|
|
1178
|
+
unitAmount: p.unit_amount ?? 0,
|
|
1179
|
+
currency: p.currency,
|
|
1180
|
+
interval: p.recurring?.interval === BILLING_INTERVAL.YEAR ? BILLING_INTERVAL.YEAR : BILLING_INTERVAL.MONTH,
|
|
1181
|
+
nickname: p.nickname ?? null,
|
|
1182
|
+
productId: typeof p.product === "string" ? p.product : p.product?.id ?? "",
|
|
1183
|
+
active: p.active ?? true
|
|
1066
1184
|
};
|
|
1067
1185
|
}
|
|
1068
1186
|
var StripeProvider = class {
|
|
@@ -1106,6 +1224,30 @@ var StripeProvider = class {
|
|
|
1106
1224
|
});
|
|
1107
1225
|
return { url: session.url ?? "" };
|
|
1108
1226
|
}
|
|
1227
|
+
async resolvePriceById(priceId) {
|
|
1228
|
+
const stripe = await this.client();
|
|
1229
|
+
try {
|
|
1230
|
+
const p = await stripe.prices.retrieve(priceId, { expand: ["product"] });
|
|
1231
|
+
return toResolvedPrice(p);
|
|
1232
|
+
} catch {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
async resolvePricesByLookupKey(lookupKeys) {
|
|
1237
|
+
const out = /* @__PURE__ */ new Map();
|
|
1238
|
+
if (lookupKeys.length === 0) return out;
|
|
1239
|
+
const stripe = await this.client();
|
|
1240
|
+
const res = await stripe.prices.list({
|
|
1241
|
+
lookup_keys: lookupKeys,
|
|
1242
|
+
active: true,
|
|
1243
|
+
expand: ["data.product"],
|
|
1244
|
+
limit: 100
|
|
1245
|
+
});
|
|
1246
|
+
for (const p of res.data) {
|
|
1247
|
+
if (p.lookup_key) out.set(p.lookup_key, toResolvedPrice(p));
|
|
1248
|
+
}
|
|
1249
|
+
return out;
|
|
1250
|
+
}
|
|
1109
1251
|
async updateSubscription(opts) {
|
|
1110
1252
|
const stripe = await this.client();
|
|
1111
1253
|
const sub = await stripe.subscriptions.retrieve(opts.subscriptionId);
|
|
@@ -1238,6 +1380,7 @@ function requireFeature(key) {
|
|
|
1238
1380
|
}
|
|
1239
1381
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1240
1382
|
0 && (module.exports = {
|
|
1383
|
+
BILLING_INTERVAL,
|
|
1241
1384
|
BillingModule,
|
|
1242
1385
|
DBCounterBackend,
|
|
1243
1386
|
MESSAGE_KEYS,
|