@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
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { LunoraPaymentError } from '../packem_shared/LunoraPaymentError-BeQlhkZj.mjs';
|
|
2
|
+
import idempotencyKey from '../packem_shared/idempotencyKey-BFzDCA7g.mjs';
|
|
3
|
+
import { r as readString, c as readBoolean, b as readNumber, a as asRecord } from '../packem_shared/json-DhcPm8EO.mjs';
|
|
4
|
+
import { compareMoney, money, zeroMoney } from '../packem_shared/addMoney-jSh_7TfF.mjs';
|
|
5
|
+
import { s as stateToEventType } from '../packem_shared/subscription-event-CwEsWRCK.mjs';
|
|
5
6
|
|
|
6
7
|
const PAYMENT_STATE_BY_STRIPE_STATUS = {
|
|
7
8
|
canceled: "canceled",
|
|
@@ -15,7 +16,14 @@ const PAYMENT_STATE_BY_STRIPE_STATUS = {
|
|
|
15
16
|
const SUBSCRIPTION_STATE_BY_STRIPE_STATUS = {
|
|
16
17
|
active: "active",
|
|
17
18
|
canceled: "canceled",
|
|
18
|
-
|
|
19
|
+
// SECURITY: `incomplete` means the FIRST payment has not succeeded (SCA
|
|
20
|
+
// `requires_action` or a failed initial charge). It must NOT map to an
|
|
21
|
+
// entitling state — Stripe's recommended `payment_behavior: "default_incomplete"`
|
|
22
|
+
// makes `incomplete` the initial status of every new subscription, so mapping
|
|
23
|
+
// it to `trialing` (which is in ACTIVE_STATES) would grant paid entitlements
|
|
24
|
+
// before any payment. Reserve `trialing` for a genuine Stripe trial (status
|
|
25
|
+
// `trialing`, mapped below); treat `incomplete` as non-entitling `past_due`.
|
|
26
|
+
incomplete: "past_due",
|
|
19
27
|
incomplete_expired: "canceled",
|
|
20
28
|
past_due: "past_due",
|
|
21
29
|
paused: "paused",
|
|
@@ -31,61 +39,49 @@ const firstItem = (object) => {
|
|
|
31
39
|
const firstPriceId = (object) => readString(asRecord(firstItem(object).price), "id");
|
|
32
40
|
const firstQuantity = (object) => readNumber(firstItem(object), "quantity");
|
|
33
41
|
const periodEndMs = (object) => {
|
|
34
|
-
const seconds = readNumber(object, "current_period_end");
|
|
42
|
+
const seconds = readNumber(firstItem(object), "current_period_end") ?? readNumber(object, "current_period_end");
|
|
35
43
|
return seconds === void 0 ? void 0 : seconds * 1e3;
|
|
36
44
|
};
|
|
37
45
|
const periodStartMs = (object) => {
|
|
38
|
-
const seconds = readNumber(object, "current_period_start");
|
|
46
|
+
const seconds = readNumber(firstItem(object), "current_period_start") ?? readNumber(object, "current_period_start");
|
|
39
47
|
return seconds === void 0 ? void 0 : seconds * 1e3;
|
|
40
48
|
};
|
|
41
|
-
const intentToSession = (
|
|
42
|
-
const
|
|
43
|
-
const
|
|
49
|
+
const intentToSession = (input) => {
|
|
50
|
+
const intent = asRecord(input);
|
|
51
|
+
const currency = readString(intent, "currency") ?? "usd";
|
|
52
|
+
const amountValue = readNumber(intent, "amount") ?? 0;
|
|
53
|
+
const amount = money(BigInt(Math.round(amountValue)), currency);
|
|
54
|
+
const state = PAYMENT_STATE_BY_STRIPE_STATUS[readString(intent, "status") ?? ""] ?? "initiated";
|
|
44
55
|
const now = Date.now();
|
|
45
56
|
return {
|
|
46
57
|
amount,
|
|
47
|
-
capturedAmount: state === "captured" ? money(BigInt(intent
|
|
58
|
+
capturedAmount: state === "captured" ? money(BigInt(Math.round(readNumber(intent, "amount_received") ?? amountValue)), currency) : zeroMoney(currency),
|
|
48
59
|
createdAt: now,
|
|
49
|
-
id: intent
|
|
60
|
+
id: readString(intent, "id") ?? "",
|
|
50
61
|
provider: "stripe",
|
|
51
|
-
referenceId: intent
|
|
52
|
-
refundedAmount: zeroMoney(
|
|
62
|
+
referenceId: readReferenceId(intent) ?? "",
|
|
63
|
+
refundedAmount: zeroMoney(currency),
|
|
53
64
|
state,
|
|
54
65
|
updatedAt: now
|
|
55
66
|
};
|
|
56
67
|
};
|
|
57
|
-
const subscriptionFromStripe = (
|
|
68
|
+
const subscriptionFromStripe = (input) => {
|
|
69
|
+
const subscription = asRecord(input);
|
|
58
70
|
const now = Date.now();
|
|
59
71
|
return {
|
|
60
|
-
cancelAtPeriodEnd: subscription
|
|
72
|
+
cancelAtPeriodEnd: readBoolean(subscription, "cancel_at_period_end") ?? false,
|
|
61
73
|
createdAt: now,
|
|
62
|
-
currentPeriodEnd: subscription
|
|
63
|
-
currentPeriodStart: subscription
|
|
64
|
-
id: subscription
|
|
65
|
-
priceId: subscription
|
|
74
|
+
currentPeriodEnd: periodEndMs(subscription),
|
|
75
|
+
currentPeriodStart: periodStartMs(subscription),
|
|
76
|
+
id: readString(subscription, "id") ?? "",
|
|
77
|
+
priceId: firstPriceId(subscription) ?? "",
|
|
66
78
|
provider: "stripe",
|
|
67
|
-
quantity: subscription
|
|
68
|
-
referenceId: subscription
|
|
69
|
-
state: SUBSCRIPTION_STATE_BY_STRIPE_STATUS[subscription
|
|
79
|
+
quantity: firstQuantity(subscription) ?? 1,
|
|
80
|
+
referenceId: readReferenceId(subscription) ?? "",
|
|
81
|
+
state: SUBSCRIPTION_STATE_BY_STRIPE_STATUS[readString(subscription, "status") ?? ""] ?? "active",
|
|
70
82
|
updatedAt: now
|
|
71
83
|
};
|
|
72
84
|
};
|
|
73
|
-
const subscriptionEventType = (status) => {
|
|
74
|
-
const state = status ? SUBSCRIPTION_STATE_BY_STRIPE_STATUS[status] : void 0;
|
|
75
|
-
if (state === "canceled") {
|
|
76
|
-
return "subscription.canceled";
|
|
77
|
-
}
|
|
78
|
-
if (state === "past_due") {
|
|
79
|
-
return "subscription.past_due";
|
|
80
|
-
}
|
|
81
|
-
if (state === "paused") {
|
|
82
|
-
return "subscription.paused";
|
|
83
|
-
}
|
|
84
|
-
if (state === "active" || state === "trialing") {
|
|
85
|
-
return "subscription.active";
|
|
86
|
-
}
|
|
87
|
-
return "subscription.updated";
|
|
88
|
-
};
|
|
89
85
|
const mapEvent = (eventId, eventType, object) => {
|
|
90
86
|
const base = { eventId, provider: "stripe", raw: { object, type: eventType } };
|
|
91
87
|
const currency = readString(object, "currency") ?? "usd";
|
|
@@ -93,7 +89,7 @@ const mapEvent = (eventId, eventType, object) => {
|
|
|
93
89
|
case "charge.refunded": {
|
|
94
90
|
return {
|
|
95
91
|
...base,
|
|
96
|
-
amount: money(BigInt(readNumber(object, "amount_refunded") ?? 0), currency),
|
|
92
|
+
amount: money(BigInt(Math.round(readNumber(object, "amount_refunded") ?? 0)), currency),
|
|
97
93
|
amountKind: "absolute",
|
|
98
94
|
referenceId: readReferenceId(object),
|
|
99
95
|
sessionId: readString(object, "payment_intent") ?? readString(object, "id"),
|
|
@@ -102,18 +98,20 @@ const mapEvent = (eventId, eventType, object) => {
|
|
|
102
98
|
}
|
|
103
99
|
case "checkout.session.completed": {
|
|
104
100
|
if (readString(object, "mode") === "subscription") {
|
|
101
|
+
const paymentStatus = readString(object, "payment_status");
|
|
102
|
+
const paid = paymentStatus === "paid" || paymentStatus === "no_payment_required";
|
|
105
103
|
return {
|
|
106
104
|
...base,
|
|
107
105
|
customerId: readString(object, "customer"),
|
|
108
106
|
referenceId: readReferenceId(object),
|
|
109
107
|
subscriptionId: readString(object, "subscription"),
|
|
110
|
-
type: "subscription.active"
|
|
108
|
+
type: paid ? "subscription.active" : "subscription.updated"
|
|
111
109
|
};
|
|
112
110
|
}
|
|
113
111
|
const amountTotal = readNumber(object, "amount_total");
|
|
114
112
|
return {
|
|
115
113
|
...base,
|
|
116
|
-
amount: amountTotal === void 0 ? void 0 : money(BigInt(amountTotal), currency),
|
|
114
|
+
amount: amountTotal === void 0 ? void 0 : money(BigInt(Math.round(amountTotal)), currency),
|
|
117
115
|
customerId: readString(object, "customer"),
|
|
118
116
|
referenceId: readReferenceId(object),
|
|
119
117
|
sessionId: readString(object, "payment_intent") ?? readString(object, "id"),
|
|
@@ -132,7 +130,7 @@ const mapEvent = (eventId, eventType, object) => {
|
|
|
132
130
|
quantity: firstQuantity(object),
|
|
133
131
|
referenceId: readReferenceId(object),
|
|
134
132
|
subscriptionId: readString(object, "id"),
|
|
135
|
-
type:
|
|
133
|
+
type: stateToEventType(SUBSCRIPTION_STATE_BY_STRIPE_STATUS[readString(object, "status") ?? ""])
|
|
136
134
|
};
|
|
137
135
|
}
|
|
138
136
|
case "customer.subscription.deleted": {
|
|
@@ -141,7 +139,7 @@ const mapEvent = (eventId, eventType, object) => {
|
|
|
141
139
|
case "payment_intent.amount_capturable_updated": {
|
|
142
140
|
return {
|
|
143
141
|
...base,
|
|
144
|
-
amount: money(BigInt(readNumber(object, "amount") ?? 0), currency),
|
|
142
|
+
amount: money(BigInt(Math.round(readNumber(object, "amount") ?? 0)), currency),
|
|
145
143
|
referenceId: readReferenceId(object),
|
|
146
144
|
sessionId: readString(object, "id"),
|
|
147
145
|
type: "payment.authorized"
|
|
@@ -153,7 +151,7 @@ const mapEvent = (eventId, eventType, object) => {
|
|
|
153
151
|
case "payment_intent.succeeded": {
|
|
154
152
|
return {
|
|
155
153
|
...base,
|
|
156
|
-
amount: money(BigInt(readNumber(object, "amount_received") ?? readNumber(object, "amount") ?? 0), currency),
|
|
154
|
+
amount: money(BigInt(Math.round(readNumber(object, "amount_received") ?? readNumber(object, "amount") ?? 0)), currency),
|
|
157
155
|
customerId: readString(object, "customer"),
|
|
158
156
|
referenceId: readReferenceId(object),
|
|
159
157
|
sessionId: readString(object, "id"),
|
|
@@ -166,7 +164,8 @@ const mapEvent = (eventId, eventType, object) => {
|
|
|
166
164
|
}
|
|
167
165
|
};
|
|
168
166
|
const createStripeAdapter = (options) => {
|
|
169
|
-
const {
|
|
167
|
+
const { webhookSecret } = options;
|
|
168
|
+
const client = options.client;
|
|
170
169
|
return {
|
|
171
170
|
cancelPayment: async (sessionId, paymentOptions) => {
|
|
172
171
|
const intent = await client.paymentIntents.cancel(sessionId, void 0, { idempotencyKey: paymentOptions?.idempotencyKey });
|
|
@@ -218,10 +217,14 @@ const createStripeAdapter = (options) => {
|
|
|
218
217
|
identifier: "stripe",
|
|
219
218
|
parseWebhook: async ({ headers, payload }) => {
|
|
220
219
|
const signatureHeader = headers.get("stripe-signature") ?? "";
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
let event;
|
|
221
|
+
try {
|
|
222
|
+
event = await client.webhooks.constructEventAsync(payload, signatureHeader, webhookSecret, options.webhookToleranceSeconds);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new LunoraPaymentError("WEBHOOK_SIGNATURE_INVALID", error instanceof Error ? error.message : "invalid webhook signature");
|
|
225
|
+
}
|
|
223
226
|
const object = asRecord(asRecord(event.data).object);
|
|
224
|
-
return mapEvent(
|
|
227
|
+
return mapEvent(event.id, event.type, object);
|
|
225
228
|
},
|
|
226
229
|
refundPayment: async (input) => {
|
|
227
230
|
await client.refunds.create(
|
|
@@ -243,7 +246,7 @@ const createStripeAdapter = (options) => {
|
|
|
243
246
|
{
|
|
244
247
|
event_name: input.featureId,
|
|
245
248
|
identifier: input.idempotencyKey,
|
|
246
|
-
payload: { stripe_customer_id: input.customerId, value: String(input.quantity) },
|
|
249
|
+
payload: { stripe_customer_id: input.customerId ?? "", value: String(input.quantity) },
|
|
247
250
|
timestamp: input.timestamp === void 0 ? void 0 : Math.floor(input.timestamp / 1e3)
|
|
248
251
|
},
|
|
249
252
|
{ idempotencyKey: input.idempotencyKey }
|
|
@@ -255,11 +258,9 @@ const createStripeAdapter = (options) => {
|
|
|
255
258
|
},
|
|
256
259
|
updateSubscription: async (subscriptionId, patch) => {
|
|
257
260
|
const parameters = {};
|
|
258
|
-
if (patch.quantity !== void 0) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (patch.priceId) {
|
|
262
|
-
parameters.items = [{ price: patch.priceId }];
|
|
261
|
+
if (patch.priceId !== void 0 || patch.quantity !== void 0) {
|
|
262
|
+
const current = await client.subscriptions.retrieve(subscriptionId);
|
|
263
|
+
parameters.items = [{ id: current.items.data[0]?.id, price: patch.priceId, quantity: patch.quantity }];
|
|
263
264
|
}
|
|
264
265
|
const subscription = await client.subscriptions.update(subscriptionId, parameters);
|
|
265
266
|
return subscriptionFromStripe(subscription);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/payment",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.21",
|
|
4
4
|
"description": "Provider-agnostic payments for Lunora: Stripe-first adapter, webhook sync, and subscription/payment state machine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"billing",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"directory": "packages/payment"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
|
-
"dist",
|
|
29
|
+
"./dist",
|
|
30
30
|
"__assets__",
|
|
31
31
|
"README.md",
|
|
32
32
|
"LICENSE.md"
|
|
@@ -41,20 +41,61 @@
|
|
|
41
41
|
"types": "./dist/index.d.ts",
|
|
42
42
|
"import": "./dist/index.mjs"
|
|
43
43
|
},
|
|
44
|
+
"./stripe": {
|
|
45
|
+
"types": "./dist/providers/stripe.d.ts",
|
|
46
|
+
"import": "./dist/providers/stripe.mjs"
|
|
47
|
+
},
|
|
48
|
+
"./polar": {
|
|
49
|
+
"types": "./dist/providers/polar.d.ts",
|
|
50
|
+
"import": "./dist/providers/polar.mjs"
|
|
51
|
+
},
|
|
52
|
+
"./autumn": {
|
|
53
|
+
"types": "./dist/providers/autumn.d.ts",
|
|
54
|
+
"import": "./dist/providers/autumn.mjs"
|
|
55
|
+
},
|
|
56
|
+
"./autumn-features": {
|
|
57
|
+
"types": "./dist/providers/autumn-features.d.ts",
|
|
58
|
+
"import": "./dist/providers/autumn-features.mjs"
|
|
59
|
+
},
|
|
60
|
+
"./dodopayments": {
|
|
61
|
+
"types": "./dist/providers/dodopayments.d.ts",
|
|
62
|
+
"import": "./dist/providers/dodopayments.mjs"
|
|
63
|
+
},
|
|
64
|
+
"./creem": {
|
|
65
|
+
"types": "./dist/providers/creem.d.ts",
|
|
66
|
+
"import": "./dist/providers/creem.mjs"
|
|
67
|
+
},
|
|
44
68
|
"./package.json": "./package.json"
|
|
45
69
|
},
|
|
46
70
|
"publishConfig": {
|
|
47
71
|
"access": "public"
|
|
48
72
|
},
|
|
49
73
|
"dependencies": {
|
|
50
|
-
"@lunora/
|
|
51
|
-
"@lunora/
|
|
74
|
+
"@lunora/errors": "1.0.0-alpha.3",
|
|
75
|
+
"@lunora/server": "1.0.0-alpha.21",
|
|
76
|
+
"@lunora/values": "1.0.0-alpha.6",
|
|
52
77
|
"dinero.js": "2.0.2"
|
|
53
78
|
},
|
|
54
79
|
"peerDependencies": {
|
|
55
|
-
"
|
|
80
|
+
"@polar-sh/sdk": "^0.48.0",
|
|
81
|
+
"autumn-js": "^1.2.0",
|
|
82
|
+
"creem": "^1.5.0",
|
|
83
|
+
"dodopayments": "^2.41.0",
|
|
84
|
+
"stripe": "^22.0.0"
|
|
56
85
|
},
|
|
57
86
|
"peerDependenciesMeta": {
|
|
87
|
+
"@polar-sh/sdk": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"autumn-js": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"creem": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"dodopayments": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
58
99
|
"stripe": {
|
|
59
100
|
"optional": true
|
|
60
101
|
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
const asRecord = (value) => typeof value === "object" && value !== null ? value : {};
|
|
2
|
-
const readString = (object, key) => typeof object[key] === "string" ? object[key] : void 0;
|
|
3
|
-
const readNumber = (object, key) => typeof object[key] === "number" ? object[key] : void 0;
|
|
4
|
-
const readBoolean = (object, key) => typeof object[key] === "boolean" ? object[key] : void 0;
|
|
5
|
-
|
|
6
|
-
export { asRecord as a, readBoolean as b, readNumber as c, readString as r };
|