@farm.js/polar 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/dist/index.js ADDED
@@ -0,0 +1,1088 @@
1
+ import { Polar } from "@polar-sh/sdk";
2
+ import { ResourceNotFound } from "@polar-sh/sdk/models/errors/resourcenotfound";
3
+ import { validateEvent as validatePolarWebhookEvent, WebhookVerificationError as PolarWebhookVerificationError, } from "@polar-sh/sdk/webhooks";
4
+ import { defineIntegration, integrationRoute, } from "@farm.js/core";
5
+ import { integrationConfig, normalizeWebhookConfig, resolveAppPath, toAbsoluteUrl, } from "@farm.js/integration-utils";
6
+ import { createPolarClientApi, } from "./client.js";
7
+ export { polarClient } from "./client.js";
8
+ const pendingPolarMeterProjection = new Map();
9
+ function headersToObject(headers) {
10
+ const result = {};
11
+ headers.forEach((value, key) => {
12
+ result[key] = value;
13
+ });
14
+ return result;
15
+ }
16
+ function createPolarApi(input) {
17
+ return createPolarClientApi(input);
18
+ }
19
+ function normalizeStatus(value) {
20
+ switch (value) {
21
+ case "trialing":
22
+ case "active":
23
+ case "past_due":
24
+ case "canceled":
25
+ case "unpaid":
26
+ case "incomplete":
27
+ return value;
28
+ default:
29
+ return "free";
30
+ }
31
+ }
32
+ function normalizeProducts(products) {
33
+ return Object.entries(products ?? {}).map(([id, product]) => {
34
+ const productId = product.polar?.productId ?? product.productId;
35
+ if (!productId) {
36
+ throw new Error(`Polar billing product "${id}" requires a productId.`);
37
+ }
38
+ return {
39
+ ...product,
40
+ productId,
41
+ id,
42
+ public: product.public ?? true,
43
+ };
44
+ });
45
+ }
46
+ function toCatalogProduct(product, plans) {
47
+ return {
48
+ id: product.id,
49
+ productId: product.productId,
50
+ name: product.name ?? product.id,
51
+ description: product.description ?? null,
52
+ kind: product.kind,
53
+ planId: product.planId ?? null,
54
+ trialDays: product.planId ? (plans[product.planId]?.trial?.days ?? null) : null,
55
+ public: product.public,
56
+ currency: product.currency ?? null,
57
+ unitAmount: product.unitAmount ?? null,
58
+ interval: product.interval ?? null,
59
+ intervalCount: product.intervalCount ?? null,
60
+ meterPrices: [],
61
+ metadata: product.metadata ?? {},
62
+ };
63
+ }
64
+ function resolvePolarTrialDays(input) {
65
+ const count = typeof input.trialIntervalCount === "number" && input.trialIntervalCount > 0
66
+ ? input.trialIntervalCount
67
+ : null;
68
+ if (!count) {
69
+ return null;
70
+ }
71
+ switch (input.trialInterval) {
72
+ case "day":
73
+ return count;
74
+ case "week":
75
+ return count * 7;
76
+ default:
77
+ return null;
78
+ }
79
+ }
80
+ function formatPolarCatalogMoney(amount, currency) {
81
+ return new Intl.NumberFormat("en-US", {
82
+ style: "currency",
83
+ currency: (currency ?? "usd").toUpperCase(),
84
+ maximumFractionDigits: amount % 100 === 0 ? 0 : 2,
85
+ }).format(amount / 100);
86
+ }
87
+ function formatPolarCatalogUnitAmount(value, currency) {
88
+ const parsed = Number(value);
89
+ if (!Number.isFinite(parsed)) {
90
+ return value;
91
+ }
92
+ return new Intl.NumberFormat("en-US", {
93
+ style: "currency",
94
+ currency: (currency ?? "usd").toUpperCase(),
95
+ minimumFractionDigits: 2,
96
+ maximumFractionDigits: 6,
97
+ }).format(parsed);
98
+ }
99
+ function summarizePolarCatalogMeterPrice(input) {
100
+ if (!input.unitAmountDecimal) {
101
+ return null;
102
+ }
103
+ const rate = `${formatPolarCatalogUnitAmount(input.unitAmountDecimal, input.currency)}/${input.unit ?? "unit"}`;
104
+ if (typeof input.capAmount === "number") {
105
+ return `${rate}, capped at ${formatPolarCatalogMoney(input.capAmount, input.currency)}`;
106
+ }
107
+ return rate;
108
+ }
109
+ async function enrichCatalogProduct(sdk, product, plans, meters) {
110
+ const base = toCatalogProduct(product, plans);
111
+ try {
112
+ const liveProduct = await sdk.products.get({
113
+ id: product.productId,
114
+ });
115
+ const byMeterId = new Map();
116
+ for (const [key, meter] of Object.entries(meters ?? {})) {
117
+ const meterId = resolvePolarMeterId(meter);
118
+ if (meterId) {
119
+ byMeterId.set(meterId, { key, meter });
120
+ }
121
+ }
122
+ const fixedPrice = (liveProduct.prices?.find((price) => isPolarFixedPrice(price) && typeof price.priceAmount === "number") ?? null);
123
+ const meterPrices = [];
124
+ for (const price of liveProduct.prices ?? []) {
125
+ if (!isPolarMeteredUnitPrice(price)) {
126
+ continue;
127
+ }
128
+ const meteredPrice = price;
129
+ const match = meteredPrice.meterId ? byMeterId.get(meteredPrice.meterId) : null;
130
+ if (!match || !meteredPrice.meterId) {
131
+ continue;
132
+ }
133
+ meterPrices.push({
134
+ key: match.key,
135
+ eventName: match.meter.eventName,
136
+ meterId: meteredPrice.meterId,
137
+ unit: match.meter.unit ?? null,
138
+ currency: product.currency ?? null,
139
+ unitAmountDecimal: meteredPrice.unitAmount ?? null,
140
+ capAmount: typeof meteredPrice.capAmount === "number" ? meteredPrice.capAmount : null,
141
+ summary: summarizePolarCatalogMeterPrice({
142
+ unitAmountDecimal: meteredPrice.unitAmount ?? null,
143
+ unit: match.meter.unit ?? null,
144
+ capAmount: typeof meteredPrice.capAmount === "number" ? meteredPrice.capAmount : null,
145
+ currency: product.currency ?? null,
146
+ }),
147
+ });
148
+ }
149
+ return {
150
+ ...base,
151
+ trialDays: resolvePolarTrialDays({
152
+ trialInterval: liveProduct.trialInterval,
153
+ trialIntervalCount: liveProduct.trialIntervalCount,
154
+ }) ?? base.trialDays,
155
+ unitAmount: typeof fixedPrice?.priceAmount === "number" ? fixedPrice.priceAmount : base.unitAmount,
156
+ meterPrices,
157
+ };
158
+ }
159
+ catch (error) {
160
+ console.warn("Could not load live Polar product pricing for catalog.", {
161
+ error,
162
+ productId: product.productId,
163
+ });
164
+ return base;
165
+ }
166
+ }
167
+ function getOwnerExternalCustomerId(owner, billing, tools) {
168
+ if (billing.resolveExternalCustomerId) {
169
+ return billing.resolveExternalCustomerId(owner, tools);
170
+ }
171
+ return `${owner.kind}:${owner.id}`;
172
+ }
173
+ async function getCustomerStateByExternalId(polar, externalId) {
174
+ try {
175
+ return await polar.customers.getStateExternal({
176
+ externalId,
177
+ });
178
+ }
179
+ catch (error) {
180
+ if (error instanceof ResourceNotFound) {
181
+ return null;
182
+ }
183
+ throw error;
184
+ }
185
+ }
186
+ function resolveActiveProduct(state, products) {
187
+ const byProductId = new Map(products.map((product) => [product.productId, product]));
188
+ const matched = [...(state?.activeSubscriptions ?? [])]
189
+ .filter((subscription) => byProductId.has(subscription.productId))
190
+ .sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime())[0];
191
+ if (!matched) {
192
+ return {
193
+ product: null,
194
+ subscriptionId: null,
195
+ status: "free",
196
+ };
197
+ }
198
+ return {
199
+ product: byProductId.get(matched.productId) ?? null,
200
+ subscriptionId: matched.id,
201
+ status: normalizeStatus(matched.status),
202
+ };
203
+ }
204
+ function findStateMeter(state, meterId) {
205
+ return state?.activeMeters.find((meter) => meter.meterId === meterId) ?? null;
206
+ }
207
+ function resolvePolarPlanLimitReference(value, defaultKey) {
208
+ if (!value || value === "plan_limit" || typeof value === "number") {
209
+ return null;
210
+ }
211
+ if (typeof value === "string") {
212
+ const match = /^plans\.([^.]+)\.limits?\.([^.]+)$/.exec(value);
213
+ if (!match) {
214
+ return null;
215
+ }
216
+ return {
217
+ planId: match[1],
218
+ key: match[2],
219
+ };
220
+ }
221
+ if (typeof value.planId === "string" && value.planId.trim()) {
222
+ return {
223
+ planId: value.planId,
224
+ key: typeof value.key === "string" && value.key.trim() ? value.key : defaultKey,
225
+ };
226
+ }
227
+ return null;
228
+ }
229
+ function resolveSoftLimit(plans, planId, key, includedLimit, guard) {
230
+ if (!guard?.softLimit) {
231
+ return null;
232
+ }
233
+ if (guard.softLimit === "plan_limit") {
234
+ return includedLimit;
235
+ }
236
+ const reference = resolvePolarPlanLimitReference(guard.softLimit, key);
237
+ if (reference) {
238
+ const referencedLimit = plans[reference.planId]?.limits?.[reference.key ?? key] ?? null;
239
+ return typeof referencedLimit === "number" ? referencedLimit : null;
240
+ }
241
+ return typeof guard.softLimit === "number" ? guard.softLimit : null;
242
+ }
243
+ function resolveHardLimit(planId, includedLimit, guard) {
244
+ if (!guard) {
245
+ return null;
246
+ }
247
+ if (typeof guard.hardLimitByPlan?.[planId] === "number") {
248
+ return guard.hardLimitByPlan[planId];
249
+ }
250
+ if (typeof guard.hardLimit === "number") {
251
+ return guard.hardLimit;
252
+ }
253
+ if (typeof includedLimit === "number" && typeof guard.hardOverageByPlan?.[planId] === "number") {
254
+ return includedLimit + guard.hardOverageByPlan[planId];
255
+ }
256
+ if (typeof includedLimit === "number" && typeof guard.hardOverage === "number") {
257
+ return includedLimit + guard.hardOverage;
258
+ }
259
+ return null;
260
+ }
261
+ function getMeterIncrement(aggregation, quantity) {
262
+ switch (aggregation) {
263
+ case "count":
264
+ return 1;
265
+ case "last":
266
+ return quantity;
267
+ default:
268
+ return quantity;
269
+ }
270
+ }
271
+ function resolvePolarMeterId(meter) {
272
+ if (meter.polar?.meterId && meter.polar.meterId.trim()) {
273
+ return meter.polar.meterId;
274
+ }
275
+ if (meter.meterId && meter.meterId.trim()) {
276
+ return meter.meterId;
277
+ }
278
+ return null;
279
+ }
280
+ function resolvePolarQuantityMetadataKey(meter) {
281
+ if (meter.polar?.quantityMetadataKey && meter.polar.quantityMetadataKey.trim()) {
282
+ return meter.polar.quantityMetadataKey;
283
+ }
284
+ if (meter.quantityMetadataKey && meter.quantityMetadataKey.trim()) {
285
+ return meter.quantityMetadataKey;
286
+ }
287
+ return "quantity";
288
+ }
289
+ function toEstimatedMeterChargeAmount(input) {
290
+ const parsedUnitAmount = Number(input.unitAmount ?? Number.NaN);
291
+ if (!Number.isFinite(parsedUnitAmount)) {
292
+ return null;
293
+ }
294
+ const uncappedAmount = Math.round(input.currentUsed * parsedUnitAmount * 100);
295
+ if (typeof input.capAmount === "number") {
296
+ return Math.min(uncappedAmount, input.capAmount);
297
+ }
298
+ return uncappedAmount;
299
+ }
300
+ function isPolarMeteredUnitPrice(value) {
301
+ return (!!value &&
302
+ typeof value === "object" &&
303
+ value.amountType === "metered_unit");
304
+ }
305
+ function isPolarFixedPrice(value) {
306
+ return (!!value &&
307
+ typeof value === "object" &&
308
+ value.amountType === "fixed");
309
+ }
310
+ function projectionKey(input) {
311
+ return `${input.externalCustomerId}:${input.meterId}:${input.currentPeriodEnd ?? "none"}`;
312
+ }
313
+ function getProjectedUsage(input) {
314
+ const key = projectionKey(input);
315
+ const projected = pendingPolarMeterProjection.get(key);
316
+ if (projected == null) {
317
+ return input.currentPeriodUsed;
318
+ }
319
+ if (input.currentPeriodUsed >= projected) {
320
+ pendingPolarMeterProjection.delete(key);
321
+ return input.currentPeriodUsed;
322
+ }
323
+ return projected;
324
+ }
325
+ function setProjectedUsage(input) {
326
+ pendingPolarMeterProjection.set(projectionKey(input), input.projectedUsage);
327
+ }
328
+ async function resolveUsageValue(input) {
329
+ const resolved = await input.billing.usage?.resolve?.(input.owner, input.key, input.tools);
330
+ if (typeof resolved === "number") {
331
+ return resolved;
332
+ }
333
+ const meter = input.billing.meters?.[input.key];
334
+ if (!meter) {
335
+ return null;
336
+ }
337
+ const meterId = resolvePolarMeterId(meter);
338
+ if (!meterId) {
339
+ return null;
340
+ }
341
+ return findStateMeter(input.state, meterId)?.consumedUnits ?? 0;
342
+ }
343
+ async function requireOwner(ctx, billing, polar) {
344
+ const tools = { ctx, polar };
345
+ const owner = await billing.resolveOwner(ctx);
346
+ if (!owner) {
347
+ throw new Error("Polar billing owner could not be resolved for this request.");
348
+ }
349
+ const externalCustomerId = await getOwnerExternalCustomerId(owner, billing, tools);
350
+ const state = await getCustomerStateByExternalId(polar, externalCustomerId);
351
+ return {
352
+ owner,
353
+ externalCustomerId,
354
+ state,
355
+ tools,
356
+ };
357
+ }
358
+ function computeMeterState(input) {
359
+ if (!input.customerFound) {
360
+ return {
361
+ state: "customer_missing",
362
+ warning: "No Polar customer exists for the active billing owner yet.",
363
+ };
364
+ }
365
+ if (!input.meterFound) {
366
+ return {
367
+ state: "meter_missing",
368
+ warning: "The configured Polar customer meter is not active for this customer.",
369
+ };
370
+ }
371
+ if (input.guard?.blockOnPastDue &&
372
+ (input.subscriptionStatus === "past_due" || input.subscriptionStatus === "unpaid")) {
373
+ return {
374
+ state: "blocked_past_due",
375
+ warning: "Usage reporting is blocked while the Polar subscription is past due.",
376
+ };
377
+ }
378
+ if (typeof input.hardLimit === "number" && input.currentUsed >= input.hardLimit) {
379
+ return {
380
+ state: "hard_limit_reached",
381
+ warning: "The configured metered hard cap has been reached for the current billing period.",
382
+ };
383
+ }
384
+ if (typeof input.softLimit === "number" && input.currentUsed >= input.softLimit) {
385
+ return {
386
+ state: "soft_limit_reached",
387
+ warning: typeof input.includedLimit === "number"
388
+ ? "Included usage has been exhausted. Additional usage is billable."
389
+ : "The configured soft limit has been reached.",
390
+ };
391
+ }
392
+ return {
393
+ state: "ok",
394
+ warning: null,
395
+ };
396
+ }
397
+ export function polar(input) {
398
+ const accessToken = input.accessToken ?? process.env.POLAR_ACCESS_TOKEN ?? "";
399
+ const server = input.server ?? process.env.POLAR_SERVER ?? "sandbox";
400
+ const appBaseUrl = input.appBaseUrl ?? process.env.APP_BASE_URL ?? undefined;
401
+ const webhookSecret = process.env.POLAR_WEBHOOK_SECRET ?? undefined;
402
+ if (!accessToken) {
403
+ throw new Error("Polar integration requires POLAR_ACCESS_TOKEN.");
404
+ }
405
+ const billing = input.billing;
406
+ const productsPath = (input.productsPath ?? "/billing/products");
407
+ const statusPath = (input.statusPath ?? "/billing/status");
408
+ const currentChargesPath = (input.currentChargesPath ??
409
+ "/billing/current-charges");
410
+ const featuresPath = (input.featuresPath ?? "/billing/features");
411
+ const limitsPath = (input.limitsPath ?? "/billing/limits");
412
+ const usagePath = (input.usagePath ?? "/billing/usage");
413
+ const meterUsagePath = (input.meterUsagePath ??
414
+ "/billing/meter-usage");
415
+ const reportUsagePath = (input.reportUsagePath ??
416
+ "/billing/report-usage");
417
+ const checkPath = (input.checkPath ?? "/billing/check");
418
+ const checkoutPath = (input.checkoutPath ?? "/billing/checkout");
419
+ const portalPath = (input.portalPath ?? "/billing/portal");
420
+ const webhookDefinitions = normalizeWebhookConfig({
421
+ webhooks: input.webhooks,
422
+ defaultName: "default",
423
+ defaultPath: "/billing/webhook",
424
+ defaultSecret: webhookSecret,
425
+ });
426
+ const plans = billing.plans ?? {};
427
+ const products = normalizeProducts(billing.products);
428
+ for (const [key, meter] of Object.entries(billing.meters ?? {})) {
429
+ if (!meter.eventName || !meter.eventName.trim()) {
430
+ throw new Error(`Polar billing meter "${key}" requires a non-empty eventName.`);
431
+ }
432
+ if (!resolvePolarMeterId(meter)) {
433
+ throw new Error(`Polar billing meter "${key}" requires polar.meterId (or legacy meterId).`);
434
+ }
435
+ }
436
+ const sdk = new Polar({
437
+ accessToken,
438
+ server,
439
+ });
440
+ const webhookRoutes = webhookDefinitions.map((definition) => integrationRoute.post(definition.path, {
441
+ responseFormat: "json",
442
+ rawBody: true,
443
+ async handler(request, context) {
444
+ const rawBody = await request.text();
445
+ const webhookContext = {
446
+ request,
447
+ route: context,
448
+ rawBody,
449
+ headers: request.headers,
450
+ webhook: {
451
+ name: definition.name,
452
+ path: definition.path,
453
+ },
454
+ };
455
+ try {
456
+ if (!definition.secret) {
457
+ throw new Error("Polar webhook secret is required to verify webhook events.");
458
+ }
459
+ const payload = validatePolarWebhookEvent(rawBody, headersToObject(request.headers), definition.secret);
460
+ const event = {
461
+ provider: "polar",
462
+ id: request.headers.get("webhook-id") ??
463
+ `${payload.type}:${payload.timestamp.toISOString()}`,
464
+ type: payload.type,
465
+ data: payload.data,
466
+ raw: payload,
467
+ };
468
+ await definition.onEvent?.(event, webhookContext);
469
+ return Response.json({
470
+ received: true,
471
+ provider: "polar",
472
+ webhook: definition.name,
473
+ eventId: event.id,
474
+ type: event.type,
475
+ });
476
+ }
477
+ catch (error) {
478
+ const override = await definition.onError?.(error, webhookContext);
479
+ if (override) {
480
+ return override;
481
+ }
482
+ return Response.json({
483
+ error: error instanceof Error ? error.message : "Polar webhook verification failed.",
484
+ }, {
485
+ status: error instanceof PolarWebhookVerificationError ? 403 : 400,
486
+ });
487
+ }
488
+ },
489
+ }));
490
+ return defineIntegration({
491
+ category: "payment",
492
+ type: "polar",
493
+ instance: {
494
+ server,
495
+ products: products.map((product) => ({
496
+ id: product.id,
497
+ productId: product.productId,
498
+ kind: product.kind,
499
+ planId: product.planId ?? null,
500
+ })),
501
+ },
502
+ config: integrationConfig({
503
+ label: "Polar integration",
504
+ env: {
505
+ accessToken: "POLAR_ACCESS_TOKEN",
506
+ server: "POLAR_SERVER",
507
+ appBaseUrl: "APP_BASE_URL",
508
+ webhookSecret: "POLAR_WEBHOOK_SECRET",
509
+ },
510
+ input: {
511
+ accessToken,
512
+ server,
513
+ appBaseUrl,
514
+ webhookSecret,
515
+ },
516
+ required: ["accessToken", "server"],
517
+ }),
518
+ api: createPolarApi({
519
+ productsPath,
520
+ statusPath,
521
+ currentChargesPath,
522
+ featuresPath,
523
+ limitsPath,
524
+ usagePath,
525
+ meterUsagePath,
526
+ reportUsagePath,
527
+ checkPath,
528
+ checkoutPath,
529
+ portalPath,
530
+ }),
531
+ log: input.log,
532
+ routes: [
533
+ integrationRoute.get(productsPath, {
534
+ responseFormat: "json",
535
+ async handler() {
536
+ return Response.json(await Promise.all(products
537
+ .filter((product) => product.public)
538
+ .map((product) => enrichCatalogProduct(sdk, product, plans, billing.meters))));
539
+ },
540
+ }),
541
+ integrationRoute.get(statusPath, {
542
+ responseFormat: "json",
543
+ async handler(_request, ctx) {
544
+ const resolved = await requireOwner(ctx, billing, sdk);
545
+ const active = resolveActiveProduct(resolved.state, products);
546
+ const planId = active.product?.planId ?? "free";
547
+ const plan = plans[planId] ?? {};
548
+ const subscription = resolved.state?.activeSubscriptions.find((entry) => entry.id === active.subscriptionId) ?? null;
549
+ return Response.json({
550
+ owner: resolved.owner,
551
+ externalCustomerId: resolved.externalCustomerId,
552
+ customerId: resolved.state?.id ?? null,
553
+ planId,
554
+ productId: active.product?.id ?? null,
555
+ status: active.status,
556
+ subscriptionId: active.subscriptionId,
557
+ currentPeriodStart: subscription?.currentPeriodStart?.toISOString() ?? null,
558
+ currentPeriodEnd: subscription?.currentPeriodEnd?.toISOString() ?? null,
559
+ cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd ?? false,
560
+ trialEndsAt: subscription?.trialEnd?.toISOString() ?? null,
561
+ features: plan.features ?? {},
562
+ limits: plan.limits ?? {},
563
+ entitlements: plan.entitlements ?? {},
564
+ });
565
+ },
566
+ }),
567
+ integrationRoute.get(currentChargesPath, {
568
+ responseFormat: "json",
569
+ async handler(_request, ctx) {
570
+ const owner = await billing.resolveOwner(ctx);
571
+ if (!owner) {
572
+ return Response.json({
573
+ owner: null,
574
+ externalCustomerId: null,
575
+ customerId: null,
576
+ planId: "free",
577
+ productId: null,
578
+ subscriptionId: null,
579
+ subscriptionStatus: "free",
580
+ currency: "usd",
581
+ currentPeriodStart: null,
582
+ currentPeriodEnd: null,
583
+ baseSubscriptionAmount: null,
584
+ pendingMeterChargeAmount: null,
585
+ estimatedTotalAmount: null,
586
+ lineItems: [],
587
+ });
588
+ }
589
+ const resolved = await requireOwner(ctx, billing, sdk);
590
+ const active = resolveActiveProduct(resolved.state, products);
591
+ const planId = active.product?.planId ?? "free";
592
+ const plan = plans[planId] ?? {};
593
+ const subscription = resolved.state?.activeSubscriptions.find((entry) => entry.id === active.subscriptionId) ?? null;
594
+ const currency = subscription?.currency ?? active.product?.currency ?? "usd";
595
+ const baseSubscriptionAmount = typeof subscription?.amount === "number" ? subscription.amount : null;
596
+ const lineItems = [];
597
+ if (baseSubscriptionAmount != null) {
598
+ lineItems.push({
599
+ key: null,
600
+ kind: "base_subscription",
601
+ label: active.product?.name ?? "Base subscription",
602
+ amount: baseSubscriptionAmount,
603
+ currency,
604
+ quantity: 1,
605
+ includedUnits: null,
606
+ overageUnits: null,
607
+ billedBuckets: null,
608
+ billingUnits: null,
609
+ unitAmountDecimal: null,
610
+ });
611
+ }
612
+ let liveProduct = null;
613
+ if (active.product?.productId) {
614
+ try {
615
+ liveProduct = await sdk.products.get({
616
+ id: active.product.productId,
617
+ });
618
+ }
619
+ catch (error) {
620
+ console.warn("Could not load live Polar product pricing for current charges.", {
621
+ error,
622
+ productId: active.product.productId,
623
+ });
624
+ }
625
+ }
626
+ let pendingMeterChargeAmount = 0;
627
+ let sawMeterLine = false;
628
+ for (const [key, meter] of Object.entries(billing.meters ?? {})) {
629
+ const meterId = resolvePolarMeterId(meter);
630
+ if (!meterId) {
631
+ continue;
632
+ }
633
+ const subscriptionMeter = subscription?.meters.find((entry) => entry.meterId === meterId) ?? null;
634
+ const customerMeter = findStateMeter(resolved.state, meterId);
635
+ const currentPeriodEnd = subscription?.currentPeriodEnd?.toISOString() ?? null;
636
+ const currentUsed = getProjectedUsage({
637
+ externalCustomerId: resolved.externalCustomerId,
638
+ meterId,
639
+ currentPeriodEnd,
640
+ currentPeriodUsed: subscriptionMeter?.consumedUnits ?? customerMeter?.consumedUnits ?? 0,
641
+ });
642
+ const liveMeteredPrice = (liveProduct?.prices?.find((price) => isPolarMeteredUnitPrice(price) &&
643
+ price.meterId === meterId) ?? null);
644
+ const unitAmountDecimal = liveMeteredPrice?.unitAmount ?? null;
645
+ const capAmount = typeof liveMeteredPrice?.capAmount === "number" ? liveMeteredPrice.capAmount : null;
646
+ const estimatedMeterChargeAmount = typeof subscriptionMeter?.amount === "number"
647
+ ? subscriptionMeter.amount
648
+ : toEstimatedMeterChargeAmount({
649
+ currentUsed,
650
+ unitAmount: unitAmountDecimal,
651
+ capAmount,
652
+ });
653
+ const includedUnits = plan.limits?.[key] ??
654
+ subscriptionMeter?.creditedUnits ??
655
+ customerMeter?.creditedUnits ??
656
+ null;
657
+ const overageUnits = typeof includedUnits === "number" ? Math.max(currentUsed - includedUnits, 0) : null;
658
+ if (currentUsed <= 0 &&
659
+ estimatedMeterChargeAmount == null &&
660
+ !subscriptionMeter &&
661
+ !customerMeter &&
662
+ !liveMeteredPrice) {
663
+ continue;
664
+ }
665
+ sawMeterLine = true;
666
+ pendingMeterChargeAmount += estimatedMeterChargeAmount ?? 0;
667
+ lineItems.push({
668
+ key,
669
+ kind: "metered_usage",
670
+ label: `${meter.unit ?? key} overage`,
671
+ amount: estimatedMeterChargeAmount,
672
+ currency,
673
+ quantity: currentUsed,
674
+ includedUnits,
675
+ overageUnits,
676
+ billedBuckets: null,
677
+ billingUnits: null,
678
+ unitAmountDecimal,
679
+ });
680
+ }
681
+ return Response.json({
682
+ owner: resolved.owner,
683
+ externalCustomerId: resolved.externalCustomerId,
684
+ customerId: resolved.state?.id ?? null,
685
+ planId,
686
+ productId: active.product?.id ?? null,
687
+ subscriptionId: active.subscriptionId,
688
+ subscriptionStatus: active.status,
689
+ currency,
690
+ currentPeriodStart: subscription?.currentPeriodStart?.toISOString() ?? null,
691
+ currentPeriodEnd: subscription?.currentPeriodEnd?.toISOString() ?? null,
692
+ baseSubscriptionAmount,
693
+ pendingMeterChargeAmount: sawMeterLine ? pendingMeterChargeAmount : null,
694
+ estimatedTotalAmount: (baseSubscriptionAmount ?? 0) + (sawMeterLine ? pendingMeterChargeAmount : 0),
695
+ lineItems,
696
+ });
697
+ },
698
+ }),
699
+ integrationRoute.get(featuresPath, {
700
+ responseFormat: "json",
701
+ async handler(_request, ctx) {
702
+ const resolved = await requireOwner(ctx, billing, sdk);
703
+ const active = resolveActiveProduct(resolved.state, products);
704
+ const planId = active.product?.planId ?? "free";
705
+ const plan = plans[planId] ?? {};
706
+ return Response.json({
707
+ planId,
708
+ features: plan.features ?? {},
709
+ });
710
+ },
711
+ }),
712
+ integrationRoute.get(limitsPath, {
713
+ responseFormat: "json",
714
+ async handler(_request, ctx) {
715
+ const resolved = await requireOwner(ctx, billing, sdk);
716
+ const active = resolveActiveProduct(resolved.state, products);
717
+ const planId = active.product?.planId ?? "free";
718
+ const plan = plans[planId] ?? {};
719
+ return Response.json({
720
+ planId,
721
+ limits: plan.limits ?? {},
722
+ });
723
+ },
724
+ }),
725
+ integrationRoute.post(usagePath, {
726
+ responseFormat: "json",
727
+ async handler(request, ctx) {
728
+ const body = (await request.json().catch(() => ({})));
729
+ if (!body.key) {
730
+ return new Response("A billing usage key is required.", { status: 400 });
731
+ }
732
+ const resolved = await requireOwner(ctx, billing, sdk);
733
+ const active = resolveActiveProduct(resolved.state, products);
734
+ const planId = active.product?.planId ?? "free";
735
+ const plan = plans[planId] ?? {};
736
+ const used = await resolveUsageValue({
737
+ owner: resolved.owner,
738
+ key: body.key,
739
+ tools: resolved.tools,
740
+ billing,
741
+ state: resolved.state,
742
+ });
743
+ const limit = plan.limits?.[body.key] ?? null;
744
+ const remaining = typeof used === "number" && typeof limit === "number"
745
+ ? Math.max(limit - used, 0)
746
+ : null;
747
+ return Response.json({
748
+ planId,
749
+ key: body.key,
750
+ used,
751
+ limit,
752
+ remaining,
753
+ });
754
+ },
755
+ }),
756
+ integrationRoute.post(meterUsagePath, {
757
+ responseFormat: "json",
758
+ async handler(request, ctx) {
759
+ const body = (await request.json().catch(() => ({})));
760
+ if (!body.key) {
761
+ return new Response("A Polar meter key is required.", { status: 400 });
762
+ }
763
+ const meter = billing.meters?.[body.key];
764
+ if (!meter) {
765
+ return new Response(`Unknown Polar meter "${body.key}".`, { status: 404 });
766
+ }
767
+ const meterId = resolvePolarMeterId(meter);
768
+ if (!meterId) {
769
+ return new Response(`Polar meter "${body.key}" is missing a meterId.`, {
770
+ status: 500,
771
+ });
772
+ }
773
+ const resolved = await requireOwner(ctx, billing, sdk);
774
+ const active = resolveActiveProduct(resolved.state, products);
775
+ const planId = active.product?.planId ?? "free";
776
+ const plan = plans[planId] ?? {};
777
+ const subscription = resolved.state?.activeSubscriptions.find((entry) => entry.id === active.subscriptionId) ?? null;
778
+ const subscriptionMeter = subscription?.meters.find((entry) => entry.meterId === meterId) ?? null;
779
+ const customerMeter = findStateMeter(resolved.state, meterId);
780
+ const currentUsed = getProjectedUsage({
781
+ externalCustomerId: resolved.externalCustomerId,
782
+ meterId,
783
+ currentPeriodEnd: subscription?.currentPeriodEnd?.toISOString() ?? null,
784
+ currentPeriodUsed: subscriptionMeter?.consumedUnits ?? customerMeter?.consumedUnits ?? 0,
785
+ });
786
+ let meterUnitAmount = null;
787
+ let meterCapAmount = null;
788
+ let estimatedMeterChargeAmount = typeof subscriptionMeter?.amount === "number" ? subscriptionMeter.amount : null;
789
+ let chargeSource = estimatedMeterChargeAmount != null ? "subscription_meter" : "catalog_rate";
790
+ let meterName = null;
791
+ const baseSubscriptionAmount = typeof subscription?.amount === "number" ? subscription.amount : null;
792
+ let currency = subscription?.currency ?? null;
793
+ if (active.product?.productId) {
794
+ try {
795
+ const liveProduct = await sdk.products.get({
796
+ id: active.product.productId,
797
+ });
798
+ const liveMeteredPrice = (liveProduct.prices?.find((price) => isPolarMeteredUnitPrice(price) &&
799
+ price.meterId === meterId) ?? null);
800
+ meterUnitAmount = liveMeteredPrice?.unitAmount ?? null;
801
+ meterCapAmount =
802
+ typeof liveMeteredPrice?.capAmount === "number" ? liveMeteredPrice.capAmount : null;
803
+ meterName =
804
+ liveMeteredPrice?.meter?.name ??
805
+ null;
806
+ if (estimatedMeterChargeAmount == null) {
807
+ estimatedMeterChargeAmount = toEstimatedMeterChargeAmount({
808
+ currentUsed,
809
+ unitAmount: meterUnitAmount,
810
+ capAmount: meterCapAmount,
811
+ });
812
+ chargeSource = "catalog_rate";
813
+ }
814
+ }
815
+ catch (error) {
816
+ console.warn("Could not load live Polar metered pricing for meter usage.", {
817
+ error,
818
+ productId: active.product.productId,
819
+ meterId,
820
+ });
821
+ }
822
+ }
823
+ const includedLimit = plan.limits?.[body.key] ?? null;
824
+ const softLimit = resolveSoftLimit(plans, planId, body.key, includedLimit, meter.guard);
825
+ const hardLimit = resolveHardLimit(planId, includedLimit, meter.guard);
826
+ const state = computeMeterState({
827
+ currentUsed,
828
+ subscriptionStatus: active.status,
829
+ includedLimit,
830
+ softLimit,
831
+ hardLimit,
832
+ guard: meter.guard,
833
+ meterFound: !!customerMeter,
834
+ customerFound: !!resolved.state,
835
+ });
836
+ return Response.json({
837
+ planId,
838
+ productId: active.product?.id ?? null,
839
+ key: body.key,
840
+ eventName: meter.eventName,
841
+ meterId,
842
+ meterName,
843
+ aggregation: meter.aggregation,
844
+ quantityMetadataKey: resolvePolarQuantityMetadataKey(meter),
845
+ activeMeterIds: resolved.state?.activeMeters?.map((entry) => entry.meterId) ?? [],
846
+ customerId: resolved.state?.id ?? null,
847
+ subscriptionId: active.subscriptionId,
848
+ subscriptionStatus: active.status,
849
+ currentPeriodStart: subscription?.currentPeriodStart?.toISOString() ?? null,
850
+ currentPeriodEnd: subscription?.currentPeriodEnd?.toISOString() ?? null,
851
+ currentPeriodUsed: currentUsed,
852
+ creditedUnits: customerMeter?.creditedUnits ?? null,
853
+ balance: customerMeter?.balance ?? null,
854
+ currency,
855
+ baseSubscriptionAmount,
856
+ meterUnitAmount,
857
+ meterCapAmount,
858
+ chargeSource,
859
+ estimatedMeterChargeAmount,
860
+ estimatedCombinedAmount: typeof baseSubscriptionAmount === "number" &&
861
+ typeof estimatedMeterChargeAmount === "number"
862
+ ? baseSubscriptionAmount + estimatedMeterChargeAmount
863
+ : null,
864
+ includedLimit,
865
+ softLimit,
866
+ hardLimit,
867
+ remainingIncluded: typeof includedLimit === "number" ? Math.max(includedLimit - currentUsed, 0) : null,
868
+ remainingHard: typeof hardLimit === "number" ? Math.max(hardLimit - currentUsed, 0) : null,
869
+ state: state.state,
870
+ warning: state.warning,
871
+ });
872
+ },
873
+ }),
874
+ integrationRoute.post(reportUsagePath, {
875
+ responseFormat: "json",
876
+ async handler(request, ctx) {
877
+ const body = (await request.json().catch(() => ({})));
878
+ if (!body.key) {
879
+ return new Response("A Polar meter key is required.", { status: 400 });
880
+ }
881
+ if (!Number.isFinite(body.quantity) || body.quantity <= 0) {
882
+ return new Response("Polar reported quantity must be a positive number.", {
883
+ status: 400,
884
+ });
885
+ }
886
+ if (!body.idempotencyKey) {
887
+ return new Response("An idempotencyKey is required for Polar usage reporting.", {
888
+ status: 400,
889
+ });
890
+ }
891
+ const meter = billing.meters?.[body.key];
892
+ if (!meter) {
893
+ return new Response(`Unknown Polar meter "${body.key}".`, { status: 404 });
894
+ }
895
+ const meterId = resolvePolarMeterId(meter);
896
+ if (!meterId) {
897
+ return new Response(`Polar meter "${body.key}" is missing a meterId.`, {
898
+ status: 500,
899
+ });
900
+ }
901
+ const occurredAt = typeof body.occurredAt === "string" && body.occurredAt
902
+ ? new Date(body.occurredAt)
903
+ : new Date();
904
+ if (Number.isNaN(occurredAt.getTime())) {
905
+ return new Response("occurredAt must be a valid ISO timestamp.", { status: 400 });
906
+ }
907
+ const resolved = await requireOwner(ctx, billing, sdk);
908
+ const active = resolveActiveProduct(resolved.state, products);
909
+ const planId = active.product?.planId ?? "free";
910
+ const plan = plans[planId] ?? {};
911
+ const subscription = resolved.state?.activeSubscriptions.find((entry) => entry.id === active.subscriptionId) ?? null;
912
+ const customerMeter = findStateMeter(resolved.state, meterId);
913
+ const currentUsed = getProjectedUsage({
914
+ externalCustomerId: resolved.externalCustomerId,
915
+ meterId,
916
+ currentPeriodEnd: subscription?.currentPeriodEnd?.toISOString() ?? null,
917
+ currentPeriodUsed: customerMeter?.consumedUnits ?? 0,
918
+ });
919
+ const includedLimit = plan.limits?.[body.key] ?? null;
920
+ const softLimit = resolveSoftLimit(plans, planId, body.key, includedLimit, meter.guard);
921
+ const hardLimit = resolveHardLimit(planId, includedLimit, meter.guard);
922
+ const state = computeMeterState({
923
+ currentUsed,
924
+ subscriptionStatus: active.status,
925
+ includedLimit,
926
+ softLimit,
927
+ hardLimit,
928
+ guard: meter.guard,
929
+ meterFound: !!customerMeter,
930
+ customerFound: !!resolved.state,
931
+ });
932
+ if (state.state === "blocked_past_due" || state.state === "hard_limit_reached") {
933
+ return new Response(state.warning ?? "Polar usage reporting is blocked for the current billing period.", { status: 409 });
934
+ }
935
+ const increment = getMeterIncrement(meter.aggregation, body.quantity);
936
+ const projectedCurrentPeriodUsed = currentUsed + increment;
937
+ if (typeof hardLimit === "number" && projectedCurrentPeriodUsed > hardLimit) {
938
+ return new Response("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.", { status: 409 });
939
+ }
940
+ const metadata = {
941
+ ...body.properties,
942
+ [resolvePolarQuantityMetadataKey(meter)]: body.quantity,
943
+ };
944
+ await sdk.events.ingest({
945
+ events: [
946
+ {
947
+ name: meter.eventName,
948
+ externalCustomerId: resolved.externalCustomerId,
949
+ externalId: body.idempotencyKey,
950
+ timestamp: occurredAt,
951
+ metadata,
952
+ },
953
+ ],
954
+ });
955
+ setProjectedUsage({
956
+ externalCustomerId: resolved.externalCustomerId,
957
+ meterId,
958
+ currentPeriodEnd: subscription?.currentPeriodEnd?.toISOString() ?? null,
959
+ projectedUsage: projectedCurrentPeriodUsed,
960
+ });
961
+ await billing.hooks?.onUsageReported?.({
962
+ owner: resolved.owner,
963
+ key: body.key,
964
+ quantity: body.quantity,
965
+ idempotencyKey: body.idempotencyKey,
966
+ occurredAt: occurredAt.toISOString(),
967
+ eventName: meter.eventName,
968
+ customerId: resolved.state?.id ?? null,
969
+ projectedCurrentPeriodUsed,
970
+ }, resolved.tools);
971
+ const nextState = typeof hardLimit === "number" && projectedCurrentPeriodUsed >= hardLimit
972
+ ? "hard_limit_reached"
973
+ : typeof softLimit === "number" && projectedCurrentPeriodUsed >= softLimit
974
+ ? "soft_limit_reached"
975
+ : "ok";
976
+ return Response.json({
977
+ key: body.key,
978
+ quantity: body.quantity,
979
+ customerId: resolved.state?.id ?? null,
980
+ eventName: meter.eventName,
981
+ eventIdentifier: body.idempotencyKey,
982
+ occurredAt: occurredAt.toISOString(),
983
+ currentPeriodUsed: currentUsed,
984
+ projectedCurrentPeriodUsed,
985
+ softLimit,
986
+ hardLimit,
987
+ state: nextState,
988
+ warning: nextState === "soft_limit_reached"
989
+ ? "Included usage has been exhausted. Additional usage is billable."
990
+ : nextState === "hard_limit_reached"
991
+ ? "The configured metered hard cap has been reached for the current billing period."
992
+ : null,
993
+ });
994
+ },
995
+ }),
996
+ integrationRoute.post(checkPath, {
997
+ responseFormat: "json",
998
+ async handler(request, ctx) {
999
+ const body = (await request.json().catch(() => ({})));
1000
+ if (!body.key) {
1001
+ return new Response("A billing check key is required.", { status: 400 });
1002
+ }
1003
+ const amount = typeof body.amount === "number" && Number.isFinite(body.amount) && body.amount > 0
1004
+ ? body.amount
1005
+ : 1;
1006
+ const resolved = await requireOwner(ctx, billing, sdk);
1007
+ const active = resolveActiveProduct(resolved.state, products);
1008
+ const planId = active.product?.planId ?? "free";
1009
+ const plan = plans[planId] ?? {};
1010
+ const used = await resolveUsageValue({
1011
+ owner: resolved.owner,
1012
+ key: body.key,
1013
+ tools: resolved.tools,
1014
+ billing,
1015
+ state: resolved.state,
1016
+ });
1017
+ const limit = plan.limits?.[body.key] ?? null;
1018
+ const remaining = typeof used === "number" && typeof limit === "number"
1019
+ ? Math.max(limit - used, 0)
1020
+ : null;
1021
+ const allowed = typeof used === "number" && typeof limit === "number" ? used + amount <= limit : true;
1022
+ return Response.json({
1023
+ planId,
1024
+ key: body.key,
1025
+ amount,
1026
+ used,
1027
+ limit,
1028
+ remaining,
1029
+ allowed,
1030
+ });
1031
+ },
1032
+ }),
1033
+ integrationRoute.post(checkoutPath, {
1034
+ responseFormat: "json",
1035
+ async handler(request, ctx) {
1036
+ const body = (await request.json().catch(() => ({})));
1037
+ if (!body.productId) {
1038
+ return new Response("A Polar product id is required.", { status: 400 });
1039
+ }
1040
+ const product = products.find((entry) => entry.id === body.productId);
1041
+ if (!product) {
1042
+ return new Response(`Unknown Polar product "${body.productId}".`, { status: 404 });
1043
+ }
1044
+ const resolved = await requireOwner(ctx, billing, sdk);
1045
+ const successPath = resolveAppPath(body.successPath ?? "/success", "Polar checkout successPath") ??
1046
+ "/success";
1047
+ const cancelPath = resolveAppPath(body.cancelPath ?? "/cancel", "Polar checkout cancelPath") ?? "/cancel";
1048
+ const successUrl = toAbsoluteUrl(successPath, request, appBaseUrl);
1049
+ successUrl.searchParams.set("checkout_id", "{CHECKOUT_ID}");
1050
+ const returnUrl = toAbsoluteUrl(cancelPath, request, appBaseUrl);
1051
+ const checkout = await sdk.checkouts.create({
1052
+ products: [product.productId],
1053
+ successUrl: successUrl.toString(),
1054
+ returnUrl: returnUrl.toString(),
1055
+ externalCustomerId: resolved.externalCustomerId,
1056
+ customerEmail: body.customerEmail ?? resolved.owner.email ?? null,
1057
+ metadata: body.metadata,
1058
+ });
1059
+ return Response.json({
1060
+ productId: product.id,
1061
+ planId: product.planId ?? null,
1062
+ checkoutId: checkout.id,
1063
+ redirectTo: checkout.url,
1064
+ mode: product.kind === "subscription" ? "subscription" : "payment",
1065
+ });
1066
+ },
1067
+ }),
1068
+ integrationRoute.post(portalPath, {
1069
+ responseFormat: "json",
1070
+ async handler(request, ctx) {
1071
+ const body = (await request.json().catch(() => ({})));
1072
+ const resolved = await requireOwner(ctx, billing, sdk);
1073
+ const returnPath = resolveAppPath(body.returnTo ?? "/", "Polar portal returnTo") ?? "/";
1074
+ const returnUrl = toAbsoluteUrl(returnPath, request, appBaseUrl);
1075
+ const session = await sdk.customerSessions.create({
1076
+ externalCustomerId: resolved.externalCustomerId,
1077
+ returnUrl: returnUrl.toString(),
1078
+ });
1079
+ return Response.json({
1080
+ customerId: session.customerId,
1081
+ redirectTo: session.customerPortalUrl,
1082
+ });
1083
+ },
1084
+ }),
1085
+ ...webhookRoutes,
1086
+ ],
1087
+ });
1088
+ }