paykitjs 0.0.1-alpha.1
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 +21 -0
- package/dist/_virtual/_rolldown/runtime.js +14 -0
- package/dist/api/define-route.d.ts +94 -0
- package/dist/api/define-route.js +153 -0
- package/dist/api/methods.d.ts +422 -0
- package/dist/api/methods.js +67 -0
- package/dist/cli/commands/init.js +264 -0
- package/dist/cli/commands/push.js +71 -0
- package/dist/cli/commands/status.js +84 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +45 -0
- package/dist/cli/templates/index.js +64 -0
- package/dist/cli/utils/detect.js +73 -0
- package/dist/cli/utils/format.js +58 -0
- package/dist/cli/utils/get-config.js +117 -0
- package/dist/cli/utils/shared.js +81 -0
- package/dist/cli/utils/telemetry.js +63 -0
- package/dist/client/index.d.ts +25 -0
- package/dist/client/index.js +27 -0
- package/dist/core/context.d.ts +17 -0
- package/dist/core/context.js +23 -0
- package/dist/core/create-paykit.d.ts +7 -0
- package/dist/core/create-paykit.js +67 -0
- package/dist/core/error-codes.d.ts +12 -0
- package/dist/core/error-codes.js +10 -0
- package/dist/core/errors.d.ts +41 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +51 -0
- package/dist/core/utils.js +21 -0
- package/dist/customer/customer.api.js +47 -0
- package/dist/customer/customer.service.js +342 -0
- package/dist/customer/customer.types.d.ts +31 -0
- package/dist/database/index.d.ts +8 -0
- package/dist/database/index.js +32 -0
- package/dist/database/migrations/0000_init.sql +157 -0
- package/dist/database/migrations/meta/0000_snapshot.json +1222 -0
- package/dist/database/migrations/meta/_journal.json +13 -0
- package/dist/database/schema.d.ts +1767 -0
- package/dist/database/schema.js +150 -0
- package/dist/entitlement/entitlement.api.js +33 -0
- package/dist/entitlement/entitlement.service.d.ts +17 -0
- package/dist/entitlement/entitlement.service.js +123 -0
- package/dist/handlers/next.d.ts +9 -0
- package/dist/handlers/next.js +9 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +6 -0
- package/dist/invoice/invoice.service.js +54 -0
- package/dist/payment/payment.service.js +49 -0
- package/dist/payment-method/payment-method.service.js +78 -0
- package/dist/product/product-sync.service.js +111 -0
- package/dist/product/product.service.js +127 -0
- package/dist/providers/provider.d.ts +159 -0
- package/dist/providers/stripe.js +547 -0
- package/dist/subscription/subscription.api.js +24 -0
- package/dist/subscription/subscription.service.js +896 -0
- package/dist/subscription/subscription.types.d.ts +18 -0
- package/dist/subscription/subscription.types.js +11 -0
- package/dist/testing/testing.api.js +29 -0
- package/dist/testing/testing.service.js +49 -0
- package/dist/types/events.d.ts +181 -0
- package/dist/types/instance.d.ts +88 -0
- package/dist/types/models.d.ts +11 -0
- package/dist/types/options.d.ts +32 -0
- package/dist/types/plugin.d.ts +11 -0
- package/dist/types/schema.d.ts +99 -0
- package/dist/types/schema.js +192 -0
- package/dist/utilities/dependencies/check-dependencies.js +16 -0
- package/dist/utilities/dependencies/get-dependencies.js +68 -0
- package/dist/utilities/dependencies/index.js +8 -0
- package/dist/utilities/dependencies/paykit-package-list.js +8 -0
- package/dist/webhook/webhook.api.js +29 -0
- package/dist/webhook/webhook.service.js +143 -0
- package/package.json +76 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { PAYKIT_ERROR_CODES, PayKitError } from "../core/errors.js";
|
|
2
|
+
import StripeSdk from "stripe";
|
|
3
|
+
//#region src/providers/stripe.ts
|
|
4
|
+
function toDate(value) {
|
|
5
|
+
return typeof value === "number" ? /* @__PURE__ */ new Date(value * 1e3) : null;
|
|
6
|
+
}
|
|
7
|
+
function getLatestPeriodEnd(subscription) {
|
|
8
|
+
const firstItem = subscription.items.data[0];
|
|
9
|
+
if (!firstItem) return subscription.current_period_end ?? null;
|
|
10
|
+
return subscription.items.data.reduce((latest, item) => {
|
|
11
|
+
return Math.max(latest, item.current_period_end);
|
|
12
|
+
}, firstItem.current_period_end);
|
|
13
|
+
}
|
|
14
|
+
function getEarliestPeriodStart(subscription) {
|
|
15
|
+
const firstItem = subscription.items.data[0];
|
|
16
|
+
if (!firstItem) return subscription.current_period_start ?? null;
|
|
17
|
+
return subscription.items.data.reduce((earliest, item) => {
|
|
18
|
+
return Math.min(earliest, item.current_period_start);
|
|
19
|
+
}, firstItem.current_period_start);
|
|
20
|
+
}
|
|
21
|
+
function getStripeCustomerId(customer) {
|
|
22
|
+
if (!customer) return null;
|
|
23
|
+
return typeof customer === "string" ? customer : customer.id;
|
|
24
|
+
}
|
|
25
|
+
function normalizeStripePaymentMethod(paymentMethod) {
|
|
26
|
+
return {
|
|
27
|
+
expiryMonth: paymentMethod.card?.exp_month ?? void 0,
|
|
28
|
+
expiryYear: paymentMethod.card?.exp_year ?? void 0,
|
|
29
|
+
last4: paymentMethod.card?.last4 ?? void 0,
|
|
30
|
+
providerMethodId: paymentMethod.id,
|
|
31
|
+
type: paymentMethod.type
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function normalizeStripePaymentIntent(paymentIntent) {
|
|
35
|
+
const providerMethodId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
|
|
36
|
+
return {
|
|
37
|
+
amount: paymentIntent.amount_received || paymentIntent.amount,
|
|
38
|
+
createdAt: /* @__PURE__ */ new Date(paymentIntent.created * 1e3),
|
|
39
|
+
currency: paymentIntent.currency,
|
|
40
|
+
description: paymentIntent.description,
|
|
41
|
+
metadata: Object.keys(paymentIntent.metadata).length > 0 ? paymentIntent.metadata : void 0,
|
|
42
|
+
providerMethodId,
|
|
43
|
+
providerPaymentId: paymentIntent.id,
|
|
44
|
+
status: paymentIntent.status
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function normalizeStripeInvoice(invoice) {
|
|
48
|
+
return {
|
|
49
|
+
currency: invoice.currency,
|
|
50
|
+
hostedUrl: invoice.hosted_invoice_url,
|
|
51
|
+
periodEndAt: toDate(invoice.period_end),
|
|
52
|
+
periodStartAt: toDate(invoice.period_start),
|
|
53
|
+
providerInvoiceId: invoice.id,
|
|
54
|
+
status: invoice.status,
|
|
55
|
+
totalAmount: invoice.total ?? 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function normalizeStripeSubscription(subscription) {
|
|
59
|
+
const firstItem = subscription.items.data[0];
|
|
60
|
+
const providerPriceId = typeof firstItem?.price === "string" ? firstItem.price : firstItem?.price.id;
|
|
61
|
+
const periodStart = getEarliestPeriodStart(subscription);
|
|
62
|
+
const periodEnd = getLatestPeriodEnd(subscription);
|
|
63
|
+
const cancelAt = subscription.cancel_at;
|
|
64
|
+
return {
|
|
65
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end || cancelAt != null && cancelAt > 0,
|
|
66
|
+
canceledAt: toDate(subscription.canceled_at),
|
|
67
|
+
currentPeriodEndAt: toDate(periodEnd),
|
|
68
|
+
currentPeriodStartAt: toDate(periodStart),
|
|
69
|
+
endedAt: toDate(subscription.ended_at),
|
|
70
|
+
providerPriceId: providerPriceId ?? null,
|
|
71
|
+
providerSubscriptionId: subscription.id,
|
|
72
|
+
providerSubscriptionScheduleId: (typeof subscription.schedule === "string" ? subscription.schedule : subscription.schedule?.id) ?? null,
|
|
73
|
+
status: subscription.status
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function normalizeStripeTestClock(clock) {
|
|
77
|
+
return {
|
|
78
|
+
frozenTime: /* @__PURE__ */ new Date(clock.frozen_time * 1e3),
|
|
79
|
+
id: clock.id,
|
|
80
|
+
name: clock.name ?? null,
|
|
81
|
+
status: clock.status
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function assertStripeTestKey(options) {
|
|
85
|
+
if (!options.secretKey.startsWith("sk_test_")) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_TEST_KEY_REQUIRED);
|
|
86
|
+
}
|
|
87
|
+
async function retrieveExpandedSubscription(client, providerSubscriptionId) {
|
|
88
|
+
return await client.subscriptions.retrieve(providerSubscriptionId, { expand: [
|
|
89
|
+
"items.data.price",
|
|
90
|
+
"latest_invoice.payment_intent",
|
|
91
|
+
"schedule"
|
|
92
|
+
] });
|
|
93
|
+
}
|
|
94
|
+
function normalizeRequiredAction(paymentIntent) {
|
|
95
|
+
const nextActionType = paymentIntent?.next_action?.type;
|
|
96
|
+
if (!nextActionType) return null;
|
|
97
|
+
return {
|
|
98
|
+
clientSecret: paymentIntent.client_secret ?? void 0,
|
|
99
|
+
paymentIntentId: paymentIntent.id,
|
|
100
|
+
type: nextActionType
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function isPaymentMethodAttachedToCustomer(paymentMethod, stripeCustomerId) {
|
|
104
|
+
if (!stripeCustomerId) return false;
|
|
105
|
+
return getStripeCustomerId(paymentMethod.customer) === stripeCustomerId;
|
|
106
|
+
}
|
|
107
|
+
async function getCheckoutPaymentDetails(client, session) {
|
|
108
|
+
const stripeCustomerId = getStripeCustomerId(session.customer);
|
|
109
|
+
if (!stripeCustomerId) return {
|
|
110
|
+
paymentIntent: null,
|
|
111
|
+
paymentMethod: null
|
|
112
|
+
};
|
|
113
|
+
if (session.mode === "payment" || session.mode === "subscription") {
|
|
114
|
+
const paymentIntentId = typeof session.payment_intent === "string" ? session.payment_intent : session.payment_intent?.id;
|
|
115
|
+
if (paymentIntentId) {
|
|
116
|
+
const paymentIntent = await client.paymentIntents.retrieve(paymentIntentId, { expand: ["payment_method"] });
|
|
117
|
+
const paymentMethod = paymentIntent.payment_method;
|
|
118
|
+
if (paymentMethod && typeof paymentMethod !== "string") return {
|
|
119
|
+
paymentIntent,
|
|
120
|
+
paymentMethod: isPaymentMethodAttachedToCustomer(paymentMethod, stripeCustomerId) ? paymentMethod : null
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (session.mode === "subscription") {
|
|
124
|
+
const subscriptionId = typeof session.subscription === "string" ? session.subscription : session.subscription?.id;
|
|
125
|
+
if (subscriptionId) {
|
|
126
|
+
const paymentMethod = (await client.subscriptions.retrieve(subscriptionId, { expand: ["default_payment_method"] })).default_payment_method;
|
|
127
|
+
if (paymentMethod && typeof paymentMethod !== "string") return {
|
|
128
|
+
paymentIntent: null,
|
|
129
|
+
paymentMethod: isPaymentMethodAttachedToCustomer(paymentMethod, stripeCustomerId) ? paymentMethod : null
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
paymentIntent: null,
|
|
135
|
+
paymentMethod: null
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (session.mode === "setup") {
|
|
139
|
+
const setupIntentId = typeof session.setup_intent === "string" ? session.setup_intent : session.setup_intent?.id;
|
|
140
|
+
if (!setupIntentId) return {
|
|
141
|
+
paymentIntent: null,
|
|
142
|
+
paymentMethod: null
|
|
143
|
+
};
|
|
144
|
+
const paymentMethod = (await client.setupIntents.retrieve(setupIntentId, { expand: ["payment_method"] })).payment_method;
|
|
145
|
+
if (!paymentMethod || typeof paymentMethod === "string") return {
|
|
146
|
+
paymentIntent: null,
|
|
147
|
+
paymentMethod: null
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
paymentIntent: null,
|
|
151
|
+
paymentMethod: isPaymentMethodAttachedToCustomer(paymentMethod, stripeCustomerId) ? paymentMethod : null
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
paymentIntent: null,
|
|
156
|
+
paymentMethod: null
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function createCheckoutCompletedEvents(client, event) {
|
|
160
|
+
if (event.type !== "checkout.session.completed") return [];
|
|
161
|
+
const session = event.data.object;
|
|
162
|
+
const stripeCustomerId = getStripeCustomerId(session.customer);
|
|
163
|
+
const providerCustomerId = session.client_reference_id ?? stripeCustomerId;
|
|
164
|
+
if (!providerCustomerId) return [];
|
|
165
|
+
const events = [];
|
|
166
|
+
const { paymentIntent, paymentMethod } = await getCheckoutPaymentDetails(client, session);
|
|
167
|
+
const providerSubscriptionId = typeof session.subscription === "string" ? session.subscription : session.subscription?.id ?? null;
|
|
168
|
+
const providerInvoiceId = typeof session.invoice === "string" ? session.invoice : session.invoice?.id ?? null;
|
|
169
|
+
const expandedSubscription = session.mode === "subscription" && providerSubscriptionId ? await retrieveExpandedSubscription(client, providerSubscriptionId) : null;
|
|
170
|
+
const expandedInvoice = providerInvoiceId != null ? await client.invoices.retrieve(providerInvoiceId, { expand: ["payment_intent"] }) : null;
|
|
171
|
+
if (paymentMethod) {
|
|
172
|
+
const normalizedPaymentMethod = {
|
|
173
|
+
...normalizeStripePaymentMethod(paymentMethod),
|
|
174
|
+
isDefault: session.mode === "subscription"
|
|
175
|
+
};
|
|
176
|
+
events.push({
|
|
177
|
+
actions: [{
|
|
178
|
+
data: {
|
|
179
|
+
paymentMethod: normalizedPaymentMethod,
|
|
180
|
+
providerCustomerId
|
|
181
|
+
},
|
|
182
|
+
type: "payment_method.upsert"
|
|
183
|
+
}],
|
|
184
|
+
name: "payment_method.attached",
|
|
185
|
+
payload: {
|
|
186
|
+
paymentMethod: normalizedPaymentMethod,
|
|
187
|
+
providerCustomerId
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (session.mode === "payment" && paymentIntent?.status === "succeeded") {
|
|
192
|
+
const normalizedPayment = normalizeStripePaymentIntent(paymentIntent);
|
|
193
|
+
events.push({
|
|
194
|
+
actions: [{
|
|
195
|
+
data: {
|
|
196
|
+
payment: normalizedPayment,
|
|
197
|
+
providerCustomerId
|
|
198
|
+
},
|
|
199
|
+
type: "payment.upsert"
|
|
200
|
+
}],
|
|
201
|
+
name: "payment.succeeded",
|
|
202
|
+
payload: {
|
|
203
|
+
payment: normalizedPayment,
|
|
204
|
+
providerCustomerId
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const sessionMetadata = session.metadata ?? {};
|
|
209
|
+
events.push({
|
|
210
|
+
name: "checkout.completed",
|
|
211
|
+
payload: {
|
|
212
|
+
checkoutSessionId: session.id,
|
|
213
|
+
invoice: expandedInvoice ? normalizeStripeInvoice(expandedInvoice) : void 0,
|
|
214
|
+
metadata: Object.keys(sessionMetadata).length > 0 ? sessionMetadata : void 0,
|
|
215
|
+
mode: session.mode ?? void 0,
|
|
216
|
+
paymentStatus: session.payment_status,
|
|
217
|
+
providerCustomerId,
|
|
218
|
+
providerEventId: event.id,
|
|
219
|
+
providerInvoiceId: providerInvoiceId ?? void 0,
|
|
220
|
+
providerSubscriptionId: providerSubscriptionId ?? void 0,
|
|
221
|
+
status: session.status,
|
|
222
|
+
subscription: expandedSubscription ? normalizeStripeSubscription(expandedSubscription) : void 0
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return events;
|
|
226
|
+
}
|
|
227
|
+
async function createSubscriptionEvents(event) {
|
|
228
|
+
if (event.type !== "customer.subscription.created" && event.type !== "customer.subscription.updated" && event.type !== "customer.subscription.deleted") return [];
|
|
229
|
+
const subscription = event.data.object;
|
|
230
|
+
const providerCustomerId = getStripeCustomerId(subscription.customer);
|
|
231
|
+
if (!providerCustomerId) return [];
|
|
232
|
+
if (event.type === "customer.subscription.deleted") return [{
|
|
233
|
+
actions: [{
|
|
234
|
+
data: {
|
|
235
|
+
providerCustomerId,
|
|
236
|
+
providerSubscriptionId: subscription.id
|
|
237
|
+
},
|
|
238
|
+
type: "subscription.delete"
|
|
239
|
+
}],
|
|
240
|
+
name: "subscription.deleted",
|
|
241
|
+
payload: {
|
|
242
|
+
providerCustomerId,
|
|
243
|
+
providerEventId: event.id,
|
|
244
|
+
providerSubscriptionId: subscription.id
|
|
245
|
+
}
|
|
246
|
+
}];
|
|
247
|
+
const normalizedSubscription = normalizeStripeSubscription(subscription);
|
|
248
|
+
return [{
|
|
249
|
+
actions: [{
|
|
250
|
+
data: {
|
|
251
|
+
providerCustomerId,
|
|
252
|
+
subscription: normalizedSubscription
|
|
253
|
+
},
|
|
254
|
+
type: "subscription.upsert"
|
|
255
|
+
}],
|
|
256
|
+
name: "subscription.updated",
|
|
257
|
+
payload: {
|
|
258
|
+
providerCustomerId,
|
|
259
|
+
providerEventId: event.id,
|
|
260
|
+
subscription: normalizedSubscription
|
|
261
|
+
}
|
|
262
|
+
}];
|
|
263
|
+
}
|
|
264
|
+
function createInvoiceEvents(event) {
|
|
265
|
+
if (event.type !== "invoice.created" && event.type !== "invoice.finalized" && event.type !== "invoice.paid" && event.type !== "invoice.payment_failed" && event.type !== "invoice.updated") return [];
|
|
266
|
+
const invoice = event.data.object;
|
|
267
|
+
const providerCustomerId = getStripeCustomerId(invoice.customer);
|
|
268
|
+
if (!providerCustomerId) return [];
|
|
269
|
+
const providerSubscriptionId = typeof invoice.subscription === "string" ? invoice.subscription : invoice.subscription?.id ?? null;
|
|
270
|
+
const normalizedInvoice = normalizeStripeInvoice(invoice);
|
|
271
|
+
return [{
|
|
272
|
+
actions: [{
|
|
273
|
+
data: {
|
|
274
|
+
invoice: normalizedInvoice,
|
|
275
|
+
providerCustomerId,
|
|
276
|
+
providerSubscriptionId
|
|
277
|
+
},
|
|
278
|
+
type: "invoice.upsert"
|
|
279
|
+
}],
|
|
280
|
+
name: "invoice.updated",
|
|
281
|
+
payload: {
|
|
282
|
+
invoice: normalizedInvoice,
|
|
283
|
+
providerCustomerId,
|
|
284
|
+
providerEventId: event.id,
|
|
285
|
+
providerSubscriptionId
|
|
286
|
+
}
|
|
287
|
+
}];
|
|
288
|
+
}
|
|
289
|
+
function createDetachedPaymentMethodEvents(event) {
|
|
290
|
+
if (event.type !== "payment_method.detached") return [];
|
|
291
|
+
const paymentMethod = event.data.object;
|
|
292
|
+
return [{
|
|
293
|
+
actions: [{
|
|
294
|
+
data: { providerMethodId: paymentMethod.id },
|
|
295
|
+
type: "payment_method.delete"
|
|
296
|
+
}],
|
|
297
|
+
name: "payment_method.detached",
|
|
298
|
+
payload: {
|
|
299
|
+
providerEventId: event.id,
|
|
300
|
+
providerMethodId: paymentMethod.id
|
|
301
|
+
}
|
|
302
|
+
}];
|
|
303
|
+
}
|
|
304
|
+
function createStripeProvider(client, options) {
|
|
305
|
+
const currency = options.currency ?? "usd";
|
|
306
|
+
return {
|
|
307
|
+
async upsertCustomer(data) {
|
|
308
|
+
let testClock;
|
|
309
|
+
if (data.createTestClock) {
|
|
310
|
+
assertStripeTestKey(options);
|
|
311
|
+
testClock = normalizeStripeTestClock(await client.testHelpers.testClocks.create({
|
|
312
|
+
frozen_time: Math.floor(Date.now() / 1e3),
|
|
313
|
+
name: data.id
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
return { providerCustomer: {
|
|
317
|
+
id: (await client.customers.create({
|
|
318
|
+
email: data.email,
|
|
319
|
+
metadata: {
|
|
320
|
+
customerId: data.id,
|
|
321
|
+
...data.metadata
|
|
322
|
+
},
|
|
323
|
+
name: data.name,
|
|
324
|
+
test_clock: testClock?.id
|
|
325
|
+
})).id,
|
|
326
|
+
frozenTime: testClock?.frozenTime.toISOString(),
|
|
327
|
+
testClockId: testClock?.id
|
|
328
|
+
} };
|
|
329
|
+
},
|
|
330
|
+
async deleteCustomer(data) {
|
|
331
|
+
await client.customers.del(data.providerCustomerId);
|
|
332
|
+
},
|
|
333
|
+
async getTestClock(data) {
|
|
334
|
+
return normalizeStripeTestClock(await client.testHelpers.testClocks.retrieve(data.testClockId));
|
|
335
|
+
},
|
|
336
|
+
async advanceTestClock(data) {
|
|
337
|
+
assertStripeTestKey(options);
|
|
338
|
+
await client.testHelpers.testClocks.advance(data.testClockId, { frozen_time: Math.floor(data.frozenTime.getTime() / 1e3) });
|
|
339
|
+
for (let i = 0; i < 60; i++) {
|
|
340
|
+
const clock = await client.testHelpers.testClocks.retrieve(data.testClockId);
|
|
341
|
+
if (clock.status === "ready") return normalizeStripeTestClock(clock);
|
|
342
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
343
|
+
}
|
|
344
|
+
throw new Error(`Test clock ${data.testClockId} did not reach 'ready' status`);
|
|
345
|
+
},
|
|
346
|
+
async attachPaymentMethod(data) {
|
|
347
|
+
const session = await client.checkout.sessions.create({
|
|
348
|
+
cancel_url: data.returnURL,
|
|
349
|
+
client_reference_id: data.providerCustomerId,
|
|
350
|
+
customer: data.providerCustomerId,
|
|
351
|
+
mode: "setup",
|
|
352
|
+
success_url: data.returnURL
|
|
353
|
+
});
|
|
354
|
+
if (!session.url) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_SESSION_INVALID);
|
|
355
|
+
return { url: session.url };
|
|
356
|
+
},
|
|
357
|
+
async createSubscriptionCheckout(data) {
|
|
358
|
+
const sessionParams = {
|
|
359
|
+
cancel_url: data.cancelUrl ?? data.successUrl,
|
|
360
|
+
client_reference_id: data.providerCustomerId,
|
|
361
|
+
customer: data.providerCustomerId,
|
|
362
|
+
line_items: [{
|
|
363
|
+
price: data.providerPriceId,
|
|
364
|
+
quantity: 1
|
|
365
|
+
}],
|
|
366
|
+
metadata: data.metadata,
|
|
367
|
+
mode: "subscription",
|
|
368
|
+
success_url: data.successUrl
|
|
369
|
+
};
|
|
370
|
+
const session = await client.checkout.sessions.create(sessionParams);
|
|
371
|
+
if (!session.url) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_SESSION_INVALID);
|
|
372
|
+
return {
|
|
373
|
+
paymentUrl: session.url,
|
|
374
|
+
providerCheckoutSessionId: session.id
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
async createSubscription(data) {
|
|
378
|
+
const createParams = {
|
|
379
|
+
customer: data.providerCustomerId,
|
|
380
|
+
items: [{ price: data.providerPriceId }],
|
|
381
|
+
payment_behavior: "default_incomplete",
|
|
382
|
+
expand: ["latest_invoice.payment_intent"]
|
|
383
|
+
};
|
|
384
|
+
const createdSubscription = await client.subscriptions.create(createParams);
|
|
385
|
+
const latestInvoice = createdSubscription.latest_invoice;
|
|
386
|
+
return {
|
|
387
|
+
invoice: latestInvoice && typeof latestInvoice !== "string" ? normalizeStripeInvoice(latestInvoice) : null,
|
|
388
|
+
paymentUrl: null,
|
|
389
|
+
requiredAction: normalizeRequiredAction((latestInvoice && typeof latestInvoice !== "string" ? latestInvoice.payment_intent : null) ?? null),
|
|
390
|
+
subscription: normalizeStripeSubscription(createdSubscription)
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
async updateSubscription(data) {
|
|
394
|
+
const currentItem = (await retrieveExpandedSubscription(client, data.providerSubscriptionId)).items.data[0];
|
|
395
|
+
if (!currentItem) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_SUBSCRIPTION_MISSING_ITEMS);
|
|
396
|
+
const updatedSubscription = await client.subscriptions.update(data.providerSubscriptionId, {
|
|
397
|
+
items: [{
|
|
398
|
+
id: currentItem.id,
|
|
399
|
+
price: data.providerPriceId
|
|
400
|
+
}],
|
|
401
|
+
payment_behavior: "pending_if_incomplete",
|
|
402
|
+
proration_behavior: "always_invoice",
|
|
403
|
+
expand: ["latest_invoice.payment_intent"]
|
|
404
|
+
});
|
|
405
|
+
const latestInvoice = updatedSubscription.latest_invoice;
|
|
406
|
+
return {
|
|
407
|
+
invoice: latestInvoice && typeof latestInvoice !== "string" ? normalizeStripeInvoice(latestInvoice) : null,
|
|
408
|
+
paymentUrl: null,
|
|
409
|
+
requiredAction: normalizeRequiredAction((latestInvoice && typeof latestInvoice !== "string" ? latestInvoice.payment_intent : null) ?? null),
|
|
410
|
+
subscription: normalizeStripeSubscription(updatedSubscription)
|
|
411
|
+
};
|
|
412
|
+
},
|
|
413
|
+
async scheduleSubscriptionChange(data) {
|
|
414
|
+
if (!data.providerPriceId) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_PRICE_REQUIRED);
|
|
415
|
+
const currentSub = await client.subscriptions.retrieve(data.providerSubscriptionId, { expand: ["items"] });
|
|
416
|
+
const periodEndSeconds = getLatestPeriodEnd(currentSub);
|
|
417
|
+
if (typeof periodEndSeconds !== "number") throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_SUBSCRIPTION_MISSING_PERIOD);
|
|
418
|
+
const currentItems = currentSub.items.data.map((item) => ({
|
|
419
|
+
price: item.price.id,
|
|
420
|
+
quantity: 1
|
|
421
|
+
}));
|
|
422
|
+
let schedule;
|
|
423
|
+
if (data.providerSubscriptionScheduleId) schedule = await client.subscriptionSchedules.retrieve(data.providerSubscriptionScheduleId);
|
|
424
|
+
else {
|
|
425
|
+
const existingScheduleId = typeof currentSub.schedule === "string" ? currentSub.schedule : currentSub.schedule?.id ?? null;
|
|
426
|
+
schedule = existingScheduleId ? await client.subscriptionSchedules.retrieve(existingScheduleId) : await client.subscriptionSchedules.create({ from_subscription: data.providerSubscriptionId });
|
|
427
|
+
}
|
|
428
|
+
const scheduleId = schedule.id;
|
|
429
|
+
const currentPhaseStart = schedule.phases[0]?.start_date ?? Math.floor(Date.now() / 1e3);
|
|
430
|
+
await client.subscriptionSchedules.update(scheduleId, {
|
|
431
|
+
end_behavior: "release",
|
|
432
|
+
phases: [{
|
|
433
|
+
items: currentItems,
|
|
434
|
+
start_date: currentPhaseStart,
|
|
435
|
+
end_date: periodEndSeconds
|
|
436
|
+
}, {
|
|
437
|
+
items: [{
|
|
438
|
+
price: data.providerPriceId,
|
|
439
|
+
quantity: 1
|
|
440
|
+
}],
|
|
441
|
+
start_date: periodEndSeconds
|
|
442
|
+
}]
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
paymentUrl: null,
|
|
446
|
+
requiredAction: null,
|
|
447
|
+
subscription: normalizeStripeSubscription(await retrieveExpandedSubscription(client, data.providerSubscriptionId))
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
async cancelSubscription(data) {
|
|
451
|
+
const currentSubscription = await client.subscriptions.retrieve(data.providerSubscriptionId);
|
|
452
|
+
let scheduleId = data.providerSubscriptionScheduleId ?? null;
|
|
453
|
+
if (!scheduleId) scheduleId = typeof currentSubscription.schedule === "string" ? currentSubscription.schedule : currentSubscription.schedule?.id ?? null;
|
|
454
|
+
if (scheduleId) {
|
|
455
|
+
const schedule = await client.subscriptionSchedules.retrieve(scheduleId);
|
|
456
|
+
if (schedule.status !== "released" && schedule.status !== "canceled") await client.subscriptionSchedules.release(scheduleId);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
paymentUrl: null,
|
|
460
|
+
requiredAction: null,
|
|
461
|
+
subscription: normalizeStripeSubscription(await client.subscriptions.update(data.providerSubscriptionId, { cancel_at_period_end: true }))
|
|
462
|
+
};
|
|
463
|
+
},
|
|
464
|
+
async listActiveSubscriptions(data) {
|
|
465
|
+
return (await client.subscriptions.list({
|
|
466
|
+
customer: data.providerCustomerId,
|
|
467
|
+
status: "active"
|
|
468
|
+
})).data.map((sub) => ({ providerSubscriptionId: sub.id }));
|
|
469
|
+
},
|
|
470
|
+
async resumeSubscription(data) {
|
|
471
|
+
let scheduleId = data.providerSubscriptionScheduleId ?? null;
|
|
472
|
+
if (!scheduleId) {
|
|
473
|
+
const sub = await client.subscriptions.retrieve(data.providerSubscriptionId);
|
|
474
|
+
scheduleId = typeof sub.schedule === "string" ? sub.schedule : sub.schedule?.id ?? null;
|
|
475
|
+
}
|
|
476
|
+
if (scheduleId) {
|
|
477
|
+
const schedule = await client.subscriptionSchedules.retrieve(scheduleId);
|
|
478
|
+
if (schedule.status !== "released" && schedule.status !== "canceled") await client.subscriptionSchedules.release(scheduleId);
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
paymentUrl: null,
|
|
482
|
+
requiredAction: null,
|
|
483
|
+
subscription: normalizeStripeSubscription(await client.subscriptions.update(data.providerSubscriptionId, { cancel_at_period_end: false }))
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
async detachPaymentMethod(data) {
|
|
487
|
+
await client.paymentMethods.detach(data.providerMethodId);
|
|
488
|
+
},
|
|
489
|
+
async syncProduct(data) {
|
|
490
|
+
let providerProductId = data.existingProviderProductId;
|
|
491
|
+
if (!providerProductId) providerProductId = (await client.products.create({
|
|
492
|
+
metadata: { paykit_product_id: data.id },
|
|
493
|
+
name: data.name
|
|
494
|
+
})).id;
|
|
495
|
+
else await client.products.update(providerProductId, { name: data.name });
|
|
496
|
+
if (data.existingProviderPriceId) return {
|
|
497
|
+
providerPriceId: data.existingProviderPriceId,
|
|
498
|
+
providerProductId
|
|
499
|
+
};
|
|
500
|
+
const priceParams = {
|
|
501
|
+
currency,
|
|
502
|
+
product: providerProductId,
|
|
503
|
+
unit_amount: data.priceAmount
|
|
504
|
+
};
|
|
505
|
+
if (data.priceInterval) priceParams.recurring = { interval: data.priceInterval };
|
|
506
|
+
return {
|
|
507
|
+
providerPriceId: (await client.prices.create(priceParams)).id,
|
|
508
|
+
providerProductId
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
async createInvoice(data) {
|
|
512
|
+
const stripeInvoice = await client.invoices.create({
|
|
513
|
+
auto_advance: data.autoAdvance ?? true,
|
|
514
|
+
collection_method: "charge_automatically",
|
|
515
|
+
customer: data.providerCustomerId,
|
|
516
|
+
currency
|
|
517
|
+
});
|
|
518
|
+
if (data.lines.length > 0) await client.invoices.addLines(stripeInvoice.id, { lines: data.lines.map((line) => ({
|
|
519
|
+
amount: line.amount,
|
|
520
|
+
description: line.description
|
|
521
|
+
})) });
|
|
522
|
+
return normalizeStripeInvoice(await client.invoices.finalizeInvoice(stripeInvoice.id));
|
|
523
|
+
},
|
|
524
|
+
async handleWebhook(data) {
|
|
525
|
+
const signature = data.headers["stripe-signature"];
|
|
526
|
+
if (!signature) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_SIGNATURE_MISSING);
|
|
527
|
+
const event = client.webhooks.constructEvent(data.body, signature, options.webhookSecret);
|
|
528
|
+
return [
|
|
529
|
+
...await createCheckoutCompletedEvents(client, event),
|
|
530
|
+
...await createSubscriptionEvents(event),
|
|
531
|
+
...createInvoiceEvents(event),
|
|
532
|
+
...createDetachedPaymentMethodEvents(event)
|
|
533
|
+
];
|
|
534
|
+
},
|
|
535
|
+
async createPortalSession(data) {
|
|
536
|
+
return { url: (await client.billingPortal.sessions.create({
|
|
537
|
+
customer: data.providerCustomerId,
|
|
538
|
+
return_url: data.returnUrl
|
|
539
|
+
})).url };
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function createStripeRuntime(options) {
|
|
544
|
+
return createStripeProvider(new StripeSdk(options.secretKey), options);
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
547
|
+
export { createStripeRuntime };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { subscribeToPlan } from "./subscription.service.js";
|
|
2
|
+
import { definePayKitMethod } from "../api/define-route.js";
|
|
3
|
+
import { subscribeBodySchema } from "./subscription.types.js";
|
|
4
|
+
//#region src/subscription/subscription.api.ts
|
|
5
|
+
/** Applies a subscription change for the resolved customer. */
|
|
6
|
+
const subscribe = definePayKitMethod({
|
|
7
|
+
input: subscribeBodySchema,
|
|
8
|
+
requireCustomer: true,
|
|
9
|
+
route: {
|
|
10
|
+
client: true,
|
|
11
|
+
method: "POST",
|
|
12
|
+
path: "/subscribe"
|
|
13
|
+
}
|
|
14
|
+
}, async (ctx) => {
|
|
15
|
+
return subscribeToPlan(ctx.paykit, {
|
|
16
|
+
customerId: ctx.customer.id,
|
|
17
|
+
forceCheckout: ctx.input.forceCheckout,
|
|
18
|
+
planId: ctx.input.planId,
|
|
19
|
+
successUrl: ctx.input.successUrl,
|
|
20
|
+
cancelUrl: ctx.input.cancelUrl
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
//#endregion
|
|
24
|
+
export { subscribe };
|