@lunora/payment 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +3 -2
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +20 -459
- package/dist/index.d.ts +20 -459
- package/dist/index.mjs +9 -11
- package/dist/packem_shared/{LunoraPaymentError-B3hEzXSs.mjs → LunoraPaymentError-BeQlhkZj.mjs} +4 -7
- package/dist/packem_shared/adapter.d-iFA55DzU.d.mts +312 -0
- package/dist/packem_shared/adapter.d-iFA55DzU.d.ts +312 -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-DzX-ji34.mjs} +17 -5
- package/dist/packem_shared/json-DhcPm8EO.mjs +11 -0
- package/dist/packem_shared/{lunoraDatabaseToPaymentDatabase-RlKX3Kcd.mjs → lunoraDatabaseToPaymentDatabase-CL2vdAXq.mjs} +2 -2
- package/dist/packem_shared/not-supported-C0onRyia.mjs +7 -0
- package/dist/packem_shared/{reconcile-CI1ukJF9.mjs → reconcile-Dtn_ErbJ.mjs} +1 -1
- package/dist/packem_shared/subscription-event-CwEsWRCK.mjs +17 -0
- package/dist/providers/autumn-features.d.mts +94 -0
- package/dist/providers/autumn-features.d.ts +94 -0
- package/dist/providers/autumn-features.mjs +95 -0
- package/dist/providers/autumn.d.mts +19 -0
- package/dist/providers/autumn.d.ts +19 -0
- package/dist/providers/autumn.mjs +294 -0
- package/dist/providers/creem.d.mts +17 -0
- package/dist/providers/creem.d.ts +17 -0
- package/dist/providers/creem.mjs +205 -0
- package/dist/providers/dodopayments.d.mts +21 -0
- package/dist/providers/dodopayments.d.ts +21 -0
- package/dist/providers/dodopayments.mjs +259 -0
- package/dist/providers/polar.d.mts +22 -0
- package/dist/providers/polar.d.ts +22 -0
- package/dist/{packem_shared/createPolarAdapter-BJtVGSlF.mjs → providers/polar.mjs} +51 -47
- package/dist/providers/stripe.d.mts +23 -0
- package/dist/providers/stripe.d.ts +23 -0
- package/dist/{packem_shared/createStripeAdapter-D40MVBXg.mjs → providers/stripe.mjs} +56 -55
- package/package.json +46 -5
- package/dist/packem_shared/json-Db337f36.mjs +0 -6
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { LunoraPaymentError } from '../packem_shared/LunoraPaymentError-BeQlhkZj.mjs';
|
|
2
|
+
import idempotencyKey from '../packem_shared/idempotencyKey-BFzDCA7g.mjs';
|
|
3
|
+
import { a as asRecord, b as readNumber, r as readString, d as referenceFromMetadata, p as parseTimestamp, c as readBoolean } from '../packem_shared/json-DhcPm8EO.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,22 @@
|
|
|
1
|
+
import { P as PaymentAdapter } from "../packem_shared/adapter.d-iFA55DzU.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
|
+
*/
|
|
7
|
+
interface PolarClientLike {
|
|
8
|
+
readonly checkouts: unknown;
|
|
9
|
+
readonly customers: unknown;
|
|
10
|
+
readonly customerSessions: unknown;
|
|
11
|
+
readonly events: unknown;
|
|
12
|
+
readonly orders: unknown;
|
|
13
|
+
readonly refunds: unknown;
|
|
14
|
+
readonly subscriptions: unknown;
|
|
15
|
+
}
|
|
16
|
+
interface PolarAdapterOptions {
|
|
17
|
+
readonly client: PolarClientLike;
|
|
18
|
+
readonly webhookSecret: string;
|
|
19
|
+
readonly webhookToleranceSeconds?: number;
|
|
20
|
+
}
|
|
21
|
+
declare const createPolarAdapter: (options: PolarAdapterOptions) => PaymentAdapter;
|
|
22
|
+
export { type PolarAdapterOptions, type PolarClientLike, createPolarAdapter };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { P as PaymentAdapter } from "../packem_shared/adapter.d-iFA55DzU.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
|
+
*/
|
|
7
|
+
interface PolarClientLike {
|
|
8
|
+
readonly checkouts: unknown;
|
|
9
|
+
readonly customers: unknown;
|
|
10
|
+
readonly customerSessions: unknown;
|
|
11
|
+
readonly events: unknown;
|
|
12
|
+
readonly orders: unknown;
|
|
13
|
+
readonly refunds: unknown;
|
|
14
|
+
readonly subscriptions: unknown;
|
|
15
|
+
}
|
|
16
|
+
interface PolarAdapterOptions {
|
|
17
|
+
readonly client: PolarClientLike;
|
|
18
|
+
readonly webhookSecret: string;
|
|
19
|
+
readonly webhookToleranceSeconds?: number;
|
|
20
|
+
}
|
|
21
|
+
declare const createPolarAdapter: (options: PolarAdapterOptions) => PaymentAdapter;
|
|
22
|
+
export { type PolarAdapterOptions, type PolarClientLike, createPolarAdapter };
|
|
@@ -1,57 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { a as asRecord, r as readString, d as referenceFromMetadata, c as readBoolean, p as parseTimestamp, b as readNumber } from '../packem_shared/json-DhcPm8EO.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
|
referenceId: "",
|
|
57
65
|
refundedAmount: state === "refunded" ? amount : zeroMoney(currency),
|
|
@@ -59,26 +67,15 @@ const orderToSession = (order) => {
|
|
|
59
67
|
updatedAt: now
|
|
60
68
|
};
|
|
61
69
|
};
|
|
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
70
|
const mapEvent = (eventId, eventType, object) => {
|
|
77
71
|
const base = { eventId, provider: "polar", raw: { object, type: eventType } };
|
|
78
72
|
const currency = readString(object, "currency") ?? "usd";
|
|
79
73
|
switch (eventType) {
|
|
80
74
|
case "order.created":
|
|
81
75
|
case "order.paid": {
|
|
76
|
+
if (eventType === "order.created" && PAYMENT_STATE_BY_POLAR_ORDER_STATUS[readString(object, "status") ?? ""] !== "captured") {
|
|
77
|
+
return { ...base, type: "unhandled" };
|
|
78
|
+
}
|
|
82
79
|
return {
|
|
83
80
|
...base,
|
|
84
81
|
amount: money(BigInt(readNumber(object, "total_amount") ?? readNumber(object, "amount") ?? 0), currency),
|
|
@@ -102,8 +99,9 @@ const mapEvent = (eventId, eventType, object) => {
|
|
|
102
99
|
case "subscription.canceled":
|
|
103
100
|
case "subscription.created":
|
|
104
101
|
case "subscription.revoked":
|
|
102
|
+
case "subscription.uncanceled":
|
|
105
103
|
case "subscription.updated": {
|
|
106
|
-
const type = eventType === "subscription.revoked" ? "subscription.canceled" :
|
|
104
|
+
const type = eventType === "subscription.revoked" ? "subscription.canceled" : stateToEventType(SUBSCRIPTION_STATE_BY_POLAR_STATUS[readString(object, "status") ?? ""]);
|
|
107
105
|
return {
|
|
108
106
|
...base,
|
|
109
107
|
cancelAtPeriodEnd: readBoolean(object, "cancel_at_period_end"),
|
|
@@ -122,7 +120,8 @@ const mapEvent = (eventId, eventType, object) => {
|
|
|
122
120
|
}
|
|
123
121
|
};
|
|
124
122
|
const createPolarAdapter = (options) => {
|
|
125
|
-
const {
|
|
123
|
+
const { webhookSecret } = options;
|
|
124
|
+
const client = options.client;
|
|
126
125
|
return {
|
|
127
126
|
cancelPayment: () => notSupported("manual payment cancellation"),
|
|
128
127
|
cancelSubscription: async (subscriptionId, cancelOptions) => {
|
|
@@ -147,9 +146,10 @@ const createPolarAdapter = (options) => {
|
|
|
147
146
|
},
|
|
148
147
|
getOrCreateCustomer: async (ref) => {
|
|
149
148
|
const customer = await client.customers.create({
|
|
150
|
-
email: ref.email,
|
|
149
|
+
email: ref.email ?? "",
|
|
151
150
|
externalId: ref.referenceId,
|
|
152
|
-
metadata: { ...ref.metadata, referenceId: ref.referenceId }
|
|
151
|
+
metadata: { ...ref.metadata, referenceId: ref.referenceId },
|
|
152
|
+
type: "individual"
|
|
153
153
|
});
|
|
154
154
|
return { createdAt: Date.now(), email: customer.email ?? void 0, id: customer.id, provider: "polar", referenceId: ref.referenceId };
|
|
155
155
|
},
|
|
@@ -170,12 +170,16 @@ const createPolarAdapter = (options) => {
|
|
|
170
170
|
return mapEvent(webhookId, readString(event, "type") ?? "", asRecord(event.data));
|
|
171
171
|
},
|
|
172
172
|
refundPayment: async (input) => {
|
|
173
|
+
const order = input.amount ? void 0 : await client.orders.get({ id: input.sessionId });
|
|
174
|
+
const currency = input.amount?.currency ?? order?.currency ?? "usd";
|
|
175
|
+
const amountMinor = input.amount ? Number(input.amount.minorUnits) : order?.totalAmount ?? 0;
|
|
173
176
|
await client.refunds.create({
|
|
174
|
-
amount:
|
|
177
|
+
amount: amountMinor,
|
|
175
178
|
orderId: input.sessionId,
|
|
179
|
+
// `reason` is an open enum; a caller-supplied string is cast onto it (defaults to customer_request).
|
|
176
180
|
reason: input.reason ?? "customer_request"
|
|
177
181
|
});
|
|
178
|
-
const refundedAmount = input.amount ?? money(
|
|
182
|
+
const refundedAmount = input.amount ?? money(BigInt(Math.round(amountMinor)), currency);
|
|
179
183
|
return {
|
|
180
184
|
amount: refundedAmount,
|
|
181
185
|
capturedAmount: refundedAmount,
|
|
@@ -195,7 +199,7 @@ const createPolarAdapter = (options) => {
|
|
|
195
199
|
externalCustomerId: input.referenceId,
|
|
196
200
|
metadata: { value: input.quantity },
|
|
197
201
|
name: input.featureId,
|
|
198
|
-
timestamp: input.timestamp === void 0 ? void 0 : new Date(input.timestamp)
|
|
202
|
+
timestamp: input.timestamp === void 0 ? void 0 : new Date(input.timestamp)
|
|
199
203
|
}
|
|
200
204
|
]
|
|
201
205
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { P as PaymentAdapter } from "../packem_shared/adapter.d-iFA55DzU.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
|
+
*/
|
|
7
|
+
interface StripeClientLike {
|
|
8
|
+
readonly billing: unknown;
|
|
9
|
+
readonly billingPortal: unknown;
|
|
10
|
+
readonly checkout: unknown;
|
|
11
|
+
readonly customers: unknown;
|
|
12
|
+
readonly paymentIntents: unknown;
|
|
13
|
+
readonly refunds: unknown;
|
|
14
|
+
readonly subscriptions: unknown;
|
|
15
|
+
readonly webhooks: unknown;
|
|
16
|
+
}
|
|
17
|
+
interface StripeAdapterOptions {
|
|
18
|
+
readonly client: StripeClientLike;
|
|
19
|
+
readonly webhookSecret: string;
|
|
20
|
+
readonly webhookToleranceSeconds?: number;
|
|
21
|
+
}
|
|
22
|
+
declare const createStripeAdapter: (options: StripeAdapterOptions) => PaymentAdapter;
|
|
23
|
+
export { type StripeAdapterOptions, type StripeClientLike, createStripeAdapter };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { P as PaymentAdapter } from "../packem_shared/adapter.d-iFA55DzU.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
|
+
*/
|
|
7
|
+
interface StripeClientLike {
|
|
8
|
+
readonly billing: unknown;
|
|
9
|
+
readonly billingPortal: unknown;
|
|
10
|
+
readonly checkout: unknown;
|
|
11
|
+
readonly customers: unknown;
|
|
12
|
+
readonly paymentIntents: unknown;
|
|
13
|
+
readonly refunds: unknown;
|
|
14
|
+
readonly subscriptions: unknown;
|
|
15
|
+
readonly webhooks: unknown;
|
|
16
|
+
}
|
|
17
|
+
interface StripeAdapterOptions {
|
|
18
|
+
readonly client: StripeClientLike;
|
|
19
|
+
readonly webhookSecret: string;
|
|
20
|
+
readonly webhookToleranceSeconds?: number;
|
|
21
|
+
}
|
|
22
|
+
declare const createStripeAdapter: (options: StripeAdapterOptions) => PaymentAdapter;
|
|
23
|
+
export { type StripeAdapterOptions, type StripeClientLike, createStripeAdapter };
|