@lunora/payment 1.0.0-alpha.3 → 1.0.0-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +5 -2
  3. package/dist/index.d.mts +266 -523
  4. package/dist/index.d.ts +266 -523
  5. package/dist/index.mjs +11 -13
  6. package/dist/packem_shared/{LunoraPaymentError-B3hEzXSs.mjs → LunoraPaymentError-BeQlhkZj.mjs} +4 -7
  7. package/dist/packem_shared/adapter.d-CMjjkTGB.d.mts +427 -0
  8. package/dist/packem_shared/adapter.d-CMjjkTGB.d.ts +427 -0
  9. package/dist/packem_shared/{addMoney-bCcs1nyw.mjs → addMoney-jSh_7TfF.mjs} +1 -1
  10. package/dist/packem_shared/{applyWebhookAction-DpAqf3Lw.mjs → applyWebhookAction-CLAx4svt.mjs} +1 -1
  11. package/dist/packem_shared/{constantTimeEqual-CfY0jYcL.mjs → constantTimeEqual-BQjoW85H.mjs} +14 -38
  12. package/dist/packem_shared/{createAdapterRegistry-BuDHFCBc.mjs → createAdapterRegistry-Ds7bx_TK.mjs} +1 -1
  13. package/dist/packem_shared/{createDatabasePaymentStore-bYB_HUE6.mjs → createDatabasePaymentStore-C0NUCj1H.mjs} +1 -1
  14. package/dist/packem_shared/{createPayment-BccfPGyw.mjs → createPayment-CKQAznOL.mjs} +26 -16
  15. package/dist/packem_shared/idempotencyKey-BzaHGdHD.mjs +9 -0
  16. package/dist/packem_shared/json-D0Cb5aMv.mjs +29 -0
  17. package/dist/packem_shared/{lunoraDatabaseToPaymentDatabase-RlKX3Kcd.mjs → lunoraDatabaseToPaymentDatabase-DTsgPvfO.mjs} +2 -2
  18. package/dist/packem_shared/not-supported-C0onRyia.mjs +7 -0
  19. package/dist/packem_shared/paymentTables-D4TbhaIk.mjs +59 -0
  20. package/dist/packem_shared/{reconcile-CI1ukJF9.mjs → reconcile-qE3ERi3D.mjs} +14 -2
  21. package/dist/packem_shared/subscription-event-CwEsWRCK.mjs +17 -0
  22. package/dist/providers/autumn-features.d.mts +129 -0
  23. package/dist/providers/autumn-features.d.ts +129 -0
  24. package/dist/providers/autumn-features.mjs +95 -0
  25. package/dist/providers/autumn.d.mts +28 -0
  26. package/dist/providers/autumn.d.ts +28 -0
  27. package/dist/providers/autumn.mjs +275 -0
  28. package/dist/providers/creem.d.mts +26 -0
  29. package/dist/providers/creem.d.ts +26 -0
  30. package/dist/providers/creem.mjs +236 -0
  31. package/dist/providers/dodopayments.d.mts +30 -0
  32. package/dist/providers/dodopayments.d.ts +30 -0
  33. package/dist/providers/dodopayments.mjs +259 -0
  34. package/dist/providers/polar.d.mts +31 -0
  35. package/dist/providers/polar.d.ts +31 -0
  36. package/dist/{packem_shared/createPolarAdapter-BJtVGSlF.mjs → providers/polar.mjs} +63 -49
  37. package/dist/providers/stripe.d.mts +32 -0
  38. package/dist/providers/stripe.d.ts +32 -0
  39. package/dist/{packem_shared/createStripeAdapter-D40MVBXg.mjs → providers/stripe.mjs} +56 -55
  40. package/package.json +46 -5
  41. package/dist/packem_shared/idempotencyKey-BFzDCA7g.mjs +0 -3
  42. package/dist/packem_shared/json-Db337f36.mjs +0 -6
  43. package/dist/packem_shared/paymentTables-DccHwWr_.mjs +0 -127
