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

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 (44) hide show
  1. package/dist/index.mjs +1 -14
  2. package/dist/packem_shared/LunoraPaymentError-DWQ-v_NC.mjs +1 -0
  3. package/dist/packem_shared/MemoryPaymentStore-BICI1RdZ.mjs +1 -0
  4. package/dist/packem_shared/PAYMENT_TERMINAL_STATES-DVNbZxBy.mjs +1 -0
  5. package/dist/packem_shared/addMoney-BiiMn67T.mjs +1 -0
  6. package/dist/packem_shared/applyWebhookAction-BMVcvJ3t.mjs +1 -0
  7. package/dist/packem_shared/constantTimeEqual-BpVBvmN-.mjs +1 -0
  8. package/dist/packem_shared/createAdapterRegistry-CNIpj46I.mjs +1 -0
  9. package/dist/packem_shared/createDatabasePaymentStore-IG8Cb3_L.mjs +1 -0
  10. package/dist/packem_shared/createPayment-CW_iR6Hg.mjs +1 -0
  11. package/dist/packem_shared/entitlementsForReference-BbEOKu5d.mjs +1 -0
  12. package/dist/packem_shared/idempotencyKey-CfJKqcE3.mjs +1 -0
  13. package/dist/packem_shared/json-D85Nbd92.mjs +1 -0
  14. package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-BFBlKLG3.mjs +1 -0
  15. package/dist/packem_shared/not-supported-DwGy4SQL.mjs +1 -0
  16. package/dist/packem_shared/observability-C9tz8mj_.mjs +1 -0
  17. package/dist/packem_shared/paymentTables-BMtUrXkM.mjs +1 -0
  18. package/dist/packem_shared/reconcile-DDdfivoy.mjs +1 -0
  19. package/dist/packem_shared/subscription-event-C-71ye9J.mjs +1 -0
  20. package/dist/providers/autumn-features.mjs +1 -95
  21. package/dist/providers/autumn.mjs +1 -275
  22. package/dist/providers/creem.mjs +1 -236
  23. package/dist/providers/dodopayments.mjs +1 -259
  24. package/dist/providers/polar.mjs +1 -231
  25. package/dist/providers/stripe.mjs +1 -271
  26. package/package.json +4 -4
  27. package/dist/packem_shared/LunoraPaymentError-BeQlhkZj.mjs +0 -19
  28. package/dist/packem_shared/MemoryPaymentStore-DvgdWa3C.mjs +0 -72
  29. package/dist/packem_shared/PAYMENT_TERMINAL_STATES-DrxV0clv.mjs +0 -26
  30. package/dist/packem_shared/addMoney-jSh_7TfF.mjs +0 -60
  31. package/dist/packem_shared/applyWebhookAction-CLAx4svt.mjs +0 -177
  32. package/dist/packem_shared/constantTimeEqual-BQjoW85H.mjs +0 -71
  33. package/dist/packem_shared/createAdapterRegistry-Ds7bx_TK.mjs +0 -24
  34. package/dist/packem_shared/createDatabasePaymentStore-C0NUCj1H.mjs +0 -172
  35. package/dist/packem_shared/createPayment-CKQAznOL.mjs +0 -200
  36. package/dist/packem_shared/entitlementsForReference-CzZGXPoZ.mjs +0 -51
  37. package/dist/packem_shared/idempotencyKey-BzaHGdHD.mjs +0 -9
  38. package/dist/packem_shared/json-D0Cb5aMv.mjs +0 -29
  39. package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-DTsgPvfO.mjs +0 -29
  40. package/dist/packem_shared/not-supported-C0onRyia.mjs +0 -7
  41. package/dist/packem_shared/observability-CvhJ205g.mjs +0 -11
  42. package/dist/packem_shared/paymentTables-D4TbhaIk.mjs +0 -59
  43. package/dist/packem_shared/reconcile-qE3ERi3D.mjs +0 -85
  44. package/dist/packem_shared/subscription-event-CwEsWRCK.mjs +0 -17
