@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.
- package/LICENSE.md +6 -0
- package/README.md +5 -2
- package/dist/index.d.mts +266 -523
- package/dist/index.d.ts +266 -523
- package/dist/index.mjs +11 -13
- package/dist/packem_shared/{LunoraPaymentError-B3hEzXSs.mjs → LunoraPaymentError-BeQlhkZj.mjs} +4 -7
- package/dist/packem_shared/adapter.d-CMjjkTGB.d.mts +427 -0
- package/dist/packem_shared/adapter.d-CMjjkTGB.d.ts +427 -0
- package/dist/packem_shared/{addMoney-bCcs1nyw.mjs → addMoney-jSh_7TfF.mjs} +1 -1
- package/dist/packem_shared/{applyWebhookAction-DpAqf3Lw.mjs → applyWebhookAction-CLAx4svt.mjs} +1 -1
- package/dist/packem_shared/{constantTimeEqual-CfY0jYcL.mjs → constantTimeEqual-BQjoW85H.mjs} +14 -38
- package/dist/packem_shared/{createAdapterRegistry-BuDHFCBc.mjs → createAdapterRegistry-Ds7bx_TK.mjs} +1 -1
- package/dist/packem_shared/{createDatabasePaymentStore-bYB_HUE6.mjs → createDatabasePaymentStore-C0NUCj1H.mjs} +1 -1
- package/dist/packem_shared/{createPayment-BccfPGyw.mjs → createPayment-CKQAznOL.mjs} +26 -16
- package/dist/packem_shared/idempotencyKey-BzaHGdHD.mjs +9 -0
- package/dist/packem_shared/json-D0Cb5aMv.mjs +29 -0
- package/dist/packem_shared/{lunoraDatabaseToPaymentDatabase-RlKX3Kcd.mjs → lunoraDatabaseToPaymentDatabase-DTsgPvfO.mjs} +2 -2
- package/dist/packem_shared/not-supported-C0onRyia.mjs +7 -0
- package/dist/packem_shared/paymentTables-D4TbhaIk.mjs +59 -0
- package/dist/packem_shared/{reconcile-CI1ukJF9.mjs → reconcile-qE3ERi3D.mjs} +14 -2
- package/dist/packem_shared/subscription-event-CwEsWRCK.mjs +17 -0
- package/dist/providers/autumn-features.d.mts +129 -0
- package/dist/providers/autumn-features.d.ts +129 -0
- package/dist/providers/autumn-features.mjs +95 -0
- package/dist/providers/autumn.d.mts +28 -0
- package/dist/providers/autumn.d.ts +28 -0
- package/dist/providers/autumn.mjs +275 -0
- package/dist/providers/creem.d.mts +26 -0
- package/dist/providers/creem.d.ts +26 -0
- package/dist/providers/creem.mjs +236 -0
- package/dist/providers/dodopayments.d.mts +30 -0
- package/dist/providers/dodopayments.d.ts +30 -0
- package/dist/providers/dodopayments.mjs +259 -0
- package/dist/providers/polar.d.mts +31 -0
- package/dist/providers/polar.d.ts +31 -0
- package/dist/{packem_shared/createPolarAdapter-BJtVGSlF.mjs → providers/polar.mjs} +63 -49
- package/dist/providers/stripe.d.mts +32 -0
- package/dist/providers/stripe.d.ts +32 -0
- package/dist/{packem_shared/createStripeAdapter-D40MVBXg.mjs → providers/stripe.mjs} +56 -55
- package/package.json +46 -5
- package/dist/packem_shared/idempotencyKey-BFzDCA7g.mjs +0 -3
- package/dist/packem_shared/json-Db337f36.mjs +0 -6
- 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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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 = (
|
|
22
|
-
|
|
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
|
|
38
|
+
cancelAtPeriodEnd: readBoolean(subscription, "cancelAtPeriodEnd") ?? false,
|
|
32
39
|
createdAt: now,
|
|
33
|
-
currentPeriodEnd:
|
|
34
|
-
currentPeriodStart:
|
|
35
|
-
id: subscription
|
|
36
|
-
priceId: subscription
|
|
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
|
|
40
|
-
state: SUBSCRIPTION_STATE_BY_POLAR_STATUS[subscription
|
|
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 = (
|
|
51
|
+
const orderToSession = (input) => {
|
|
52
|
+
const order = asRecord(input);
|
|
45
53
|
const now = Date.now();
|
|
46
|
-
const currency = order
|
|
47
|
-
const amount = money(BigInt(order
|
|
48
|
-
const state = PAYMENT_STATE_BY_POLAR_ORDER_STATUS[order
|
|
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
|
|
62
|
+
id: readString(order, "id") ?? "",
|
|
55
63
|
provider: "polar",
|
|
56
|
-
|
|
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" :
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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)
|
|
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 };
|