@@ -0,0 +1,259 @@
1
+ import { LunoraPaymentError } from '../packem_shared/LunoraPaymentError-BeQlhkZj.mjs';
2
+ import { idempotencyKey } from '../packem_shared/idempotencyKey-BzaHGdHD.mjs';
3
+ import { a as asRecord, b as readNumber, r as readString, f as referenceFromMetadata, p as parseTimestamp, e as readBoolean } from '../packem_shared/json-D0Cb5aMv.mjs';
4
+ import { money, zeroMoney } from '../packem_shared/addMoney-jSh_7TfF.mjs';
5
+ import { verifyStandardWebhook } from '../packem_shared/constantTimeEqual-BQjoW85H.mjs';
6
+ import { m as makeNotSupported } from '../packem_shared/not-supported-C0onRyia.mjs';
7
+ import { s as stateToEventType } from '../packem_shared/subscription-event-CwEsWRCK.mjs';
8
+
9
+ const PAYMENT_STATE_BY_DODO_STATUS = {
10
+ cancelled: "canceled",
11
+ failed: "failed",
12
+ partially_captured: "captured",
13
+ partially_captured_and_capturable: "captured",
14
+ processing: "initiated",
15
+ requires_capture: "authorized",
16
+ requires_confirmation: "initiated",
17
+ requires_customer_action: "authorized",
18
+ requires_merchant_action: "authorized",
19
+ requires_payment_method: "initiated",
20
+ succeeded: "captured"
21
+ };
22
+ const SUBSCRIPTION_STATE_BY_DODO_STATUS = {
23
+ active: "active",
24
+ cancelled: "canceled",
25
+ expired: "canceled",
26
+ // SECURITY: `pending` (first payment not completed), `failed`, and `on_hold` (dunning) must not
27
+ // map to an entitling state — see the equivalent `incomplete` note in the Stripe/Polar adapters.
28
+ // Treat them as non-entitling `past_due`; Dodo has no trial status in this enum.
29
+ failed: "past_due",
30
+ on_hold: "past_due",
31
+ // A paused subscription is non-entitling until it resumes, but distinct from dunning — it maps to
32
+ // the `paused` state so a `subscription.paused` webhook routes to `subscription.paused` (not the
33
+ // generic `subscription.updated`) and stays consistent with `subscriptionFromDodo`.
34
+ paused: "paused",
35
+ pending: "past_due"
36
+ };
37
+ const notSupported = makeNotSupported("dodopayments (merchant-of-record)");
38
+ const customerIdOf = (object) => readString(asRecord(object.customer), "customer_id") ?? readString(object, "customer_id");
39
+ const subscriptionFromDodo = (input) => {
40
+ const subscription = asRecord(input);
41
+ const now = Date.now();
42
+ const status = readString(subscription, "status") ?? "";
43
+ return {
44
+ cancelAtPeriodEnd: readBoolean(subscription, "cancel_at_next_billing_date") ?? false,
45
+ createdAt: now,
46
+ currentPeriodEnd: parseTimestamp(readString(subscription, "next_billing_date")),
47
+ currentPeriodStart: parseTimestamp(readString(subscription, "previous_billing_date")),
48
+ id: readString(subscription, "subscription_id") ?? "",
49
+ priceId: readString(subscription, "product_id") ?? "",
50
+ provider: "dodopayments",
51
+ quantity: readNumber(subscription, "quantity") ?? 1,
52
+ referenceId: referenceFromMetadata(subscription) ?? customerIdOf(subscription) ?? "",
53
+ // Fail closed: an unrecognized Dodo status is treated as non-entitling `past_due`.
54
+ state: SUBSCRIPTION_STATE_BY_DODO_STATUS[status] ?? "past_due",
55
+ updatedAt: now
56
+ };
57
+ };
58
+ const paymentFromDodo = (input) => {
59
+ const payment = asRecord(input);
60
+ const now = Date.now();
61
+ const currency = readString(payment, "currency") ?? "usd";
62
+ const amount = money(BigInt(Math.round(readNumber(payment, "total_amount") ?? 0)), currency);
63
+ const state = PAYMENT_STATE_BY_DODO_STATUS[readString(payment, "status") ?? ""] ?? "initiated";
64
+ return {
65
+ amount,
66
+ capturedAmount: state === "captured" ? amount : zeroMoney(currency),
67
+ createdAt: now,
68
+ id: readString(payment, "payment_id") ?? "",
69
+ provider: "dodopayments",
70
+ referenceId: referenceFromMetadata(payment) ?? "",
71
+ refundedAmount: zeroMoney(currency),
72
+ state,
73
+ updatedAt: now
74
+ };
75
+ };
76
+ const mapEvent = (eventId, eventType, object) => {
77
+ const base = { eventId, provider: "dodopayments", raw: { object, type: eventType } };
78
+ const currency = readString(object, "currency") ?? "usd";
79
+ switch (eventType) {
80
+ // A lost chargeback (`dispute.lost`) is a definitive funds reversal carrying `amount` +
81
+ // `payment_id` — economically a refund, so it shares the refund mapping (record it so a customer
82
+ // who charges back doesn't stay entitled). Provisional dispute stages move no money and stay
83
+ // `unhandled`; a won dispute needs no transition.
84
+ case "dispute.lost":
85
+ case "refund.succeeded": {
86
+ return {
87
+ ...base,
88
+ amount: money(BigInt(Math.round(readNumber(object, "amount") ?? 0)), currency),
89
+ referenceId: referenceFromMetadata(object),
90
+ sessionId: readString(object, "payment_id"),
91
+ type: "payment.refunded"
92
+ };
93
+ }
94
+ // A cancelled payment never settled — record it as a non-entitling failure (there is no
95
+ // dedicated `payment.canceled` action; `failed` is the closest terminal, non-entitling state).
96
+ case "payment.cancelled":
97
+ case "payment.failed": {
98
+ return { ...base, referenceId: referenceFromMetadata(object), sessionId: readString(object, "payment_id"), type: "payment.failed" };
99
+ }
100
+ case "payment.succeeded": {
101
+ return {
102
+ ...base,
103
+ amount: money(BigInt(Math.round(readNumber(object, "total_amount") ?? 0)), currency),
104
+ customerId: customerIdOf(object),
105
+ referenceId: referenceFromMetadata(object),
106
+ sessionId: readString(object, "payment_id"),
107
+ subscriptionId: readString(object, "subscription_id"),
108
+ type: "payment.captured"
109
+ };
110
+ }
111
+ case "subscription.active":
112
+ case "subscription.cancelled":
113
+ case "subscription.expired":
114
+ case "subscription.failed":
115
+ case "subscription.on_hold":
116
+ case "subscription.paused":
117
+ case "subscription.plan_changed":
118
+ case "subscription.renewed":
119
+ case "subscription.updated": {
120
+ const status = readString(object, "status");
121
+ return {
122
+ ...base,
123
+ cancelAtPeriodEnd: readBoolean(object, "cancel_at_next_billing_date"),
124
+ currentPeriodEnd: parseTimestamp(readString(object, "next_billing_date")),
125
+ currentPeriodStart: parseTimestamp(readString(object, "previous_billing_date")),
126
+ customerId: customerIdOf(object),
127
+ priceId: readString(object, "product_id"),
128
+ quantity: readNumber(object, "quantity"),
129
+ referenceId: referenceFromMetadata(object) ?? customerIdOf(object),
130
+ subscriptionId: readString(object, "subscription_id"),
131
+ type: stateToEventType(SUBSCRIPTION_STATE_BY_DODO_STATUS[status ?? ""])
132
+ };
133
+ }
134
+ default: {
135
+ return { ...base, type: "unhandled" };
136
+ }
137
+ }
138
+ };
139
+ const createDodoPaymentsAdapter = (options) => {
140
+ const { webhookSecret } = options;
141
+ const client = options.client;
142
+ return {
143
+ // Dodo is a Merchant-of-Record: it moves the money, so there is no manual payment-intent
144
+ // authorize/capture/cancel. Refunds, however, are supported below.
145
+ cancelPayment: () => notSupported("manual payment cancellation"),
146
+ cancelSubscription: async (subscriptionId, cancelOptions) => {
147
+ const body = cancelOptions?.atPeriodEnd ? { cancel_at_next_billing_date: true } : { status: "cancelled" };
148
+ return subscriptionFromDodo(await client.subscriptions.update(subscriptionId, body));
149
+ },
150
+ capabilities: { merchantOfRecord: true, portal: true, usageMetering: true },
151
+ capturePayment: (_input) => notSupported("manual capture"),
152
+ createCheckout: async (input) => {
153
+ const session = asRecord(
154
+ await client.checkoutSessions.create({
155
+ // An existing Dodo customer is attached by id; otherwise Dodo collects one at checkout.
156
+ customer: input.customerId ? { customer_id: input.customerId } : void 0,
157
+ // Pin the framework-controlled `referenceId` LAST so caller metadata can never override it.
158
+ metadata: { ...input.metadata, referenceId: input.referenceId },
159
+ product_cart: [{ product_id: input.priceId, quantity: input.quantity ?? 1 }],
160
+ return_url: input.successUrl
161
+ })
162
+ );
163
+ return { id: readString(session, "session_id") ?? "", provider: "dodopayments", url: readString(session, "checkout_url") ?? "" };
164
+ },
165
+ createPortalSession: async (input) => {
166
+ const session = asRecord(await client.customers.customerPortal.create(input.customerId, { return_url: input.returnUrl }));
167
+ return { url: readString(session, "link") ?? readString(session, "url") ?? "" };
168
+ },
169
+ getOrCreateCustomer: async (ref) => {
170
+ const customer = asRecord(
171
+ await client.customers.create(
172
+ { email: ref.email ?? "", name: ref.metadata?.name ?? ref.referenceId },
173
+ { idempotencyKey: idempotencyKey("customer", "dodopayments", ref.referenceId) }
174
+ )
175
+ );
176
+ return {
177
+ createdAt: Date.now(),
178
+ email: readString(customer, "email") ?? ref.email,
179
+ id: readString(customer, "customer_id") ?? "",
180
+ provider: "dodopayments",
181
+ referenceId: ref.referenceId
182
+ };
183
+ },
184
+ getPaymentStatus: async (sessionId) => paymentFromDodo(await client.payments.retrieve(sessionId)),
185
+ getSubscriptionStatus: async (subscriptionId) => subscriptionFromDodo(await client.subscriptions.retrieve(subscriptionId)),
186
+ identifier: "dodopayments",
187
+ parseWebhook: async ({ headers, payload }) => {
188
+ const webhookId = headers.get("webhook-id") ?? "";
189
+ await verifyStandardWebhook({
190
+ payload,
191
+ secret: webhookSecret,
192
+ toleranceSeconds: options.webhookToleranceSeconds,
193
+ webhookId,
194
+ webhookSignature: headers.get("webhook-signature") ?? "",
195
+ webhookTimestamp: headers.get("webhook-timestamp") ?? ""
196
+ });
197
+ const event = asRecord(JSON.parse(payload));
198
+ return mapEvent(webhookId, readString(event, "type") ?? "", asRecord(event.data));
199
+ },
200
+ refundPayment: async (input) => {
201
+ if (input.amount !== void 0) {
202
+ throw new LunoraPaymentError(
203
+ "PROVIDER_ERROR",
204
+ "dodopayments refunds a payment in full; partial refunds require line items and aren't supported here"
205
+ );
206
+ }
207
+ const refund = asRecord(await client.refunds.create({ payment_id: input.sessionId, reason: input.reason }));
208
+ const currency = readString(refund, "currency") ?? "usd";
209
+ const refundedAmount = money(BigInt(Math.round(readNumber(refund, "amount") ?? 0)), currency);
210
+ let state = "captured";
211
+ if (readString(refund, "status") === "succeeded") {
212
+ state = "refunded";
213
+ }
214
+ return {
215
+ amount: refundedAmount,
216
+ capturedAmount: refundedAmount,
217
+ createdAt: Date.now(),
218
+ id: input.sessionId,
219
+ provider: "dodopayments",
220
+ referenceId: "",
221
+ refundedAmount,
222
+ state,
223
+ updatedAt: Date.now()
224
+ };
225
+ },
226
+ reportUsage: async (input) => {
227
+ await client.usageEvents.ingest({
228
+ events: [
229
+ {
230
+ customer_id: input.customerId ?? input.referenceId,
231
+ event_id: input.idempotencyKey,
232
+ event_name: input.featureId,
233
+ metadata: { value: input.quantity },
234
+ timestamp: input.timestamp === void 0 ? void 0 : new Date(input.timestamp).toISOString()
235
+ }
236
+ ]
237
+ });
238
+ },
239
+ resumeSubscription: async (subscriptionId) => {
240
+ const subscription = await client.subscriptions.update(subscriptionId, { cancel_at_next_billing_date: false });
241
+ return subscriptionFromDodo(subscription);
242
+ },
243
+ updateSubscription: async (subscriptionId, patch) => {
244
+ if (patch.priceId !== void 0 || patch.quantity !== void 0) {
245
+ const current = asRecord(
246
+ patch.priceId !== void 0 && patch.quantity !== void 0 ? void 0 : await client.subscriptions.retrieve(subscriptionId)
247
+ );
248
+ await client.subscriptions.changePlan(subscriptionId, {
249
+ product_id: patch.priceId ?? readString(current, "product_id") ?? "",
250
+ proration_billing_mode: "prorated_immediately",
251
+ quantity: patch.quantity ?? readNumber(current, "quantity") ?? 1
252
+ });
253
+ }
254
+ return subscriptionFromDodo(await client.subscriptions.retrieve(subscriptionId));
255
+ }
256
+ };
257
+ };
258
+
259
+ export { createDodoPaymentsAdapter };
@@ -0,0 +1,31 @@
1
+ import { P as PaymentAdapter } from "../packem_shared/adapter.d-CMjjkTGB.mjs";
2
+ /**
3
+ * The `@polar-sh/sdk` surface the adapter uses, as a structural type — a real `Polar` instance
4
+ * satisfies it without a cast. Resources are `unknown` (the adapter re-types the client as the real
5
+ * `Polar` internally); this keeps the SDK's full type out of the published declarations.
6
+ * @experimental
7
+ */
8
+ interface PolarClientLike {
9
+ readonly checkouts: unknown;
10
+ readonly customers: unknown;
11
+ readonly customerSessions: unknown;
12
+ readonly events: unknown;
13
+ readonly orders: unknown;
14
+ readonly refunds: unknown;
15
+ readonly subscriptions: unknown;
16
+ }
17
+ /**
18
+ * `PolarAdapterOptions` is part of the experimental `@lunora/payment` API and may change without a major version bump.
19
+ * @experimental
20
+ */
21
+ interface PolarAdapterOptions {
22
+ readonly client: PolarClientLike;
23
+ readonly webhookSecret: string;
24
+ readonly webhookToleranceSeconds?: number;
25
+ }
26
+ /**
27
+ * `createPolarAdapter` is part of the experimental `@lunora/payment` API and may change without a major version bump.
28
+ * @experimental
29
+ */
30
+ declare const createPolarAdapter: (options: PolarAdapterOptions) => PaymentAdapter;
31
+ export { type PolarAdapterOptions, type PolarClientLike, createPolarAdapter };
@@ -0,0 +1,31 @@
1
+ import { P as PaymentAdapter } from "../packem_shared/adapter.d-CMjjkTGB.js";
2
+ /**
3
+ * The `@polar-sh/sdk` surface the adapter uses, as a structural type — a real `Polar` instance
4
+ * satisfies it without a cast. Resources are `unknown` (the adapter re-types the client as the real
5
+ * `Polar` internally); this keeps the SDK's full type out of the published declarations.
6
+ * @experimental
7
+ */
8
+ interface PolarClientLike {
9
+ readonly checkouts: unknown;
10
+ readonly customers: unknown;
11
+ readonly customerSessions: unknown;
12
+ readonly events: unknown;
13
+ readonly orders: unknown;
14
+ readonly refunds: unknown;
15
+ readonly subscriptions: unknown;
16
+ }
17
+ /**
18
+ * `PolarAdapterOptions` is part of the experimental `@lunora/payment` API and may change without a major version bump.
19
+ * @experimental
20
+ */
21
+ interface PolarAdapterOptions {
22
+ readonly client: PolarClientLike;
23
+ readonly webhookSecret: string;
24
+ readonly webhookToleranceSeconds?: number;
25
+ }
26
+ /**
27
+ * `createPolarAdapter` is part of the experimental `@lunora/payment` API and may change without a major version bump.
28
+ * @experimental
29
+ */
30
+ declare const createPolarAdapter: (options: PolarAdapterOptions) => PaymentAdapter;
31
+ export { type PolarAdapterOptions, type PolarClientLike, createPolarAdapter };
@@ -1,84 +1,83 @@
1
- import { LunoraPaymentError } from './LunoraPaymentError-B3hEzXSs.mjs';
2
- import { a as asRecord, r as readString, b as readBoolean, c as readNumber } from './json-Db337f36.mjs';
3
- import { money, zeroMoney } from './addMoney-bCcs1nyw.mjs';
4
- import { verifyStandardWebhook } from './constantTimeEqual-CfY0jYcL.mjs';
1
+ import { a as asRecord, r as readString, f as referenceFromMetadata, e as readBoolean, p as parseTimestamp, b as readNumber } from '../packem_shared/json-D0Cb5aMv.mjs';
2
+ import { money, zeroMoney } from '../packem_shared/addMoney-jSh_7TfF.mjs';
3
+ import { verifyStandardWebhook } from '../packem_shared/constantTimeEqual-BQjoW85H.mjs';
4
+ import { m as makeNotSupported } from '../packem_shared/not-supported-C0onRyia.mjs';
5
+ import { s as stateToEventType } from '../packem_shared/subscription-event-CwEsWRCK.mjs';
5
6
 