@@ -1,236 +1 @@
1
- import { a as asRecord, c as readAny, r as readString, f as referenceFromMetadata, b as readNumber, p as parseTimestamp, e as readBoolean, d as readAnyNumber } from '../packem_shared/json-D0Cb5aMv.mjs';
2
- import { money, zeroMoney } from '../packem_shared/addMoney-jSh_7TfF.mjs';
3
- import { verifyCreemSignature } 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';
6
-
7
- const ALREADY_EXISTS_PATTERN = /already exists/iu;
8
- const PAYMENT_STATE_BY_CREEM_STATUS = {
9
- canceled: "canceled",
10
- completed: "captured",
11
- expired: "canceled",
12
- paid: "captured",
13
- partially_refunded: "partially_refunded",
14
- pending: "initiated",
15
- refunded: "refunded"
16
- };
17
- const SUBSCRIPTION_STATE_BY_CREEM_STATUS = {
18
- active: "active",
19
- canceled: "canceled",
20
- cancelled: "canceled",
21
- expired: "canceled",
22
- // SECURITY: `incomplete`/`unpaid`/`past_due` (first or a renewal payment not settled) must not map
23
- // to an entitling state — see the equivalent `incomplete` note in the Stripe/Polar adapters. A
24
- // `scheduled_cancel` subscription is still active until period end, so it entitles (the pending
25
- // cancellation surfaces via `cancelAtPeriodEnd`).
26
- incomplete: "past_due",
27
- paid: "active",
28
- past_due: "past_due",
29
- paused: "paused",
30
- scheduled_cancel: "active",
31
- trialing: "trialing",
32
- unpaid: "past_due"
33
- };
34
- const notSupported = makeNotSupported("creem (merchant-of-record)");
35
- const idOf = (value) => typeof value === "string" ? value : readString(asRecord(value), "id");
36
- const readCheckoutUrl = (checkout) => readAny(checkout, "checkout_url", "checkoutUrl") ?? "";
37
- const isCanceling = (subscription) => readAny(subscription, "canceled_at", "canceledAt") !== void 0 || readString(subscription, "status") === "scheduled_cancel";
38
- const isDuplicateCustomerError = (error) => {
39
- if (!(error instanceof Error)) {
40
- return false;
41
- }
42
- const { statusCode } = error;
43
- if (typeof statusCode === "number" && statusCode !== 400 && statusCode !== 409) {
44
- return false;
45
- }
46
- return ALREADY_EXISTS_PATTERN.test(error.message);
47
- };
48
- const subscriptionFromCreem = (input) => {
49
- const subscription = asRecord(input);
50
- const now = Date.now();
51
- const status = readString(subscription, "status") ?? "";
52
- return {
53
- cancelAtPeriodEnd: isCanceling(subscription),
54
- createdAt: now,
55
- currentPeriodEnd: parseTimestamp(readAny(subscription, "current_period_end_date", "currentPeriodEndDate")),
56
- currentPeriodStart: parseTimestamp(readAny(subscription, "current_period_start_date", "currentPeriodStartDate")),
57
- id: readString(subscription, "id") ?? "",
58
- priceId: idOf(subscription.product) ?? "",
59
- provider: "creem",
60
- quantity: readNumber(subscription, "units") ?? 1,
61
- referenceId: referenceFromMetadata(subscription) ?? idOf(subscription.customer) ?? "",
62
- // Fail closed: an unrecognized Creem status is treated as non-entitling `past_due`.
63
- state: SUBSCRIPTION_STATE_BY_CREEM_STATUS[status] ?? "past_due",
64
- updatedAt: now
65
- };
66
- };
67
- const checkoutToSession = (input) => {
68
- const checkout = asRecord(input);
69
- const now = Date.now();
70
- const order = asRecord(checkout.order);
71
- const currency = readString(order, "currency") ?? readString(checkout, "currency") ?? "usd";
72
- const amount = money(BigInt(Math.round(readNumber(order, "amount") ?? readNumber(checkout, "amount") ?? 0)), currency);
73
- const state = PAYMENT_STATE_BY_CREEM_STATUS[readString(order, "status") ?? readString(checkout, "status") ?? ""] ?? "initiated";
74
- const settled = state === "captured" || state === "partially_refunded" || state === "refunded";
75
- return {
76
- amount,
77
- capturedAmount: settled ? amount : zeroMoney(currency),
78
- createdAt: now,
79
- id: readString(checkout, "id") ?? "",
80
- provider: "creem",
81
- referenceId: referenceFromMetadata(checkout) ?? "",
82
- refundedAmount: state === "refunded" ? amount : zeroMoney(currency),
83
- state,
84
- updatedAt: now
85
- };
86
- };
87
- const mapEvent = (eventId, eventType, object) => {
88
- const base = { eventId, provider: "creem", raw: { object, type: eventType } };
89
- const order = asRecord(object.order);
90
- const currency = readString(order, "currency") ?? readString(object, "currency") ?? "usd";
91
- switch (eventType) {
92
- case "checkout.completed": {
93
- const amount = readNumber(order, "amount") ?? readNumber(object, "amount");
94
- return {
95
- ...base,
96
- // Round before BigInt: Creem documents integer minor units, but a stray fractional amount
97
- // would throw a RangeError out of `parseWebhook` (a 400 → provider retry loop). Match Autumn.
98
- amount: amount === void 0 ? void 0 : money(BigInt(Math.round(amount)), currency),
99
- customerId: idOf(object.customer),
100
- referenceId: referenceFromMetadata(object),
101
- sessionId: readString(object, "id"),
102
- subscriptionId: idOf(object.subscription),
103
- type: "payment.captured"
104
- };
105
- }
106
- case "refund.created": {
107
- const amount = readAnyNumber(object, "refund_amount", "refundAmount", "amount") ?? readNumber(order, "amount");
108
- const refundCurrency = readAny(object, "refund_currency", "refundCurrency") ?? currency;
109
- return {
110
- ...base,
111
- amount: amount === void 0 ? void 0 : money(BigInt(Math.round(amount)), refundCurrency),
112
- referenceId: referenceFromMetadata(object),
113
- sessionId: idOf(object.transaction) ?? idOf(object.subscription) ?? idOf(object.order) ?? idOf(object.checkout) ?? readString(object, "id"),
114
- type: "payment.refunded"
115
- };
116
- }
117
- case "subscription.active":
118
- case "subscription.canceled":
119
- case "subscription.expired":
120
- case "subscription.paid":
121
- case "subscription.past_due":
122
- case "subscription.paused":
123
- case "subscription.scheduled_cancel":
124
- case "subscription.trialing":
125
- case "subscription.unpaid":
126
- case "subscription.update": {
127
- const status = eventType === "subscription.scheduled_cancel" ? "scheduled_cancel" : readString(object, "status");
128
- return {
129
- ...base,
130
- cancelAtPeriodEnd: readBoolean(object, "cancel_at_period_end") ?? isCanceling(object),
131
- currentPeriodEnd: parseTimestamp(readAny(object, "current_period_end_date", "currentPeriodEndDate")),
132
- currentPeriodStart: parseTimestamp(readAny(object, "current_period_start_date", "currentPeriodStartDate")),
133
- customerId: idOf(object.customer),
134
- priceId: idOf(object.product),
135
- referenceId: referenceFromMetadata(object) ?? idOf(object.customer),
136
- subscriptionId: readString(object, "id"),
137
- type: stateToEventType(SUBSCRIPTION_STATE_BY_CREEM_STATUS[status ?? ""])
138
- };
139
- }
140
- default: {
141
- return { ...base, type: "unhandled" };
142
- }
143
- }
144
- };
145
- const createCreemAdapter = (options) => {
146
- const { webhookSecret } = options;
147
- const client = options.client;
148
- return {
149
- // Creem is a Merchant-of-Record: it moves the money, so there is no manual payment-intent flow.
150
- cancelPayment: () => notSupported("manual payment cancellation"),
151
- cancelSubscription: async (subscriptionId, cancelOptions) => (
152
- // Creem supports both immediate and period-end cancellation via `mode`; omitting it defers
153
- // to the store's configured default, so pass it explicitly. Default (no `atPeriodEnd`) is
154
- // immediate, matching the other adapters. Creem reports the resulting state, which we return.
155
- subscriptionFromCreem(await client.subscriptions.cancel(subscriptionId, { mode: cancelOptions?.atPeriodEnd === true ? "scheduled" : "immediate" }))
156
- ),
157
- capabilities: { merchantOfRecord: true, portal: true, usageMetering: false },
158
- capturePayment: (_input) => notSupported("manual capture"),
159
- createCheckout: async (input) => {
160
- const checkout = await client.checkouts.create({
161
- customer: input.customerId ? { id: input.customerId } : void 0,
162
- // Pin the framework-controlled `referenceId` LAST so caller metadata can never override it.
163
- metadata: { ...input.metadata, referenceId: input.referenceId },
164
- productId: input.priceId,
165
- requestId: input.idempotencyKey,
166
- successUrl: input.successUrl,
167
- units: input.quantity
168
- });
169
- return { id: readString(checkout, "id") ?? "", provider: "creem", url: readCheckoutUrl(checkout) };
170
- },
171
- createPortalSession: async (input) => {
172
- const link = await client.customers.generateBillingLinks({ customerId: input.customerId });
173
- return { url: readAny(link, "customer_portal_link", "customerPortalLink") ?? "" };
174
- },
175
- getOrCreateCustomer: async (ref) => {
176
- const toCustomer = (record) => {
177
- return {
178
- createdAt: Date.now(),
179
- email: readString(record, "email") ?? ref.email,
180
- id: readString(record, "id") ?? "",
181
- provider: "creem",
182
- referenceId: ref.referenceId
183
- };
184
- };
185
- try {
186
- return toCustomer(
187
- asRecord(
188
- await client.customers.create({
189
- email: ref.email ?? "",
190
- // Pin the framework-controlled `referenceId` LAST so caller metadata can never
191
- // override it (same pattern as `createCheckout`) — this is the only thing Creem
192
- // lets us key a customer on (no `externalId`-style create, unlike Polar/Dodo), so
193
- // the recovery path below can verify it before ever adopting a retrieved customer.
194
- metadata: { ...ref.metadata, referenceId: ref.referenceId },
195
- name: ref.metadata?.name ?? ref.referenceId
196
- })
197
- )
198
- );
199
- } catch (error) {
200
- if (ref.email !== void 0 && isDuplicateCustomerError(error)) {
201
- const existing = asRecord(await client.customers.retrieve(void 0, ref.email));
202
- const existingReferenceId = referenceFromMetadata(existing);
203
- if (existingReferenceId !== ref.referenceId) {
204
- throw new Error(
205
- `Creem customer for email "${ref.email}" already belongs to a different reference; refusing to bind it to "${ref.referenceId}".`,
206
- { cause: error }
207
- );
208
- }
209
- return toCustomer(existing);
210
- }
211
- throw error;
212
- }
213
- },
214
- getPaymentStatus: async (sessionId) => checkoutToSession(await client.checkouts.retrieve(sessionId)),
215
- getSubscriptionStatus: async (subscriptionId) => subscriptionFromCreem(await client.subscriptions.get(subscriptionId)),
216
- identifier: "creem",
217
- parseWebhook: async ({ headers, payload }) => {
218
- await verifyCreemSignature({ payload, secret: webhookSecret, signature: headers.get("creem-signature") ?? "" });
219
- const event = asRecord(JSON.parse(payload));
220
- return mapEvent(readString(event, "id") ?? "", readAny(event, "eventType", "type") ?? "", asRecord(event.object));
221
- },
222
- // Creem refunds are issued from the dashboard; there is no SDK endpoint to initiate one.
223
- refundPayment: () => notSupported("programmatic refunds"),
224
- resumeSubscription: async (subscriptionId) => subscriptionFromCreem(await client.subscriptions.resume(subscriptionId)),
225
- updateSubscription: async (subscriptionId, patch) => {
226
- if (patch.priceId) {
227
- return subscriptionFromCreem(
228
- await client.subscriptions.upgrade(subscriptionId, { productId: patch.priceId, updateBehavior: "proration-charge-immediately" })
229
- );
230
- }
231
- return subscriptionFromCreem(await client.subscriptions.get(subscriptionId));
232
- }
233
- };
234
- };
235
-
236
- export { createCreemAdapter };
1
+ import{a as u,c as o,r as n,f as p,b as m,p as f,e as v,d as w}from"../packem_shared/json-D85Nbd92.mjs";import{money as I,zeroMoney as b}from"../packem_shared/addMoney-BiiMn67T.mjs";import{verifyCreemSignature as P}from"../packem_shared/constantTimeEqual-BpVBvmN-.mjs";import{e as S}from"../packem_shared/not-supported-DwGy4SQL.mjs";import{i as A}from"../packem_shared/subscription-event-C-71ye9J.mjs";const k=/already exists/iu,E={canceled:"canceled",completed:"captured",expired:"canceled",paid:"captured",partially_refunded:"partially_refunded",pending:"initiated",refunded:"refunded"},_={active:"active",canceled:"canceled",cancelled:"canceled",expired:"canceled",incomplete:"past_due",paid:"active",past_due:"past_due",paused:"paused",scheduled_cancel:"active",trialing:"trialing",unpaid:"past_due"},y=S("creem (merchant-of-record)"),s=c=>typeof c=="string"?c:n(u(c),"id"),C=c=>o(c,"checkout_url","checkoutUrl")??"",g=c=>o(c,"canceled_at","canceledAt")!==void 0||n(c,"status")==="scheduled_cancel",D=c=>{if(!(c instanceof Error))return!1;const{statusCode:t}=c;return typeof t=="number"&&t!==400&&t!==409?!1:k.test(c.message)},l=c=>{const t=u(c),r=Date.now(),e=n(t,"status")??"";return{cancelAtPeriodEnd:g(t),createdAt:r,currentPeriodEnd:f(o(t,"current_period_end_date","currentPeriodEndDate")),currentPeriodStart:f(o(t,"current_period_start_date","currentPeriodStartDate")),id:n(t,"id")??"",priceId:s(t.product)??"",provider:"creem",quantity:m(t,"units")??1,referenceId:p(t)??s(t.customer)??"",state:_[e]??"past_due",updatedAt:r}},x=c=>{const t=u(c),r=Date.now(),e=u(t.order),a=n(e,"currency")??n(t,"currency")??"usd",d=I(BigInt(Math.round(m(e,"amount")??m(t,"amount")??0)),a),i=E[n(e,"status")??n(t,"status")??""]??"initiated";return{amount:d,capturedAmount:i==="captured"||i==="partially_refunded"||i==="refunded"?d:b(a),createdAt:r,id:n(t,"id")??"",provider:"creem",referenceId:p(t)??"",refundedAmount:i==="refunded"?d:b(a),state:i,updatedAt:r}},B=(c,t,r)=>{const e={eventId:c,provider:"creem",raw:{object:r,type:t}},a=u(r.order),d=n(a,"currency")??n(r,"currency")??"usd";switch(t){case"checkout.completed":{const i=m(a,"amount")??m(r,"amount");return{...e,amount:i===void 0?void 0:I(BigInt(Math.round(i)),d),customerId:s(r.customer),referenceId:p(r),sessionId:n(r,"id"),subscriptionId:s(r.subscription),type:"payment.captured"}}case"refund.created":{const i=w(r,"refund_amount","refundAmount","amount")??m(a,"amount"),h=o(r,"refund_currency","refundCurrency")??d;return{...e,amount:i===void 0?void 0:I(BigInt(Math.round(i)),h),referenceId:p(r),sessionId:s(r.transaction)??s(r.subscription)??s(r.order)??s(r.checkout)??n(r,"id"),type:"payment.refunded"}}case"subscription.active":case"subscription.canceled":case"subscription.expired":case"subscription.paid":case"subscription.past_due":case"subscription.paused":case"subscription.scheduled_cancel":case"subscription.trialing":case"subscription.unpaid":case"subscription.update":{const i=t==="subscription.scheduled_cancel"?"scheduled_cancel":n(r,"status");return{...e,cancelAtPeriodEnd:v(r,"cancel_at_period_end")??g(r),currentPeriodEnd:f(o(r,"current_period_end_date","currentPeriodEndDate")),currentPeriodStart:f(o(r,"current_period_start_date","currentPeriodStartDate")),customerId:s(r.customer),priceId:s(r.product),referenceId:p(r)??s(r.customer),subscriptionId:n(r,"id"),type:A(_[i??""])}}default:return{...e,type:"unhandled"}}},T=c=>{const{webhookSecret:t}=c,r=c.client;return{cancelPayment:()=>y("manual payment cancellation"),cancelSubscription:async(e,a)=>l(await r.subscriptions.cancel(e,{mode:a?.atPeriodEnd===!0?"scheduled":"immediate"})),capabilities:{merchantOfRecord:!0,portal:!0,usageMetering:!1},capturePayment:e=>y("manual capture"),createCheckout:async e=>{const a=await r.checkouts.create({customer:e.customerId?{id:e.customerId}:void 0,metadata:{...e.metadata,referenceId:e.referenceId},productId:e.priceId,requestId:e.idempotencyKey,successUrl:e.successUrl,units:e.quantity});return{id:n(a,"id")??"",provider:"creem",url:C(a)}},createPortalSession:async e=>{const a=await r.customers.generateBillingLinks({customerId:e.customerId});return{url:o(a,"customer_portal_link","customerPortalLink")??""}},getOrCreateCustomer:async e=>{const a=d=>({createdAt:Date.now(),email:n(d,"email")??e.email,id:n(d,"id")??"",provider:"creem",referenceId:e.referenceId});try{return a(u(await r.customers.create({email:e.email??"",metadata:{...e.metadata,referenceId:e.referenceId},name:e.metadata?.name??e.referenceId})))}catch(d){if(e.email!==void 0&&D(d)){const i=u(await r.customers.retrieve(void 0,e.email));if(p(i)!==e.referenceId)throw new Error(`Creem customer for email "${e.email}" already belongs to a different reference; refusing to bind it to "${e.referenceId}".`,{cause:d});return a(i)}throw d}},getPaymentStatus:async e=>x(await r.checkouts.retrieve(e)),getSubscriptionStatus:async e=>l(await r.subscriptions.get(e)),identifier:"creem",parseWebhook:async({headers:e,payload:a})=>{await P({payload:a,secret:t,signature:e.get("creem-signature")??""});const d=u(JSON.parse(a));return B(n(d,"id")??"",o(d,"eventType","type")??"",u(d.object))},refundPayment:()=>y("programmatic refunds"),resumeSubscription:async e=>l(await r.subscriptions.resume(e)),updateSubscription:async(e,a)=>a.priceId?l(await r.subscriptions.upgrade(e,{productId:a.priceId,updateBehavior:"proration-charge-immediately"})):l(await r.subscriptions.get(e))}};export{T as createCreemAdapter};
@@ -1,259 +1 @@
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 };
1
+ import{LunoraPaymentError as g}from"../packem_shared/LunoraPaymentError-DWQ-v_NC.mjs";import{idempotencyKey as v}from"../packem_shared/idempotencyKey-CfJKqcE3.mjs";import{a as o,b as s,r,f as u,p as l,e as I}from"../packem_shared/json-D85Nbd92.mjs";import{money as y,zeroMoney as f}from"../packem_shared/addMoney-BiiMn67T.mjs";import{verifyStandardWebhook as w}from"../packem_shared/constantTimeEqual-BpVBvmN-.mjs";import{e as S}from"../packem_shared/not-supported-DwGy4SQL.mjs";import{i as q}from"../packem_shared/subscription-event-C-71ye9J.mjs";const P={cancelled:"canceled",failed:"failed",partially_captured:"captured",partially_captured_and_capturable:"captured",processing:"initiated",requires_capture:"authorized",requires_confirmation:"initiated",requires_customer_action:"authorized",requires_merchant_action:"authorized",requires_payment_method:"initiated",succeeded:"captured"},h={active:"active",cancelled:"canceled",expired:"canceled",failed:"past_due",on_hold:"past_due",paused:"paused",pending:"past_due"},b=S("dodopayments (merchant-of-record)"),m=d=>r(o(d.customer),"customer_id")??r(d,"customer_id"),p=d=>{const n=o(d),t=Date.now(),e=r(n,"status")??"";return{cancelAtPeriodEnd:I(n,"cancel_at_next_billing_date")??!1,createdAt:t,currentPeriodEnd:l(r(n,"next_billing_date")),currentPeriodStart:l(r(n,"previous_billing_date")),id:r(n,"subscription_id")??"",priceId:r(n,"product_id")??"",provider:"dodopayments",quantity:s(n,"quantity")??1,referenceId:u(n)??m(n)??"",state:h[e]??"past_due",updatedAt:t}},A=d=>{const n=o(d),t=Date.now(),e=r(n,"currency")??"usd",a=y(BigInt(Math.round(s(n,"total_amount")??0)),e),i=P[r(n,"status")??""]??"initiated";return{amount:a,capturedAmount:i==="captured"?a:f(e),createdAt:t,id:r(n,"payment_id")??"",provider:"dodopayments",referenceId:u(n)??"",refundedAmount:f(e),state:i,updatedAt:t}},k=(d,n,t)=>{const e={eventId:d,provider:"dodopayments",raw:{object:t,type:n}},a=r(t,"currency")??"usd";switch(n){case"dispute.lost":case"refund.succeeded":return{...e,amount:y(BigInt(Math.round(s(t,"amount")??0)),a),referenceId:u(t),sessionId:r(t,"payment_id"),type:"payment.refunded"};case"payment.cancelled":case"payment.failed":return{...e,referenceId:u(t),sessionId:r(t,"payment_id"),type:"payment.failed"};case"payment.succeeded":return{...e,amount:y(BigInt(Math.round(s(t,"total_amount")??0)),a),customerId:m(t),referenceId:u(t),sessionId:r(t,"payment_id"),subscriptionId:r(t,"subscription_id"),type:"payment.captured"};case"subscription.active":case"subscription.cancelled":case"subscription.expired":case"subscription.failed":case"subscription.on_hold":case"subscription.paused":case"subscription.plan_changed":case"subscription.renewed":case"subscription.updated":{const i=r(t,"status");return{...e,cancelAtPeriodEnd:I(t,"cancel_at_next_billing_date"),currentPeriodEnd:l(r(t,"next_billing_date")),currentPeriodStart:l(r(t,"previous_billing_date")),customerId:m(t),priceId:r(t,"product_id"),quantity:s(t,"quantity"),referenceId:u(t)??m(t),subscriptionId:r(t,"subscription_id"),type:q(h[i??""])}}default:return{...e,type:"unhandled"}}},z=d=>{const{webhookSecret:n}=d,t=d.client;return{cancelPayment:()=>b("manual payment cancellation"),cancelSubscription:async(e,a)=>{const i=a?.atPeriodEnd?{cancel_at_next_billing_date:!0}:{status:"cancelled"};return p(await t.subscriptions.update(e,i))},capabilities:{merchantOfRecord:!0,portal:!0,usageMetering:!0},capturePayment:e=>b("manual capture"),createCheckout:async e=>{const a=o(await t.checkoutSessions.create({customer:e.customerId?{customer_id:e.customerId}:void 0,metadata:{...e.metadata,referenceId:e.referenceId},product_cart:[{product_id:e.priceId,quantity:e.quantity??1}],return_url:e.successUrl}));return{id:r(a,"session_id")??"",provider:"dodopayments",url:r(a,"checkout_url")??""}},createPortalSession:async e=>{const a=o(await t.customers.customerPortal.create(e.customerId,{return_url:e.returnUrl}));return{url:r(a,"link")??r(a,"url")??""}},getOrCreateCustomer:async e=>{const a=o(await t.customers.create({email:e.email??"",name:e.metadata?.name??e.referenceId},{idempotencyKey:v("customer","dodopayments",e.referenceId)}));return{createdAt:Date.now(),email:r(a,"email")??e.email,id:r(a,"customer_id")??"",provider:"dodopayments",referenceId:e.referenceId}},getPaymentStatus:async e=>A(await t.payments.retrieve(e)),getSubscriptionStatus:async e=>p(await t.subscriptions.retrieve(e)),identifier:"dodopayments",parseWebhook:async({headers:e,payload:a})=>{const i=e.get("webhook-id")??"";await w({payload:a,secret:n,toleranceSeconds:d.webhookToleranceSeconds,webhookId:i,webhookSignature:e.get("webhook-signature")??"",webhookTimestamp:e.get("webhook-timestamp")??""});const c=o(JSON.parse(a));return k(i,r(c,"type")??"",o(c.data))},refundPayment:async e=>{if(e.amount!==void 0)throw new g("PROVIDER_ERROR","dodopayments refunds a payment in full; partial refunds require line items and aren't supported here");const a=o(await t.refunds.create({payment_id:e.sessionId,reason:e.reason})),i=r(a,"currency")??"usd",c=y(BigInt(Math.round(s(a,"amount")??0)),i);let _="captured";return r(a,"status")==="succeeded"&&(_="refunded"),{amount:c,capturedAmount:c,createdAt:Date.now(),id:e.sessionId,provider:"dodopayments",referenceId:"",refundedAmount:c,state:_,updatedAt:Date.now()}},reportUsage:async e=>{await t.usageEvents.ingest({events:[{customer_id:e.customerId??e.referenceId,event_id:e.idempotencyKey,event_name:e.featureId,metadata:{value:e.quantity},timestamp:e.timestamp===void 0?void 0:new Date(e.timestamp).toISOString()}]})},resumeSubscription:async e=>{const a=await t.subscriptions.update(e,{cancel_at_next_billing_date:!1});return p(a)},updateSubscription:async(e,a)=>{if(a.priceId!==void 0||a.quantity!==void 0){const i=o(a.priceId!==void 0&&a.quantity!==void 0?void 0:await t.subscriptions.retrieve(e));await t.subscriptions.changePlan(e,{product_id:a.priceId??r(i,"product_id")??"",proration_billing_mode:"prorated_immediately",quantity:a.quantity??s(i,"quantity")??1})}return p(await t.subscriptions.retrieve(e))}}};export{z as createDodoPaymentsAdapter};