@farm.js/autumn 0.1.0-beta.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/LICENSE +22 -0
- package/README.md +11 -0
- package/dist/client.d.ts +274 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +81 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1303 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1303 @@
|
|
|
1
|
+
import { Autumn as AutumnSDK } from "autumn-js";
|
|
2
|
+
import { Webhook as StandardWebhook, WebhookVerificationError as StandardWebhookVerificationError, } from "standardwebhooks";
|
|
3
|
+
import { defineIntegration, integrationRoute, } from "@farm.js/core";
|
|
4
|
+
import { integrationConfig, normalizeWebhookConfig, resolveAppPath, toAbsoluteUrl, } from "@farm.js/integration-utils";
|
|
5
|
+
import { createAutumnClientApi, } from "./client.js";
|
|
6
|
+
export { autumnClient } from "./client.js";
|
|
7
|
+
const pendingAutumnMeterProjection = new Map();
|
|
8
|
+
function createAutumnApi(input) {
|
|
9
|
+
return createAutumnClientApi(input);
|
|
10
|
+
}
|
|
11
|
+
function normalizeProducts(products) {
|
|
12
|
+
return Object.entries(products ?? {}).map(([id, product]) => {
|
|
13
|
+
const autumnPlanId = product.autumn?.planId ?? product.autumnPlanId;
|
|
14
|
+
if (!autumnPlanId) {
|
|
15
|
+
throw new Error(`Autumn billing product "${id}" requires autumn.planId (or autumnPlanId).`);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
...product,
|
|
19
|
+
autumnPlanId,
|
|
20
|
+
id,
|
|
21
|
+
public: product.public ?? true,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function toCatalogProduct(product, plans) {
|
|
26
|
+
return {
|
|
27
|
+
id: product.id,
|
|
28
|
+
autumnPlanId: product.autumnPlanId,
|
|
29
|
+
name: product.name ?? product.id,
|
|
30
|
+
description: product.description ?? null,
|
|
31
|
+
kind: product.kind,
|
|
32
|
+
planId: product.planId ?? null,
|
|
33
|
+
trialDays: product.planId ? (plans[product.planId]?.trial?.days ?? null) : null,
|
|
34
|
+
public: product.public,
|
|
35
|
+
currency: product.currency ?? null,
|
|
36
|
+
unitAmount: product.unitAmount ?? null,
|
|
37
|
+
interval: product.interval ?? null,
|
|
38
|
+
intervalCount: product.intervalCount ?? null,
|
|
39
|
+
meterPrices: [],
|
|
40
|
+
metadata: product.metadata ?? {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function normalizeAutumnInterval(value) {
|
|
44
|
+
switch (value) {
|
|
45
|
+
case "day":
|
|
46
|
+
case "week":
|
|
47
|
+
case "month":
|
|
48
|
+
case "year":
|
|
49
|
+
return value;
|
|
50
|
+
default:
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function formatCatalogMoney(amount, currency) {
|
|
55
|
+
return new Intl.NumberFormat("en-US", {
|
|
56
|
+
style: "currency",
|
|
57
|
+
currency: (currency ?? "usd").toUpperCase(),
|
|
58
|
+
maximumFractionDigits: amount % 100 === 0 ? 0 : 2,
|
|
59
|
+
}).format(amount / 100);
|
|
60
|
+
}
|
|
61
|
+
function formatCatalogUnitAmount(value, currency) {
|
|
62
|
+
const parsed = Number(value);
|
|
63
|
+
if (!Number.isFinite(parsed)) {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
return new Intl.NumberFormat("en-US", {
|
|
67
|
+
style: "currency",
|
|
68
|
+
currency: (currency ?? "usd").toUpperCase(),
|
|
69
|
+
minimumFractionDigits: 2,
|
|
70
|
+
maximumFractionDigits: 6,
|
|
71
|
+
}).format(parsed);
|
|
72
|
+
}
|
|
73
|
+
function projectMeterUsage(input) {
|
|
74
|
+
switch (input.aggregation) {
|
|
75
|
+
case "count":
|
|
76
|
+
return input.currentUsed + 1;
|
|
77
|
+
case "last":
|
|
78
|
+
return input.quantity;
|
|
79
|
+
default:
|
|
80
|
+
return input.currentUsed + input.quantity;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function toCentsFromDollars(amount) {
|
|
84
|
+
return typeof amount === "number" && Number.isFinite(amount) ? Math.round(amount * 100) : null;
|
|
85
|
+
}
|
|
86
|
+
function toDecimalStringFromDollars(amount) {
|
|
87
|
+
return typeof amount === "number" && Number.isFinite(amount) ? amount.toFixed(6) : null;
|
|
88
|
+
}
|
|
89
|
+
function priceCapAmount(price) {
|
|
90
|
+
if (!price || typeof price.maxPurchase !== "number" || price.maxPurchase <= 0) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (typeof price.amount !== "number" || price.amount <= 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const billingUnits = typeof price.billingUnits === "number" && price.billingUnits > 0 ? price.billingUnits : 1;
|
|
97
|
+
const cappedBuckets = Math.ceil(price.maxPurchase / billingUnits);
|
|
98
|
+
return Math.round(cappedBuckets * price.amount * 100);
|
|
99
|
+
}
|
|
100
|
+
function resolveEffectiveLimit(planLimit, granted) {
|
|
101
|
+
if (typeof planLimit === "number" && typeof granted === "number") {
|
|
102
|
+
return Math.max(planLimit, granted);
|
|
103
|
+
}
|
|
104
|
+
if (typeof planLimit === "number") {
|
|
105
|
+
return planLimit;
|
|
106
|
+
}
|
|
107
|
+
if (typeof granted === "number") {
|
|
108
|
+
return granted;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function summarizeAutumnCatalogMeterPrice(input) {
|
|
113
|
+
if (!input.unitAmountDecimal) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const parts = [];
|
|
117
|
+
if (typeof input.includedUnits === "number" && input.includedUnits > 0) {
|
|
118
|
+
parts.push(`${input.includedUnits.toLocaleString()} included`);
|
|
119
|
+
}
|
|
120
|
+
const unitLabel = typeof input.billingUnits === "number" && input.billingUnits > 1
|
|
121
|
+
? `${input.billingUnits.toLocaleString()} ${input.unit ?? "units"}`
|
|
122
|
+
: (input.unit ?? "unit");
|
|
123
|
+
parts.push(`${formatCatalogUnitAmount(input.unitAmountDecimal, input.currency)}/${unitLabel}`);
|
|
124
|
+
if (typeof input.capAmount === "number") {
|
|
125
|
+
parts.push(`capped at ${formatCatalogMoney(input.capAmount, input.currency)}`);
|
|
126
|
+
}
|
|
127
|
+
return parts.join(", ");
|
|
128
|
+
}
|
|
129
|
+
function resolveMeterFeatureId(meter) {
|
|
130
|
+
return meter?.featureId?.trim() ? meter.featureId : null;
|
|
131
|
+
}
|
|
132
|
+
async function listPlansById(sdk) {
|
|
133
|
+
const response = await sdk.plans.list();
|
|
134
|
+
const byId = new Map();
|
|
135
|
+
for (const summary of response.list ?? []) {
|
|
136
|
+
const plan = await sdk.plans.get({
|
|
137
|
+
planId: summary.id,
|
|
138
|
+
});
|
|
139
|
+
byId.set(plan.id, plan);
|
|
140
|
+
}
|
|
141
|
+
return byId;
|
|
142
|
+
}
|
|
143
|
+
function findPlanMeterPrice(plan, featureId) {
|
|
144
|
+
for (const item of plan?.items ?? []) {
|
|
145
|
+
if (item.featureId !== featureId || !item.price) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
return { item, price: item.price };
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function toAutumnProductItem(item) {
|
|
153
|
+
return {
|
|
154
|
+
featureId: item.featureId,
|
|
155
|
+
included: item.included ?? undefined,
|
|
156
|
+
unlimited: item.unlimited ?? undefined,
|
|
157
|
+
reset: item.reset ?? undefined,
|
|
158
|
+
price: item.price
|
|
159
|
+
? {
|
|
160
|
+
amount: item.price.amount ?? undefined,
|
|
161
|
+
interval: normalizeAutumnInterval(item.price.interval ?? null) ?? "month",
|
|
162
|
+
intervalCount: item.price.intervalCount ?? undefined,
|
|
163
|
+
billingUnits: item.price.billingUnits ?? undefined,
|
|
164
|
+
billingMethod: item.price.billingMethod,
|
|
165
|
+
maxPurchase: item.price.maxPurchase ?? undefined,
|
|
166
|
+
}
|
|
167
|
+
: undefined,
|
|
168
|
+
rollover: item.rollover
|
|
169
|
+
? {
|
|
170
|
+
max: item.rollover.max ?? undefined,
|
|
171
|
+
maxPercentage: item.rollover.maxPercentage ?? undefined,
|
|
172
|
+
expiryDurationType: item.rollover.expiryDurationType,
|
|
173
|
+
expiryDurationLength: item.rollover.expiryDurationLength ?? undefined,
|
|
174
|
+
}
|
|
175
|
+
: undefined,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function mergeAutumnProductItems(livePlan, product) {
|
|
179
|
+
const merged = new Map();
|
|
180
|
+
for (const item of livePlan?.items ?? []) {
|
|
181
|
+
merged.set(item.featureId, toAutumnProductItem(item));
|
|
182
|
+
}
|
|
183
|
+
for (const item of product?.autumn?.items ?? []) {
|
|
184
|
+
const previous = merged.get(item.featureId);
|
|
185
|
+
merged.set(item.featureId, {
|
|
186
|
+
...previous,
|
|
187
|
+
...item,
|
|
188
|
+
reset: item.reset ?? previous?.reset,
|
|
189
|
+
price: item.price ?? previous?.price,
|
|
190
|
+
proration: item.proration ?? previous?.proration,
|
|
191
|
+
rollover: item.rollover ?? previous?.rollover,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return [...merged.values()];
|
|
195
|
+
}
|
|
196
|
+
function findConfiguredMeterPrice(input) {
|
|
197
|
+
for (const item of mergeAutumnProductItems(input.livePlan, input.product)) {
|
|
198
|
+
if (item.featureId !== input.featureId || !item.price) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
item,
|
|
203
|
+
price: item.price,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
async function enrichCatalogProduct(sdk, product, plans, meters, livePlansById) {
|
|
209
|
+
const base = toCatalogProduct(product, plans);
|
|
210
|
+
const livePlan = livePlansById.get(product.autumnPlanId) ?? null;
|
|
211
|
+
if (!livePlan) {
|
|
212
|
+
return base;
|
|
213
|
+
}
|
|
214
|
+
const meterPrices = [];
|
|
215
|
+
for (const [key, meter] of Object.entries(meters ?? {})) {
|
|
216
|
+
const featureId = resolveMeterFeatureId(meter);
|
|
217
|
+
if (!featureId) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const matched = findConfiguredMeterPrice({
|
|
221
|
+
livePlan,
|
|
222
|
+
product,
|
|
223
|
+
featureId,
|
|
224
|
+
});
|
|
225
|
+
if (!matched) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const unitAmountDecimal = toDecimalStringFromDollars(matched.price.amount ?? null);
|
|
229
|
+
const capAmount = priceCapAmount(matched.price);
|
|
230
|
+
meterPrices.push({
|
|
231
|
+
key,
|
|
232
|
+
eventName: meter.eventName ?? featureId,
|
|
233
|
+
featureId,
|
|
234
|
+
unit: meter.unit ?? null,
|
|
235
|
+
currency: product.currency ?? "usd",
|
|
236
|
+
unitAmountDecimal,
|
|
237
|
+
capAmount,
|
|
238
|
+
billingUnits: matched.price.billingUnits ?? null,
|
|
239
|
+
includedUnits: matched.item.included ?? null,
|
|
240
|
+
maxPurchaseUnits: matched.price.maxPurchase ?? null,
|
|
241
|
+
summary: summarizeAutumnCatalogMeterPrice({
|
|
242
|
+
unitAmountDecimal,
|
|
243
|
+
unit: meter.unit ?? null,
|
|
244
|
+
capAmount,
|
|
245
|
+
includedUnits: matched.item.included ?? null,
|
|
246
|
+
billingUnits: matched.price.billingUnits ?? null,
|
|
247
|
+
currency: product.currency ?? "usd",
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
...base,
|
|
253
|
+
name: livePlan.name || base.name,
|
|
254
|
+
description: livePlan.description ?? base.description,
|
|
255
|
+
trialDays: livePlan.freeTrial?.durationType === "day" ? livePlan.freeTrial.durationLength : null,
|
|
256
|
+
currency: base.currency ?? "usd",
|
|
257
|
+
unitAmount: toCentsFromDollars(livePlan.price?.amount) ?? base.unitAmount,
|
|
258
|
+
interval: normalizeAutumnInterval(livePlan.price?.interval ?? null) ?? base.interval,
|
|
259
|
+
intervalCount: livePlan.price?.intervalCount ?? base.intervalCount,
|
|
260
|
+
meterPrices,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function getOwnerExternalCustomerId(owner, billing, tools) {
|
|
264
|
+
if (billing.resolveExternalCustomerId) {
|
|
265
|
+
return billing.resolveExternalCustomerId(owner, tools);
|
|
266
|
+
}
|
|
267
|
+
const normalizedId = owner.id
|
|
268
|
+
.trim()
|
|
269
|
+
.replace(/[^a-zA-Z0-9_-]+/g, "-")
|
|
270
|
+
.replace(/^-+|-+$/g, "");
|
|
271
|
+
return `${owner.kind}_${normalizedId || "unknown"}`;
|
|
272
|
+
}
|
|
273
|
+
function resolveAutumnPlanLimitReference(value, defaultKey) {
|
|
274
|
+
if (!value || value === "plan_limit" || typeof value === "number") {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
if (typeof value === "string") {
|
|
278
|
+
const match = /^plans\.([^.]+)\.limits?\.([^.]+)$/.exec(value);
|
|
279
|
+
if (!match) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
planId: match[1],
|
|
284
|
+
key: match[2],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (typeof value.planId === "string" && value.planId.trim()) {
|
|
288
|
+
return {
|
|
289
|
+
planId: value.planId,
|
|
290
|
+
key: typeof value.key === "string" && value.key.trim() ? value.key : defaultKey,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
function resolveSoftLimit(plans, planId, key, includedLimit, guard) {
|
|
296
|
+
if (!guard?.softLimit) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
if (guard.softLimit === "plan_limit") {
|
|
300
|
+
return includedLimit;
|
|
301
|
+
}
|
|
302
|
+
const reference = resolveAutumnPlanLimitReference(guard.softLimit, key);
|
|
303
|
+
if (reference) {
|
|
304
|
+
const referencedLimit = plans[reference.planId]?.limits?.[reference.key ?? key] ?? null;
|
|
305
|
+
return typeof referencedLimit === "number" ? referencedLimit : null;
|
|
306
|
+
}
|
|
307
|
+
return typeof guard.softLimit === "number" ? guard.softLimit : null;
|
|
308
|
+
}
|
|
309
|
+
function resolveHardLimit(planId, includedLimit, guard) {
|
|
310
|
+
if (!guard) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (typeof guard.hardLimitByPlan?.[planId] === "number") {
|
|
314
|
+
return guard.hardLimitByPlan[planId];
|
|
315
|
+
}
|
|
316
|
+
if (typeof guard.hardLimit === "number") {
|
|
317
|
+
return guard.hardLimit;
|
|
318
|
+
}
|
|
319
|
+
if (typeof includedLimit === "number" && typeof guard.hardOverageByPlan?.[planId] === "number") {
|
|
320
|
+
return includedLimit + guard.hardOverageByPlan[planId];
|
|
321
|
+
}
|
|
322
|
+
if (typeof includedLimit === "number" && typeof guard.hardOverage === "number") {
|
|
323
|
+
return includedLimit + guard.hardOverage;
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
function projectionKey(input) {
|
|
328
|
+
return `${input.externalCustomerId}:${input.featureId}:${input.currentPeriodEnd ?? "none"}`;
|
|
329
|
+
}
|
|
330
|
+
function getProjectedUsage(input) {
|
|
331
|
+
const key = projectionKey(input);
|
|
332
|
+
const projected = pendingAutumnMeterProjection.get(key);
|
|
333
|
+
if (projected == null) {
|
|
334
|
+
return input.currentPeriodUsed;
|
|
335
|
+
}
|
|
336
|
+
if (input.currentPeriodUsed >= projected) {
|
|
337
|
+
pendingAutumnMeterProjection.delete(key);
|
|
338
|
+
return input.currentPeriodUsed;
|
|
339
|
+
}
|
|
340
|
+
return projected;
|
|
341
|
+
}
|
|
342
|
+
function setProjectedUsage(input) {
|
|
343
|
+
pendingAutumnMeterProjection.set(projectionKey(input), input.projectedUsage);
|
|
344
|
+
}
|
|
345
|
+
function normalizeAutumnStatus(customer, subscription) {
|
|
346
|
+
if (!customer || !subscription) {
|
|
347
|
+
return "free";
|
|
348
|
+
}
|
|
349
|
+
if (subscription.pastDue) {
|
|
350
|
+
return "past_due";
|
|
351
|
+
}
|
|
352
|
+
if (subscription.canceledAt != null) {
|
|
353
|
+
return "canceled";
|
|
354
|
+
}
|
|
355
|
+
if (subscription.trialEndsAt != null && subscription.trialEndsAt > Date.now()) {
|
|
356
|
+
return "trialing";
|
|
357
|
+
}
|
|
358
|
+
if (subscription.status === "scheduled") {
|
|
359
|
+
return "scheduled";
|
|
360
|
+
}
|
|
361
|
+
return "active";
|
|
362
|
+
}
|
|
363
|
+
function resolveActiveProduct(customer, products) {
|
|
364
|
+
const byPlanId = new Map(products.map((product) => [product.autumnPlanId, product]));
|
|
365
|
+
const matchedSubscription = [...(customer?.subscriptions ?? [])]
|
|
366
|
+
.filter((entry) => byPlanId.has(entry.planId))
|
|
367
|
+
.sort((left, right) => right.startedAt - left.startedAt)[0];
|
|
368
|
+
if (matchedSubscription) {
|
|
369
|
+
return {
|
|
370
|
+
product: byPlanId.get(matchedSubscription.planId) ?? null,
|
|
371
|
+
subscription: matchedSubscription,
|
|
372
|
+
purchase: null,
|
|
373
|
+
status: normalizeAutumnStatus(customer, matchedSubscription),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const matchedPurchase = [...(customer?.purchases ?? [])]
|
|
377
|
+
.filter((entry) => byPlanId.has(entry.planId))
|
|
378
|
+
.sort((left, right) => right.startedAt - left.startedAt)[0];
|
|
379
|
+
if (matchedPurchase) {
|
|
380
|
+
return {
|
|
381
|
+
product: byPlanId.get(matchedPurchase.planId) ?? null,
|
|
382
|
+
subscription: null,
|
|
383
|
+
purchase: matchedPurchase,
|
|
384
|
+
status: "active",
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
product: null,
|
|
389
|
+
subscription: null,
|
|
390
|
+
purchase: null,
|
|
391
|
+
status: "free",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function currentPeriodStart(subscription) {
|
|
395
|
+
return subscription?.currentPeriodStart != null
|
|
396
|
+
? new Date(subscription.currentPeriodStart).toISOString()
|
|
397
|
+
: null;
|
|
398
|
+
}
|
|
399
|
+
function currentPeriodEnd(subscription) {
|
|
400
|
+
return subscription?.currentPeriodEnd != null
|
|
401
|
+
? new Date(subscription.currentPeriodEnd).toISOString()
|
|
402
|
+
: null;
|
|
403
|
+
}
|
|
404
|
+
function trialEndsAt(subscription) {
|
|
405
|
+
return subscription?.trialEndsAt != null
|
|
406
|
+
? new Date(subscription.trialEndsAt).toISOString()
|
|
407
|
+
: null;
|
|
408
|
+
}
|
|
409
|
+
function computeMeterState(input) {
|
|
410
|
+
if (!input.featureFound) {
|
|
411
|
+
return {
|
|
412
|
+
state: "meter_missing",
|
|
413
|
+
warning: "The configured Autumn feature is not active for this customer yet.",
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (input.guard?.blockOnPastDue && input.subscriptionStatus === "past_due") {
|
|
417
|
+
return {
|
|
418
|
+
state: "blocked_past_due",
|
|
419
|
+
warning: "Usage reporting is blocked while the Autumn subscription is past due.",
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (typeof input.hardLimit === "number" && input.currentUsed >= input.hardLimit) {
|
|
423
|
+
return {
|
|
424
|
+
state: "hard_limit_reached",
|
|
425
|
+
warning: "The configured metered hard cap has been reached for the current billing period.",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (typeof input.softLimit === "number" && input.currentUsed >= input.softLimit) {
|
|
429
|
+
return {
|
|
430
|
+
state: "soft_limit_reached",
|
|
431
|
+
warning: typeof input.includedLimit === "number"
|
|
432
|
+
? "Included usage has been exhausted. Additional usage is billable."
|
|
433
|
+
: "The configured soft limit has been reached.",
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
state: "ok",
|
|
438
|
+
warning: null,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function resolveUsageValue(input) {
|
|
442
|
+
if (input.billing.usage) {
|
|
443
|
+
return input.billing.usage.resolve(input.owner, input.key, input.tools);
|
|
444
|
+
}
|
|
445
|
+
const featureId = resolveMeterFeatureId(input.billing.meters?.[input.key] ?? null);
|
|
446
|
+
if (!featureId) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
return input.customer?.balances?.[featureId]?.usage ?? null;
|
|
450
|
+
}
|
|
451
|
+
function estimateUsageChargeFromBalance(input) {
|
|
452
|
+
const balanceBreakdown = input.balance?.breakdown?.find((entry) => entry.price);
|
|
453
|
+
const matched = balanceBreakdown?.price
|
|
454
|
+
? {
|
|
455
|
+
item: null,
|
|
456
|
+
price: balanceBreakdown.price,
|
|
457
|
+
}
|
|
458
|
+
: findPlanMeterPrice(input.livePlan, input.featureId);
|
|
459
|
+
if (!matched) {
|
|
460
|
+
return {
|
|
461
|
+
chargeSource: "catalog_rate",
|
|
462
|
+
unitAmountDecimal: null,
|
|
463
|
+
capAmount: null,
|
|
464
|
+
billingUnits: null,
|
|
465
|
+
estimatedMeterChargeAmount: null,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const billingUnits = typeof matched.price.billingUnits === "number" && matched.price.billingUnits > 0
|
|
469
|
+
? matched.price.billingUnits
|
|
470
|
+
: 1;
|
|
471
|
+
const unitAmount = matched.price.amount ?? null;
|
|
472
|
+
const unitAmountDecimal = toDecimalStringFromDollars(unitAmount);
|
|
473
|
+
const capAmount = priceCapAmount(matched.price);
|
|
474
|
+
const balance = input.balance;
|
|
475
|
+
const overageUnits = balance && balance.overageAllowed ? Math.max(balance.usage - balance.granted, 0) : 0;
|
|
476
|
+
const billableBuckets = Math.ceil(overageUnits / billingUnits);
|
|
477
|
+
const uncapped = typeof unitAmount === "number" && unitAmount > 0
|
|
478
|
+
? Math.round(billableBuckets * unitAmount * 100)
|
|
479
|
+
: null;
|
|
480
|
+
return {
|
|
481
|
+
chargeSource: balanceBreakdown?.price ? "balance" : "catalog_rate",
|
|
482
|
+
unitAmountDecimal,
|
|
483
|
+
capAmount,
|
|
484
|
+
billingUnits,
|
|
485
|
+
estimatedMeterChargeAmount: uncapped == null || capAmount == null ? uncapped : Math.min(uncapped, capAmount),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function isPrepaidFeatureQuantity(input) {
|
|
489
|
+
const matched = findConfiguredMeterPrice({
|
|
490
|
+
livePlan: input.livePlan,
|
|
491
|
+
product: input.product,
|
|
492
|
+
featureId: input.featureId,
|
|
493
|
+
});
|
|
494
|
+
return matched?.price.billingMethod === "prepaid";
|
|
495
|
+
}
|
|
496
|
+
function toCurrentChargeLine(input) {
|
|
497
|
+
return {
|
|
498
|
+
key: input.key,
|
|
499
|
+
kind: input.kind,
|
|
500
|
+
label: input.label,
|
|
501
|
+
amount: input.amount ?? null,
|
|
502
|
+
currency: input.currency,
|
|
503
|
+
quantity: input.quantity ?? null,
|
|
504
|
+
includedUnits: input.includedUnits ?? null,
|
|
505
|
+
overageUnits: input.overageUnits ?? null,
|
|
506
|
+
billedBuckets: input.billedBuckets ?? null,
|
|
507
|
+
billingUnits: input.billingUnits ?? null,
|
|
508
|
+
unitAmountDecimal: input.unitAmountDecimal ?? null,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async function requireOwner(ctx, billing, autumn) {
|
|
512
|
+
const tools = { ctx, autumn };
|
|
513
|
+
const owner = await billing.resolveOwner(ctx);
|
|
514
|
+
if (!owner) {
|
|
515
|
+
throw new Error("Autumn billing owner could not be resolved for this request.");
|
|
516
|
+
}
|
|
517
|
+
const externalCustomerId = await getOwnerExternalCustomerId(owner, billing, tools);
|
|
518
|
+
const customer = await autumn.customers.getOrCreate({
|
|
519
|
+
customerId: externalCustomerId,
|
|
520
|
+
name: owner.name,
|
|
521
|
+
email: owner.email,
|
|
522
|
+
fingerprint: owner.fingerprint,
|
|
523
|
+
metadata: owner.metadata,
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
owner,
|
|
527
|
+
externalCustomerId,
|
|
528
|
+
customer,
|
|
529
|
+
tools,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function webhookHeadersToStandardHeaders(headers) {
|
|
533
|
+
const result = {};
|
|
534
|
+
headers.forEach((value, key) => {
|
|
535
|
+
result[key.toLowerCase()] = value;
|
|
536
|
+
});
|
|
537
|
+
if (result["svix-id"] && !result["webhook-id"]) {
|
|
538
|
+
result["webhook-id"] = result["svix-id"];
|
|
539
|
+
}
|
|
540
|
+
if (result["svix-signature"] && !result["webhook-signature"]) {
|
|
541
|
+
result["webhook-signature"] = result["svix-signature"];
|
|
542
|
+
}
|
|
543
|
+
if (result["svix-timestamp"] && !result["webhook-timestamp"]) {
|
|
544
|
+
result["webhook-timestamp"] = result["svix-timestamp"];
|
|
545
|
+
}
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
function createStandardWebhook(secret) {
|
|
549
|
+
return new StandardWebhook(Buffer.from(secret, "utf-8").toString("base64"));
|
|
550
|
+
}
|
|
551
|
+
export function autumn(input) {
|
|
552
|
+
const secretKey = input.secretKey ?? process.env.AUTUMN_SECRET_KEY ?? "";
|
|
553
|
+
const appBaseUrl = input.appBaseUrl ?? process.env.APP_BASE_URL ?? undefined;
|
|
554
|
+
const webhookSecret = process.env.AUTUMN_WEBHOOK_SECRET ?? undefined;
|
|
555
|
+
if (!input.instance && !secretKey) {
|
|
556
|
+
throw new Error("Autumn integration requires AUTUMN_SECRET_KEY or an Autumn instance.");
|
|
557
|
+
}
|
|
558
|
+
const billing = input.billing;
|
|
559
|
+
const productsPath = (input.productsPath ?? "/billing/products");
|
|
560
|
+
const statusPath = (input.statusPath ?? "/billing/status");
|
|
561
|
+
const currentChargesPath = (input.currentChargesPath ??
|
|
562
|
+
"/billing/current-charges");
|
|
563
|
+
const invoicesPath = (input.invoicesPath ?? "/billing/invoices");
|
|
564
|
+
const featuresPath = (input.featuresPath ?? "/billing/features");
|
|
565
|
+
const limitsPath = (input.limitsPath ?? "/billing/limits");
|
|
566
|
+
const usagePath = (input.usagePath ?? "/billing/usage");
|
|
567
|
+
const meterUsagePath = (input.meterUsagePath ??
|
|
568
|
+
"/billing/meter-usage");
|
|
569
|
+
const reportUsagePath = (input.reportUsagePath ??
|
|
570
|
+
"/billing/report-usage");
|
|
571
|
+
const checkPath = (input.checkPath ?? "/billing/check");
|
|
572
|
+
const checkoutPath = (input.checkoutPath ?? "/billing/checkout");
|
|
573
|
+
const portalPath = (input.portalPath ?? "/billing/portal");
|
|
574
|
+
const webhookDefinitions = normalizeWebhookConfig({
|
|
575
|
+
webhooks: input.webhooks,
|
|
576
|
+
defaultName: "default",
|
|
577
|
+
defaultPath: "/billing/webhook",
|
|
578
|
+
defaultSecret: webhookSecret,
|
|
579
|
+
});
|
|
580
|
+
const plans = billing.plans ?? {};
|
|
581
|
+
const products = normalizeProducts(billing.products);
|
|
582
|
+
for (const [key, meter] of Object.entries(billing.meters ?? {})) {
|
|
583
|
+
if (!resolveMeterFeatureId(meter)) {
|
|
584
|
+
throw new Error(`Autumn billing meter "${key}" requires a featureId.`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const sdk = input.instance ??
|
|
588
|
+
new AutumnSDK({
|
|
589
|
+
secretKey,
|
|
590
|
+
serverURL: input.serverURL,
|
|
591
|
+
});
|
|
592
|
+
const webhookRoutes = webhookDefinitions.map((definition) => integrationRoute.post(definition.path, {
|
|
593
|
+
responseFormat: "json",
|
|
594
|
+
rawBody: true,
|
|
595
|
+
async handler(request, context) {
|
|
596
|
+
const rawBody = await request.text();
|
|
597
|
+
const webhookContext = {
|
|
598
|
+
request,
|
|
599
|
+
route: context,
|
|
600
|
+
rawBody,
|
|
601
|
+
headers: request.headers,
|
|
602
|
+
webhook: {
|
|
603
|
+
name: definition.name,
|
|
604
|
+
path: definition.path,
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
try {
|
|
608
|
+
if (!definition.secret) {
|
|
609
|
+
throw new Error("Autumn webhook secret is required to verify webhook events.");
|
|
610
|
+
}
|
|
611
|
+
const verifier = createStandardWebhook(definition.secret);
|
|
612
|
+
const payload = verifier.verify(rawBody, webhookHeadersToStandardHeaders(request.headers));
|
|
613
|
+
const eventId = request.headers.get("svix-id") ??
|
|
614
|
+
request.headers.get("webhook-id") ??
|
|
615
|
+
`${payload.type}:${Date.now()}`;
|
|
616
|
+
const event = {
|
|
617
|
+
provider: "autumn",
|
|
618
|
+
id: eventId,
|
|
619
|
+
type: payload.type,
|
|
620
|
+
data: payload.data,
|
|
621
|
+
raw: payload,
|
|
622
|
+
};
|
|
623
|
+
await definition.onEvent?.(event, webhookContext);
|
|
624
|
+
return Response.json({
|
|
625
|
+
received: true,
|
|
626
|
+
provider: "autumn",
|
|
627
|
+
webhook: definition.name,
|
|
628
|
+
eventId: event.id,
|
|
629
|
+
type: event.type,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
const override = await definition.onError?.(error, webhookContext);
|
|
634
|
+
if (override) {
|
|
635
|
+
return override;
|
|
636
|
+
}
|
|
637
|
+
return Response.json({
|
|
638
|
+
error: error instanceof Error ? error.message : "Autumn webhook verification failed.",
|
|
639
|
+
}, {
|
|
640
|
+
status: error instanceof StandardWebhookVerificationError ? 403 : 400,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
}));
|
|
645
|
+
return defineIntegration({
|
|
646
|
+
category: "payment",
|
|
647
|
+
type: "autumn",
|
|
648
|
+
instance: {
|
|
649
|
+
products: products.map((product) => ({
|
|
650
|
+
id: product.id,
|
|
651
|
+
autumnPlanId: product.autumnPlanId,
|
|
652
|
+
kind: product.kind,
|
|
653
|
+
planId: product.planId ?? null,
|
|
654
|
+
})),
|
|
655
|
+
},
|
|
656
|
+
config: integrationConfig({
|
|
657
|
+
label: "Autumn integration",
|
|
658
|
+
env: {
|
|
659
|
+
secretKey: "AUTUMN_SECRET_KEY",
|
|
660
|
+
appBaseUrl: "APP_BASE_URL",
|
|
661
|
+
webhookSecret: "AUTUMN_WEBHOOK_SECRET",
|
|
662
|
+
},
|
|
663
|
+
input: {
|
|
664
|
+
secretKey,
|
|
665
|
+
serverURL: input.serverURL,
|
|
666
|
+
appBaseUrl,
|
|
667
|
+
webhookSecret,
|
|
668
|
+
},
|
|
669
|
+
required: input.instance ? [] : ["secretKey"],
|
|
670
|
+
}),
|
|
671
|
+
api: createAutumnApi({
|
|
672
|
+
productsPath,
|
|
673
|
+
statusPath,
|
|
674
|
+
currentChargesPath,
|
|
675
|
+
invoicesPath,
|
|
676
|
+
featuresPath,
|
|
677
|
+
limitsPath,
|
|
678
|
+
usagePath,
|
|
679
|
+
meterUsagePath,
|
|
680
|
+
reportUsagePath,
|
|
681
|
+
checkPath,
|
|
682
|
+
checkoutPath,
|
|
683
|
+
portalPath,
|
|
684
|
+
}),
|
|
685
|
+
log: input.log,
|
|
686
|
+
routes: [
|
|
687
|
+
integrationRoute.get(productsPath, {
|
|
688
|
+
responseFormat: "json",
|
|
689
|
+
async handler() {
|
|
690
|
+
let livePlansById;
|
|
691
|
+
try {
|
|
692
|
+
livePlansById = await listPlansById(sdk);
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
return Response.json(products
|
|
696
|
+
.filter((product) => product.public)
|
|
697
|
+
.map((product) => toCatalogProduct(product, plans)));
|
|
698
|
+
}
|
|
699
|
+
return Response.json(await Promise.all(products
|
|
700
|
+
.filter((product) => product.public)
|
|
701
|
+
.map((product) => enrichCatalogProduct(sdk, product, plans, billing.meters, livePlansById))));
|
|
702
|
+
},
|
|
703
|
+
}),
|
|
704
|
+
integrationRoute.get(statusPath, {
|
|
705
|
+
responseFormat: "json",
|
|
706
|
+
async handler(_request, ctx) {
|
|
707
|
+
const owner = await billing.resolveOwner(ctx);
|
|
708
|
+
if (!owner) {
|
|
709
|
+
const plan = plans.free ?? {};
|
|
710
|
+
return Response.json({
|
|
711
|
+
owner: null,
|
|
712
|
+
externalCustomerId: null,
|
|
713
|
+
customerId: null,
|
|
714
|
+
planId: "free",
|
|
715
|
+
productId: null,
|
|
716
|
+
status: "free",
|
|
717
|
+
subscriptionId: null,
|
|
718
|
+
currentPeriodStart: null,
|
|
719
|
+
currentPeriodEnd: null,
|
|
720
|
+
cancelAtPeriodEnd: false,
|
|
721
|
+
trialEndsAt: null,
|
|
722
|
+
features: plan.features ?? {},
|
|
723
|
+
limits: plan.limits ?? {},
|
|
724
|
+
entitlements: plan.entitlements ?? {},
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
728
|
+
const active = resolveActiveProduct(resolved.customer, products);
|
|
729
|
+
const planId = active.product?.planId ?? "free";
|
|
730
|
+
const plan = plans[planId] ?? {};
|
|
731
|
+
return Response.json({
|
|
732
|
+
owner: resolved.owner,
|
|
733
|
+
externalCustomerId: resolved.externalCustomerId,
|
|
734
|
+
customerId: resolved.customer.id,
|
|
735
|
+
planId,
|
|
736
|
+
productId: active.product?.id ?? null,
|
|
737
|
+
status: active.status,
|
|
738
|
+
subscriptionId: active.subscription?.id ?? null,
|
|
739
|
+
currentPeriodStart: currentPeriodStart(active.subscription),
|
|
740
|
+
currentPeriodEnd: currentPeriodEnd(active.subscription),
|
|
741
|
+
cancelAtPeriodEnd: active.subscription?.expiresAt != null &&
|
|
742
|
+
(active.subscription.canceledAt == null ||
|
|
743
|
+
active.subscription.canceledAt > Date.now()),
|
|
744
|
+
trialEndsAt: trialEndsAt(active.subscription),
|
|
745
|
+
features: plan.features ?? {},
|
|
746
|
+
limits: plan.limits ?? {},
|
|
747
|
+
entitlements: plan.entitlements ?? {},
|
|
748
|
+
});
|
|
749
|
+
},
|
|
750
|
+
}),
|
|
751
|
+
integrationRoute.get(currentChargesPath, {
|
|
752
|
+
responseFormat: "json",
|
|
753
|
+
async handler(_request, ctx) {
|
|
754
|
+
const owner = await billing.resolveOwner(ctx);
|
|
755
|
+
if (!owner) {
|
|
756
|
+
return Response.json({
|
|
757
|
+
owner: null,
|
|
758
|
+
externalCustomerId: null,
|
|
759
|
+
customerId: null,
|
|
760
|
+
planId: "free",
|
|
761
|
+
productId: null,
|
|
762
|
+
subscriptionId: null,
|
|
763
|
+
currency: "usd",
|
|
764
|
+
currentPeriodStart: null,
|
|
765
|
+
currentPeriodEnd: null,
|
|
766
|
+
baseSubscriptionAmount: null,
|
|
767
|
+
pendingMeterChargeAmount: 0,
|
|
768
|
+
estimatedTotalAmount: null,
|
|
769
|
+
lineItems: [],
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
773
|
+
const active = resolveActiveProduct(resolved.customer, products);
|
|
774
|
+
const planId = active.product?.planId ?? "free";
|
|
775
|
+
const plan = plans[planId] ?? {};
|
|
776
|
+
const livePlan = active.product?.autumnPlanId
|
|
777
|
+
? await sdk.plans.get({ planId: active.product.autumnPlanId })
|
|
778
|
+
: null;
|
|
779
|
+
const currency = active.product?.currency ?? "usd";
|
|
780
|
+
const baseSubscriptionAmount = toCentsFromDollars(livePlan?.price?.amount ?? null);
|
|
781
|
+
const lineItems = [];
|
|
782
|
+
if (baseSubscriptionAmount != null) {
|
|
783
|
+
lineItems.push(toCurrentChargeLine({
|
|
784
|
+
key: null,
|
|
785
|
+
kind: "base_subscription",
|
|
786
|
+
label: active.product?.name ?? "Base subscription",
|
|
787
|
+
amount: baseSubscriptionAmount,
|
|
788
|
+
currency,
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
let pendingMeterChargeAmount = 0;
|
|
792
|
+
let sawMeterLine = false;
|
|
793
|
+
for (const [key, meter] of Object.entries(billing.meters ?? {})) {
|
|
794
|
+
const featureId = resolveMeterFeatureId(meter);
|
|
795
|
+
if (!featureId) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const balance = resolved.customer.balances?.[featureId] ?? null;
|
|
799
|
+
if (!balance) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const usedUnits = getProjectedUsage({
|
|
803
|
+
externalCustomerId: resolved.externalCustomerId,
|
|
804
|
+
featureId,
|
|
805
|
+
currentPeriodEnd: currentPeriodEnd(active.subscription),
|
|
806
|
+
currentPeriodUsed: balance.usage ?? 0,
|
|
807
|
+
});
|
|
808
|
+
const includedUnits = plan.limits?.[key] ?? balance.granted ?? null;
|
|
809
|
+
const overageUnits = typeof includedUnits === "number" ? Math.max(usedUnits - includedUnits, 0) : null;
|
|
810
|
+
const estimate = estimateUsageChargeFromBalance({
|
|
811
|
+
balance,
|
|
812
|
+
livePlan,
|
|
813
|
+
featureId,
|
|
814
|
+
});
|
|
815
|
+
const billedBuckets = typeof overageUnits === "number" &&
|
|
816
|
+
typeof estimate.billingUnits === "number" &&
|
|
817
|
+
estimate.billingUnits > 0
|
|
818
|
+
? Math.ceil(overageUnits / estimate.billingUnits)
|
|
819
|
+
: 0;
|
|
820
|
+
sawMeterLine = true;
|
|
821
|
+
pendingMeterChargeAmount += estimate.estimatedMeterChargeAmount ?? 0;
|
|
822
|
+
lineItems.push(toCurrentChargeLine({
|
|
823
|
+
key,
|
|
824
|
+
kind: "metered_usage",
|
|
825
|
+
label: `${meter.unit ?? key} overage`,
|
|
826
|
+
amount: estimate.estimatedMeterChargeAmount ?? 0,
|
|
827
|
+
currency,
|
|
828
|
+
quantity: usedUnits,
|
|
829
|
+
includedUnits,
|
|
830
|
+
overageUnits,
|
|
831
|
+
billedBuckets,
|
|
832
|
+
billingUnits: estimate.billingUnits,
|
|
833
|
+
unitAmountDecimal: estimate.unitAmountDecimal,
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
return Response.json({
|
|
837
|
+
owner: resolved.owner,
|
|
838
|
+
externalCustomerId: resolved.externalCustomerId,
|
|
839
|
+
customerId: resolved.customer.id,
|
|
840
|
+
planId,
|
|
841
|
+
productId: active.product?.id ?? null,
|
|
842
|
+
subscriptionId: active.subscription?.id ?? null,
|
|
843
|
+
currency,
|
|
844
|
+
currentPeriodStart: currentPeriodStart(active.subscription),
|
|
845
|
+
currentPeriodEnd: currentPeriodEnd(active.subscription),
|
|
846
|
+
baseSubscriptionAmount,
|
|
847
|
+
pendingMeterChargeAmount: sawMeterLine ? pendingMeterChargeAmount : null,
|
|
848
|
+
estimatedTotalAmount: (baseSubscriptionAmount ?? 0) + (sawMeterLine ? pendingMeterChargeAmount : 0),
|
|
849
|
+
lineItems,
|
|
850
|
+
});
|
|
851
|
+
},
|
|
852
|
+
}),
|
|
853
|
+
integrationRoute.get(invoicesPath, {
|
|
854
|
+
responseFormat: "json",
|
|
855
|
+
async handler(_request, ctx) {
|
|
856
|
+
const owner = await billing.resolveOwner(ctx);
|
|
857
|
+
if (!owner) {
|
|
858
|
+
return Response.json({
|
|
859
|
+
owner: null,
|
|
860
|
+
externalCustomerId: null,
|
|
861
|
+
customerId: null,
|
|
862
|
+
invoices: [],
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
866
|
+
const expandedCustomer = await sdk.customers.getOrCreate({
|
|
867
|
+
customerId: resolved.externalCustomerId,
|
|
868
|
+
name: resolved.owner.name,
|
|
869
|
+
email: resolved.owner.email,
|
|
870
|
+
fingerprint: resolved.owner.fingerprint,
|
|
871
|
+
metadata: resolved.owner.metadata,
|
|
872
|
+
expand: ["invoices"],
|
|
873
|
+
});
|
|
874
|
+
const invoices = (expandedCustomer.invoices ?? [])
|
|
875
|
+
.map((invoice) => ({
|
|
876
|
+
stripeInvoiceId: invoice.stripeId,
|
|
877
|
+
planIds: invoice.planIds ?? [],
|
|
878
|
+
status: invoice.status,
|
|
879
|
+
totalAmount: toCentsFromDollars(invoice.total) ?? 0,
|
|
880
|
+
currency: invoice.currency ?? "usd",
|
|
881
|
+
createdAt: new Date(invoice.createdAt).toISOString(),
|
|
882
|
+
hostedInvoiceUrl: invoice.hostedInvoiceUrl ?? null,
|
|
883
|
+
}))
|
|
884
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
885
|
+
return Response.json({
|
|
886
|
+
owner: resolved.owner,
|
|
887
|
+
externalCustomerId: resolved.externalCustomerId,
|
|
888
|
+
customerId: expandedCustomer.id,
|
|
889
|
+
invoices,
|
|
890
|
+
});
|
|
891
|
+
},
|
|
892
|
+
}),
|
|
893
|
+
integrationRoute.get(featuresPath, {
|
|
894
|
+
responseFormat: "json",
|
|
895
|
+
async handler(_request, ctx) {
|
|
896
|
+
const owner = await billing.resolveOwner(ctx);
|
|
897
|
+
if (!owner) {
|
|
898
|
+
const plan = plans.free ?? {};
|
|
899
|
+
return Response.json({
|
|
900
|
+
planId: "free",
|
|
901
|
+
features: plan.features ?? {},
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
905
|
+
const active = resolveActiveProduct(resolved.customer, products);
|
|
906
|
+
const planId = active.product?.planId ?? "free";
|
|
907
|
+
const plan = plans[planId] ?? {};
|
|
908
|
+
return Response.json({
|
|
909
|
+
planId,
|
|
910
|
+
features: plan.features ?? {},
|
|
911
|
+
});
|
|
912
|
+
},
|
|
913
|
+
}),
|
|
914
|
+
integrationRoute.get(limitsPath, {
|
|
915
|
+
responseFormat: "json",
|
|
916
|
+
async handler(_request, ctx) {
|
|
917
|
+
const owner = await billing.resolveOwner(ctx);
|
|
918
|
+
if (!owner) {
|
|
919
|
+
const plan = plans.free ?? {};
|
|
920
|
+
return Response.json({
|
|
921
|
+
planId: "free",
|
|
922
|
+
limits: plan.limits ?? {},
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
926
|
+
const active = resolveActiveProduct(resolved.customer, products);
|
|
927
|
+
const planId = active.product?.planId ?? "free";
|
|
928
|
+
const plan = plans[planId] ?? {};
|
|
929
|
+
return Response.json({
|
|
930
|
+
planId,
|
|
931
|
+
limits: plan.limits ?? {},
|
|
932
|
+
});
|
|
933
|
+
},
|
|
934
|
+
}),
|
|
935
|
+
integrationRoute.post(usagePath, {
|
|
936
|
+
responseFormat: "json",
|
|
937
|
+
async handler(request, ctx) {
|
|
938
|
+
const body = (await request.json().catch(() => ({})));
|
|
939
|
+
if (!body.key) {
|
|
940
|
+
return new Response("A billing usage key is required.", { status: 400 });
|
|
941
|
+
}
|
|
942
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
943
|
+
const active = resolveActiveProduct(resolved.customer, products);
|
|
944
|
+
const planId = active.product?.planId ?? "free";
|
|
945
|
+
const plan = plans[planId] ?? {};
|
|
946
|
+
const featureId = resolveMeterFeatureId(billing.meters?.[body.key] ?? null);
|
|
947
|
+
const granted = featureId && resolved.customer.balances?.[featureId]
|
|
948
|
+
? (resolved.customer.balances[featureId].granted ?? null)
|
|
949
|
+
: null;
|
|
950
|
+
const used = await resolveUsageValue({
|
|
951
|
+
owner: resolved.owner,
|
|
952
|
+
key: body.key,
|
|
953
|
+
tools: resolved.tools,
|
|
954
|
+
billing,
|
|
955
|
+
customer: resolved.customer,
|
|
956
|
+
});
|
|
957
|
+
const limit = resolveEffectiveLimit(plan.limits?.[body.key] ?? null, granted);
|
|
958
|
+
const remaining = typeof used === "number" && typeof limit === "number"
|
|
959
|
+
? Math.max(limit - used, 0)
|
|
960
|
+
: null;
|
|
961
|
+
return Response.json({
|
|
962
|
+
planId,
|
|
963
|
+
key: body.key,
|
|
964
|
+
used,
|
|
965
|
+
limit,
|
|
966
|
+
remaining,
|
|
967
|
+
});
|
|
968
|
+
},
|
|
969
|
+
}),
|
|
970
|
+
integrationRoute.post(meterUsagePath, {
|
|
971
|
+
responseFormat: "json",
|
|
972
|
+
async handler(request, ctx) {
|
|
973
|
+
const body = (await request.json().catch(() => ({})));
|
|
974
|
+
if (!body.key) {
|
|
975
|
+
return new Response("An Autumn meter key is required.", { status: 400 });
|
|
976
|
+
}
|
|
977
|
+
const meter = billing.meters?.[body.key];
|
|
978
|
+
if (!meter) {
|
|
979
|
+
return new Response(`Unknown Autumn meter "${body.key}".`, { status: 404 });
|
|
980
|
+
}
|
|
981
|
+
const featureId = resolveMeterFeatureId(meter);
|
|
982
|
+
if (!featureId) {
|
|
983
|
+
return new Response(`Autumn meter "${body.key}" is missing a featureId.`, {
|
|
984
|
+
status: 500,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
988
|
+
const active = resolveActiveProduct(resolved.customer, products);
|
|
989
|
+
const planId = active.product?.planId ?? "free";
|
|
990
|
+
const plan = plans[planId] ?? {};
|
|
991
|
+
const balance = resolved.customer.balances?.[featureId] ?? null;
|
|
992
|
+
const livePlan = active.product?.autumnPlanId
|
|
993
|
+
? await sdk.plans.get({ planId: active.product.autumnPlanId })
|
|
994
|
+
: null;
|
|
995
|
+
const currentUsed = getProjectedUsage({
|
|
996
|
+
externalCustomerId: resolved.externalCustomerId,
|
|
997
|
+
featureId,
|
|
998
|
+
currentPeriodEnd: currentPeriodEnd(active.subscription),
|
|
999
|
+
currentPeriodUsed: balance?.usage ?? 0,
|
|
1000
|
+
});
|
|
1001
|
+
const estimate = estimateUsageChargeFromBalance({
|
|
1002
|
+
balance,
|
|
1003
|
+
livePlan,
|
|
1004
|
+
featureId,
|
|
1005
|
+
});
|
|
1006
|
+
const baseSubscriptionAmount = toCentsFromDollars(livePlan?.price?.amount ?? null);
|
|
1007
|
+
const includedLimit = plan.limits?.[body.key] ?? balance?.granted ?? null;
|
|
1008
|
+
const softLimit = resolveSoftLimit(plans, planId, body.key, includedLimit, meter.guard);
|
|
1009
|
+
const hardLimit = resolveHardLimit(planId, includedLimit, meter.guard);
|
|
1010
|
+
const state = computeMeterState({
|
|
1011
|
+
currentUsed,
|
|
1012
|
+
subscriptionStatus: active.status,
|
|
1013
|
+
includedLimit,
|
|
1014
|
+
softLimit,
|
|
1015
|
+
hardLimit,
|
|
1016
|
+
guard: meter.guard,
|
|
1017
|
+
featureFound: balance != null,
|
|
1018
|
+
});
|
|
1019
|
+
return Response.json({
|
|
1020
|
+
planId,
|
|
1021
|
+
productId: active.product?.id ?? null,
|
|
1022
|
+
key: body.key,
|
|
1023
|
+
eventName: meter.eventName ?? featureId,
|
|
1024
|
+
meterId: featureId,
|
|
1025
|
+
meterName: livePlan?.items.find((item) => item.featureId === featureId)?.feature?.name ??
|
|
1026
|
+
featureId,
|
|
1027
|
+
unit: meter.unit ??
|
|
1028
|
+
livePlan?.items.find((item) => item.featureId === featureId)?.feature?.display
|
|
1029
|
+
?.plural ??
|
|
1030
|
+
null,
|
|
1031
|
+
aggregation: meter.aggregation,
|
|
1032
|
+
quantityMetadataKey: null,
|
|
1033
|
+
activeMeterIds: balance ? [featureId] : [],
|
|
1034
|
+
customerId: resolved.customer.id,
|
|
1035
|
+
subscriptionId: active.subscription?.id ?? null,
|
|
1036
|
+
subscriptionStatus: active.status,
|
|
1037
|
+
currentPeriodStart: currentPeriodStart(active.subscription),
|
|
1038
|
+
currentPeriodEnd: currentPeriodEnd(active.subscription),
|
|
1039
|
+
currentPeriodUsed: currentUsed,
|
|
1040
|
+
creditedUnits: balance?.granted ?? null,
|
|
1041
|
+
balance: balance?.remaining ?? null,
|
|
1042
|
+
currency: active.product?.currency ?? "usd",
|
|
1043
|
+
baseSubscriptionAmount,
|
|
1044
|
+
meterUnitAmount: estimate.unitAmountDecimal,
|
|
1045
|
+
meterCapAmount: estimate.capAmount,
|
|
1046
|
+
billingUnits: estimate.billingUnits,
|
|
1047
|
+
chargeSource: estimate.chargeSource,
|
|
1048
|
+
estimatedMeterChargeAmount: estimate.estimatedMeterChargeAmount,
|
|
1049
|
+
estimatedCombinedAmount: baseSubscriptionAmount != null && estimate.estimatedMeterChargeAmount != null
|
|
1050
|
+
? baseSubscriptionAmount + estimate.estimatedMeterChargeAmount
|
|
1051
|
+
: baseSubscriptionAmount,
|
|
1052
|
+
includedLimit,
|
|
1053
|
+
softLimit,
|
|
1054
|
+
hardLimit,
|
|
1055
|
+
remainingIncluded: typeof includedLimit === "number" ? Math.max(includedLimit - currentUsed, 0) : null,
|
|
1056
|
+
remainingHard: typeof hardLimit === "number" ? Math.max(hardLimit - currentUsed, 0) : null,
|
|
1057
|
+
state: state.state,
|
|
1058
|
+
warning: state.warning,
|
|
1059
|
+
});
|
|
1060
|
+
},
|
|
1061
|
+
}),
|
|
1062
|
+
integrationRoute.post(reportUsagePath, {
|
|
1063
|
+
responseFormat: "json",
|
|
1064
|
+
async handler(request, ctx) {
|
|
1065
|
+
const body = (await request.json().catch(() => ({})));
|
|
1066
|
+
if (!body.key) {
|
|
1067
|
+
return new Response("An Autumn meter key is required.", { status: 400 });
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof body.quantity !== "number" || !Number.isFinite(body.quantity)) {
|
|
1070
|
+
return new Response("A numeric quantity is required.", { status: 400 });
|
|
1071
|
+
}
|
|
1072
|
+
const meter = billing.meters?.[body.key];
|
|
1073
|
+
if (!meter) {
|
|
1074
|
+
return new Response(`Unknown Autumn meter "${body.key}".`, { status: 404 });
|
|
1075
|
+
}
|
|
1076
|
+
const featureId = resolveMeterFeatureId(meter);
|
|
1077
|
+
if (!featureId) {
|
|
1078
|
+
return new Response(`Autumn meter "${body.key}" is missing a featureId.`, {
|
|
1079
|
+
status: 500,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
1083
|
+
const active = resolveActiveProduct(resolved.customer, products);
|
|
1084
|
+
const planId = active.product?.planId ?? "free";
|
|
1085
|
+
const plan = plans[planId] ?? {};
|
|
1086
|
+
const livePlan = active.product?.autumnPlanId
|
|
1087
|
+
? await sdk.plans.get({ planId: active.product.autumnPlanId })
|
|
1088
|
+
: null;
|
|
1089
|
+
const currentUsed = getProjectedUsage({
|
|
1090
|
+
externalCustomerId: resolved.externalCustomerId,
|
|
1091
|
+
featureId,
|
|
1092
|
+
currentPeriodEnd: currentPeriodEnd(active.subscription),
|
|
1093
|
+
currentPeriodUsed: resolved.customer.balances?.[featureId]?.usage ?? 0,
|
|
1094
|
+
});
|
|
1095
|
+
const includedLimit = plan.limits?.[body.key] ?? resolved.customer.balances?.[featureId]?.granted ?? null;
|
|
1096
|
+
const softLimit = resolveSoftLimit(plans, planId, body.key, includedLimit, meter.guard);
|
|
1097
|
+
const hardLimit = resolveHardLimit(planId, includedLimit, meter.guard);
|
|
1098
|
+
const projectedCurrentPeriodUsed = projectMeterUsage({
|
|
1099
|
+
aggregation: meter.aggregation,
|
|
1100
|
+
currentUsed,
|
|
1101
|
+
quantity: body.quantity,
|
|
1102
|
+
});
|
|
1103
|
+
if (typeof hardLimit === "number" && projectedCurrentPeriodUsed > hardLimit) {
|
|
1104
|
+
return Response.json({
|
|
1105
|
+
key: body.key,
|
|
1106
|
+
quantity: body.quantity,
|
|
1107
|
+
customerId: resolved.customer.id,
|
|
1108
|
+
eventName: meter.eventName ?? featureId,
|
|
1109
|
+
eventIdentifier: body.idempotencyKey,
|
|
1110
|
+
occurredAt: body.occurredAt ?? new Date().toISOString(),
|
|
1111
|
+
currentPeriodUsed: currentUsed,
|
|
1112
|
+
projectedCurrentPeriodUsed,
|
|
1113
|
+
softLimit,
|
|
1114
|
+
hardLimit,
|
|
1115
|
+
state: "hard_limit_reached",
|
|
1116
|
+
warning: "The configured metered hard cap has been reached for the current billing period. Reported usage is blocked until the next cycle or a plan change.",
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
if (isPrepaidFeatureQuantity({ livePlan, product: active.product, featureId })) {
|
|
1120
|
+
if (!active.product?.autumnPlanId) {
|
|
1121
|
+
return new Response("Autumn prepaid quantity updates require an active subscription product.", { status: 409 });
|
|
1122
|
+
}
|
|
1123
|
+
const customizedItems = mergeAutumnProductItems(livePlan, active.product);
|
|
1124
|
+
await sdk.billing.update({
|
|
1125
|
+
customerId: resolved.customer.id ?? resolved.externalCustomerId,
|
|
1126
|
+
planId: active.product.autumnPlanId,
|
|
1127
|
+
featureQuantities: [
|
|
1128
|
+
{
|
|
1129
|
+
featureId,
|
|
1130
|
+
quantity: body.quantity,
|
|
1131
|
+
},
|
|
1132
|
+
],
|
|
1133
|
+
customize: customizedItems.length > 0
|
|
1134
|
+
? {
|
|
1135
|
+
items: customizedItems,
|
|
1136
|
+
}
|
|
1137
|
+
: undefined,
|
|
1138
|
+
prorationBehavior: "prorate_immediately",
|
|
1139
|
+
redirectMode: "never",
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
// Autumn's current SDK/runtime track path is inconsistent across the published
|
|
1144
|
+
// types/docs and the actual endpoint behavior. Update the balance usage directly so
|
|
1145
|
+
// the example remains functional and type-safe for metered demos.
|
|
1146
|
+
await sdk.balances.update({
|
|
1147
|
+
customerId: resolved.customer.id ?? resolved.externalCustomerId,
|
|
1148
|
+
featureId,
|
|
1149
|
+
usage: projectedCurrentPeriodUsed,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
const nextUsed = projectedCurrentPeriodUsed;
|
|
1153
|
+
setProjectedUsage({
|
|
1154
|
+
externalCustomerId: resolved.externalCustomerId,
|
|
1155
|
+
featureId,
|
|
1156
|
+
currentPeriodEnd: currentPeriodEnd(active.subscription),
|
|
1157
|
+
projectedUsage: nextUsed,
|
|
1158
|
+
});
|
|
1159
|
+
await billing.hooks?.onUsageReported?.({
|
|
1160
|
+
owner: resolved.owner,
|
|
1161
|
+
key: body.key,
|
|
1162
|
+
quantity: body.quantity,
|
|
1163
|
+
idempotencyKey: body.idempotencyKey,
|
|
1164
|
+
occurredAt: body.occurredAt ?? new Date().toISOString(),
|
|
1165
|
+
eventName: meter.eventName ?? featureId,
|
|
1166
|
+
customerId: resolved.customer.id,
|
|
1167
|
+
projectedCurrentPeriodUsed: nextUsed,
|
|
1168
|
+
}, resolved.tools);
|
|
1169
|
+
return Response.json({
|
|
1170
|
+
key: body.key,
|
|
1171
|
+
quantity: body.quantity,
|
|
1172
|
+
customerId: resolved.customer.id,
|
|
1173
|
+
eventName: meter.eventName ?? featureId,
|
|
1174
|
+
eventIdentifier: body.idempotencyKey,
|
|
1175
|
+
occurredAt: body.occurredAt ?? new Date().toISOString(),
|
|
1176
|
+
currentPeriodUsed: currentUsed,
|
|
1177
|
+
projectedCurrentPeriodUsed: nextUsed,
|
|
1178
|
+
softLimit,
|
|
1179
|
+
hardLimit,
|
|
1180
|
+
state: typeof hardLimit === "number" && nextUsed >= hardLimit
|
|
1181
|
+
? "hard_limit_reached"
|
|
1182
|
+
: typeof softLimit === "number" && nextUsed >= softLimit
|
|
1183
|
+
? "soft_limit_reached"
|
|
1184
|
+
: "ok",
|
|
1185
|
+
warning: typeof hardLimit === "number" && nextUsed >= hardLimit
|
|
1186
|
+
? "The configured metered hard cap has been reached for the current billing period."
|
|
1187
|
+
: typeof softLimit === "number" && nextUsed >= softLimit
|
|
1188
|
+
? "Included usage has been exhausted. Additional usage is billable."
|
|
1189
|
+
: null,
|
|
1190
|
+
});
|
|
1191
|
+
},
|
|
1192
|
+
}),
|
|
1193
|
+
integrationRoute.post(checkPath, {
|
|
1194
|
+
responseFormat: "json",
|
|
1195
|
+
async handler(request, ctx) {
|
|
1196
|
+
const body = (await request.json().catch(() => ({})));
|
|
1197
|
+
if (!body.key) {
|
|
1198
|
+
return new Response("An Autumn meter key is required.", { status: 400 });
|
|
1199
|
+
}
|
|
1200
|
+
const meter = billing.meters?.[body.key];
|
|
1201
|
+
if (!meter) {
|
|
1202
|
+
return new Response(`Unknown Autumn meter "${body.key}".`, { status: 404 });
|
|
1203
|
+
}
|
|
1204
|
+
const featureId = resolveMeterFeatureId(meter);
|
|
1205
|
+
if (!featureId) {
|
|
1206
|
+
return new Response(`Autumn meter "${body.key}" is missing a featureId.`, {
|
|
1207
|
+
status: 500,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
const amount = typeof body.amount === "number" && Number.isFinite(body.amount) && body.amount > 0
|
|
1211
|
+
? body.amount
|
|
1212
|
+
: 1;
|
|
1213
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
1214
|
+
const active = resolveActiveProduct(resolved.customer, products);
|
|
1215
|
+
const planId = active.product?.planId ?? "free";
|
|
1216
|
+
const plan = plans[planId] ?? {};
|
|
1217
|
+
const checked = (await sdk.check({
|
|
1218
|
+
customerId: resolved.customer.id ?? resolved.externalCustomerId,
|
|
1219
|
+
featureId,
|
|
1220
|
+
requiredBalance: amount,
|
|
1221
|
+
}));
|
|
1222
|
+
const used = checked.balance?.usage ?? null;
|
|
1223
|
+
const limit = resolveEffectiveLimit(plan.limits?.[body.key] ?? null, checked.balance?.granted ?? null);
|
|
1224
|
+
const remaining = checked.balance?.remaining ?? null;
|
|
1225
|
+
return Response.json({
|
|
1226
|
+
planId,
|
|
1227
|
+
key: body.key,
|
|
1228
|
+
amount,
|
|
1229
|
+
used,
|
|
1230
|
+
limit,
|
|
1231
|
+
remaining,
|
|
1232
|
+
allowed: checked.allowed,
|
|
1233
|
+
});
|
|
1234
|
+
},
|
|
1235
|
+
}),
|
|
1236
|
+
integrationRoute.post(checkoutPath, {
|
|
1237
|
+
responseFormat: "json",
|
|
1238
|
+
async handler(request, ctx) {
|
|
1239
|
+
const body = (await request.json().catch(() => ({})));
|
|
1240
|
+
if (!body.productId) {
|
|
1241
|
+
return new Response("A billing productId is required.", { status: 400 });
|
|
1242
|
+
}
|
|
1243
|
+
const product = products.find((entry) => entry.id === body.productId);
|
|
1244
|
+
if (!product) {
|
|
1245
|
+
return new Response(`Unknown Autumn billing product "${body.productId}".`, {
|
|
1246
|
+
status: 404,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
1250
|
+
const successPath = resolveAppPath(body.successPath, "Autumn checkout successPath");
|
|
1251
|
+
const successUrl = successPath
|
|
1252
|
+
? toAbsoluteUrl(successPath, ctx.request, appBaseUrl).toString()
|
|
1253
|
+
: undefined;
|
|
1254
|
+
const livePlan = await sdk.plans.get({ planId: product.autumnPlanId });
|
|
1255
|
+
const customizedItems = mergeAutumnProductItems(livePlan, product);
|
|
1256
|
+
const attached = (await sdk.billing.attach({
|
|
1257
|
+
customerId: resolved.customer.id ?? resolved.externalCustomerId,
|
|
1258
|
+
planId: product.autumnPlanId,
|
|
1259
|
+
redirectMode: "always",
|
|
1260
|
+
successUrl,
|
|
1261
|
+
metadata: body.metadata,
|
|
1262
|
+
customize: customizedItems.length > 0
|
|
1263
|
+
? {
|
|
1264
|
+
items: customizedItems,
|
|
1265
|
+
}
|
|
1266
|
+
: undefined,
|
|
1267
|
+
featureQuantities: Object.entries(product.autumn?.featureQuantities ?? {}).map(([featureId, quantity]) => ({
|
|
1268
|
+
featureId,
|
|
1269
|
+
quantity,
|
|
1270
|
+
})),
|
|
1271
|
+
}));
|
|
1272
|
+
return Response.json({
|
|
1273
|
+
productId: product.id,
|
|
1274
|
+
planId: product.planId ?? null,
|
|
1275
|
+
customerId: attached.customerId,
|
|
1276
|
+
redirectTo: attached.paymentUrl ?? successUrl ?? appBaseUrl ?? "/",
|
|
1277
|
+
requiredActionCode: attached.requiredAction?.code ?? null,
|
|
1278
|
+
});
|
|
1279
|
+
},
|
|
1280
|
+
}),
|
|
1281
|
+
integrationRoute.post(portalPath, {
|
|
1282
|
+
responseFormat: "json",
|
|
1283
|
+
async handler(request, ctx) {
|
|
1284
|
+
const body = (await request.json().catch(() => ({})));
|
|
1285
|
+
const resolved = await requireOwner(ctx, billing, sdk);
|
|
1286
|
+
const returnPath = resolveAppPath(body.returnTo, "Autumn portal returnTo");
|
|
1287
|
+
const returnUrl = returnPath
|
|
1288
|
+
? toAbsoluteUrl(returnPath, ctx.request, appBaseUrl).toString()
|
|
1289
|
+
: appBaseUrl;
|
|
1290
|
+
const portal = await sdk.billing.openCustomerPortal({
|
|
1291
|
+
customerId: resolved.customer.id ?? resolved.externalCustomerId,
|
|
1292
|
+
returnUrl,
|
|
1293
|
+
});
|
|
1294
|
+
return Response.json({
|
|
1295
|
+
customerId: portal.customerId,
|
|
1296
|
+
redirectTo: portal.url,
|
|
1297
|
+
});
|
|
1298
|
+
},
|
|
1299
|
+
}),
|
|
1300
|
+
...webhookRoutes,
|
|
1301
|
+
],
|
|
1302
|
+
});
|
|
1303
|
+
}
|