7
+ const toEpochMs = (value) => {
8
+ if (value instanceof Date) {
9
+ return value.getTime();
10
+ }
11
+ return parseTimestamp(typeof value === "string" ? value : void 0);
12
+ };
6
13
  const PAYMENT_STATE_BY_POLAR_ORDER_STATUS = {
14
+ draft: "initiated",
7
15
  paid: "captured",
8
16
  partially_refunded: "partially_refunded",
9
17
  pending: "initiated",
10
- refunded: "refunded"
18
+ refunded: "refunded",
19
+ void: "canceled"
11
20
  };
12
21
  const SUBSCRIPTION_STATE_BY_POLAR_STATUS = {
13
22
  active: "active",
14
23
  canceled: "canceled",
15
- incomplete: "trialing",
24
+ // SECURITY: `incomplete` (first payment not completed) must not map to an
25
+ // entitling state — see the equivalent note in the Stripe adapter. Treat it as
26
+ // non-entitling `past_due`; reserve `trialing` for a genuine trial.
27
+ incomplete: "past_due",
16
28
  incomplete_expired: "canceled",
17
29
  past_due: "past_due",
18
30
  trialing: "trialing",
19
31
  unpaid: "past_due"
20
32
  };
21
- const notSupported = (operation) => {
22
- throw new LunoraPaymentError("PROVIDER_ERROR", `polar (merchant-of-record) does not support ${operation}`);
23
- };
24
- const parseTimestamp = (value) => {
25
- const parsed = typeof value === "string" ? Date.parse(value) : Number.NaN;
26
- return Number.isNaN(parsed) ? void 0 : parsed;
27
- };
28
- const subscriptionFromPolar = (subscription) => {
33
+ const notSupported = makeNotSupported("polar (merchant-of-record)");
34
+ const subscriptionFromPolar = (input) => {
35
+ const subscription = asRecord(input);
29
36
  const now = Date.now();
30
37
  return {
31
- cancelAtPeriodEnd: subscription.cancelAtPeriodEnd ?? false,
38
+ cancelAtPeriodEnd: readBoolean(subscription, "cancelAtPeriodEnd") ?? false,
32
39
  createdAt: now,
33
- currentPeriodEnd: parseTimestamp(subscription.currentPeriodEnd),
34
- currentPeriodStart: parseTimestamp(subscription.currentPeriodStart),
35
- id: subscription.id,
36
- priceId: subscription.productId ?? "",
40
+ currentPeriodEnd: toEpochMs(subscription.currentPeriodEnd),
41
+ currentPeriodStart: toEpochMs(subscription.currentPeriodStart),
42
+ id: readString(subscription, "id") ?? "",
43
+ priceId: readString(subscription, "productId") ?? "",
37
44
  provider: "polar",
38
45
  quantity: 1,
39
- referenceId: subscription.metadata?.referenceId ?? "",
40
- state: SUBSCRIPTION_STATE_BY_POLAR_STATUS[subscription.status] ?? "active",
46
+ referenceId: referenceFromMetadata(subscription) ?? "",
47
+ state: SUBSCRIPTION_STATE_BY_POLAR_STATUS[readString(subscription, "status") ?? ""] ?? "active",
41
48
  updatedAt: now
42
49
  };
43
50
  };
44
- const orderToSession = (order) => {
51
+ const orderToSession = (input) => {
52
+ const order = asRecord(input);
45
53
  const now = Date.now();
46
- const currency = order.currency ?? "usd";
47
- const amount = money(BigInt(order.totalAmount ?? order.amount ?? 0), currency);
48
- const state = PAYMENT_STATE_BY_POLAR_ORDER_STATUS[order.status] ?? "initiated";
54
+ const currency = readString(order, "currency") ?? "usd";
55
+ const amount = money(BigInt(Math.round(readNumber(order, "totalAmount") ?? readNumber(order, "amount") ?? 0)), currency);
56
+ const state = PAYMENT_STATE_BY_POLAR_ORDER_STATUS[readString(order, "status") ?? ""] ?? "initiated";
49
57
  const settled = state === "captured" || state === "partially_refunded" || state === "refunded";
50
58
  return {
51
59
  amount,
52
60
  capturedAmount: settled ? amount : zeroMoney(currency),
53
61
  createdAt: now,
54
- id: order.id,
62
+ id: readString(order, "id") ?? "",
55
63
  provider: "polar",
56
- referenceId: "",
64
+ // Polar copies checkout metadata onto the order, so recover the framework-pinned `referenceId`
65
+ // rather than blanking it — a reconcile sweep would otherwise orphan the row from `by_reference`.
66
+ referenceId: referenceFromMetadata(order) ?? "",
57
67
  refundedAmount: state === "refunded" ? amount : zeroMoney(currency),
58
68
  state,
59
69
  updatedAt: now
60
70
  };
61
71
  };
62
- const subscriptionEventType = (status) => {
63
- const state = status ? SUBSCRIPTION_STATE_BY_POLAR_STATUS[status] : void 0;
64
- if (state === "canceled") {
65
- return "subscription.canceled";
66
- }
67
- if (state === "past_due") {
68
- return "subscription.past_due";
69
- }
70
- if (state === "active" || state === "trialing") {
71
- return "subscription.active";
72
- }
73
- return "subscription.updated";
74
- };
75
- const referenceFromMetadata = (object) => readString(asRecord(object.metadata), "referenceId");
76
72
  const mapEvent = (eventId, eventType, object) => {
77
73
  const base = { eventId, provider: "polar", raw: { object, type: eventType } };
78
74
  const currency = readString(object, "currency") ?? "usd";
79
75
  switch (eventType) {
80
76
  case "order.created":
81
77
  case "order.paid": {
78
+ if (eventType === "order.created" && PAYMENT_STATE_BY_POLAR_ORDER_STATUS[readString(object, "status") ?? ""] !== "captured") {
79
+ return { ...base, type: "unhandled" };
80
+ }
82
81
  return {
83
82
  ...base,
84
83
  amount: money(BigInt(readNumber(object, "total_amount") ?? readNumber(object, "amount") ?? 0), currency),
@@ -102,8 +101,9 @@ const mapEvent = (eventId, eventType, object) => {
102
101
  case "subscription.canceled":
103
102
  case "subscription.created":
104
103
  case "subscription.revoked":
104
+ case "subscription.uncanceled":
105
105
  case "subscription.updated": {
106
- const type = eventType === "subscription.revoked" ? "subscription.canceled" : subscriptionEventType(readString(object, "status"));
106
+ const type = eventType === "subscription.revoked" ? "subscription.canceled" : stateToEventType(SUBSCRIPTION_STATE_BY_POLAR_STATUS[readString(object, "status") ?? ""]);
107
107
  return {
108
108
  ...base,
109
109
  cancelAtPeriodEnd: readBoolean(object, "cancel_at_period_end"),
@@ -122,7 +122,8 @@ const mapEvent = (eventId, eventType, object) => {
122
122
  }
123
123
  };
124
124
  const createPolarAdapter = (options) => {
125
- const { client, webhookSecret } = options;
125
+ const { webhookSecret } = options;
126
+ const client = options.client;
126
127
  return {
127
128
  cancelPayment: () => notSupported("manual payment cancellation"),
128
129
  cancelSubscription: async (subscriptionId, cancelOptions) => {
@@ -133,10 +134,18 @@ const createPolarAdapter = (options) => {
133
134
  capturePayment: (_input) => notSupported("manual capture"),
134
135
  createCheckout: async (input) => {
135
136
  const checkout = await client.checkouts.create({
136
- customerEmail: void 0,
137
+ // Bind the checkout to the customer the facade already reused/minted for this reference,
138
+ // else Polar mints a SECOND orphan customer at completion (leaving the stored customer
139
+ // with no subscription — its portal empty, its metered usage misrouted). `externalCustomerId`
140
+ // carries our reference for the direct-adapter path; `customerEmail` pre-fills when neither is known.
141
+ customerEmail: input.customerId ? void 0 : input.email,
142
+ customerId: input.customerId,
143
+ externalCustomerId: input.referenceId,
137
144
  // Pin the framework-controlled `referenceId` LAST so caller metadata can never override it.
138
145
  metadata: { ...input.metadata, referenceId: input.referenceId },
139
146
  products: [input.priceId],
147
+ // Polar shows a back button to this URL; map the caller's cancel URL onto it.
148
+ returnUrl: input.cancelUrl,
140
149
  successUrl: input.successUrl
141
150
  });
142
151
  return { id: checkout.id, provider: "polar", url: checkout.url };
@@ -147,9 +156,10 @@ const createPolarAdapter = (options) => {
147
156
  },
148
157
  getOrCreateCustomer: async (ref) => {
149
158
  const customer = await client.customers.create({
150
- email: ref.email,
159
+ email: ref.email ?? "",
151
160
  externalId: ref.referenceId,
152
- metadata: { ...ref.metadata, referenceId: ref.referenceId }
161
+ metadata: { ...ref.metadata, referenceId: ref.referenceId },
162
+ type: "individual"
153
163
  });
154
164
  return { createdAt: Date.now(), email: customer.email ?? void 0, id: customer.id, provider: "polar", referenceId: ref.referenceId };
155
165
  },
@@ -170,12 +180,16 @@ const createPolarAdapter = (options) => {
170
180
  return mapEvent(webhookId, readString(event, "type") ?? "", asRecord(event.data));
171
181
  },
172
182
  refundPayment: async (input) => {
183
+ const order = input.amount ? void 0 : await client.orders.get({ id: input.sessionId });
184
+ const currency = input.amount?.currency ?? order?.currency ?? "usd";
185
+ const amountMinor = input.amount ? Number(input.amount.minorUnits) : order?.totalAmount ?? 0;
173
186
  await client.refunds.create({
174
- amount: input.amount ? Number(input.amount.minorUnits) : void 0,
187
+ amount: amountMinor,
175
188
  orderId: input.sessionId,
189
+ // `reason` is an open enum; a caller-supplied string is cast onto it (defaults to customer_request).
176
190
  reason: input.reason ?? "customer_request"
177
191
  });
178
- const refundedAmount = input.amount ?? money(0, "usd");
192
+ const refundedAmount = input.amount ?? money(BigInt(Math.round(amountMinor)), currency);
179
193
  return {
180
194
  amount: refundedAmount,
181
195
  capturedAmount: refundedAmount,
@@ -195,7 +209,7 @@ const createPolarAdapter = (options) => {
195
209
  externalCustomerId: input.referenceId,
196
210
  metadata: { value: input.quantity },
197
211
  name: input.featureId,
198
- timestamp: input.timestamp === void 0 ? void 0 : new Date(input.timestamp).toISOString()
212
+ timestamp: input.timestamp === void 0 ? void 0 : new Date(input.timestamp)
199
213
  }
200
214
  ]
201
215
  });
@@ -0,0 +1,32 @@
1
+ import { P as PaymentAdapter } from "../packem_shared/adapter.d-CMjjkTGB.mjs";
2
+ /**
3
+ * The Stripe SDK surface the adapter uses, as a structural type — a real `Stripe` instance satisfies
4
+ * it without a cast. Resources are `unknown` here (the adapter re-types the client as the real
5
+ * `Stripe` internally); this keeps the SDK's full type out of the published declaration files.
6
+ * @experimental
7
+ */
8
+ interface StripeClientLike {
9
+ readonly billing: unknown;
10
+ readonly billingPortal: unknown;
11
+ readonly checkout: unknown;
12
+ readonly customers: unknown;
13
+ readonly paymentIntents: unknown;
14
+ readonly refunds: unknown;
15
+ readonly subscriptions: unknown;
16
+ readonly webhooks: unknown;
17
+ }
18
+ /**
19
+ * `StripeAdapterOptions` is part of the experimental `@lunora/payment` API and may change without a major version bump.
20
+ * @experimental
21
+ */
22
+ interface StripeAdapterOptions {
23
+ readonly client: StripeClientLike;
24
+ readonly webhookSecret: string;
25
+ readonly webhookToleranceSeconds?: number;
26
+ }
27
+ /**
28
+ * `createStripeAdapter` is part of the experimental `@lunora/payment` API and may change without a major version bump.
29
+ * @experimental
30
+ */
31
+ declare const createStripeAdapter: (options: StripeAdapterOptions) => PaymentAdapter;
32
+ export { type StripeAdapterOptions, type StripeClientLike, createStripeAdapter };
@@ -0,0 +1,32 @@
1
+ import { P as PaymentAdapter } from "../packem_shared/adapter.d-CMjjkTGB.js";
2
+ /**
3
+ * The Stripe SDK surface the adapter uses, as a structural type — a real `Stripe` instance satisfies
4
+ * it without a cast. Resources are `unknown` here (the adapter re-types the client as the real
5
+ * `Stripe` internally); this keeps the SDK's full type out of the published declaration files.
6
+ * @experimental
7
+ */
8
+ interface StripeClientLike {
9
+ readonly billing: unknown;
10
+ readonly billingPortal: unknown;
11
+ readonly checkout: unknown;
12
+ readonly customers: unknown;
13
+ readonly paymentIntents: unknown;
14
+ readonly refunds: unknown;
15
+ readonly subscriptions: unknown;
16
+ readonly webhooks: unknown;
17
+ }
18
+ /**
19
+ * `StripeAdapterOptions` is part of the experimental `@lunora/payment` API and may change without a major version bump.
20
+ * @experimental
21
+ */
22
+ interface StripeAdapterOptions {
23
+ readonly client: StripeClientLike;
24
+ readonly webhookSecret: string;
25
+ readonly webhookToleranceSeconds?: number;
26
+ }
27
+ /**
28
+ * `createStripeAdapter` is part of the experimental `@lunora/payment` API and may change without a major version bump.
29
+ * @experimental
30
+ */
31
+ declare const createStripeAdapter: (options: StripeAdapterOptions) => PaymentAdapter;
32
+ export { type StripeAdapterOptions, type StripeClientLike, createStripeAdapter };