@xenterprises/fastify-xstripe 1.0.0

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.
@@ -0,0 +1,355 @@
1
+ // src/handlers/defaultHandlers.js
2
+
3
+ /**
4
+ * Default webhook event handlers
5
+ * These provide basic logging and can be overridden by user-provided handlers
6
+ */
7
+
8
+ export const defaultHandlers = {
9
+ // ========== Subscription Events ==========
10
+
11
+ /**
12
+ * customer.subscription.created
13
+ * Fired when a subscription is created
14
+ */
15
+ "customer.subscription.created": async (event, fastify, stripe) => {
16
+ const subscription = event.data.object;
17
+ fastify.log.info({
18
+ event: "subscription.created",
19
+ subscriptionId: subscription.id,
20
+ customerId: subscription.customer,
21
+ status: subscription.status,
22
+ planId: subscription.items.data[0]?.price.id,
23
+ });
24
+ },
25
+
26
+ /**
27
+ * customer.subscription.updated
28
+ * Fired when a subscription is updated (plan change, status change, etc.)
29
+ */
30
+ "customer.subscription.updated": async (event, fastify, stripe) => {
31
+ const subscription = event.data.object;
32
+ const previousAttributes = event.data.previous_attributes;
33
+
34
+ fastify.log.info({
35
+ event: "subscription.updated",
36
+ subscriptionId: subscription.id,
37
+ customerId: subscription.customer,
38
+ status: subscription.status,
39
+ changes: previousAttributes,
40
+ });
41
+ },
42
+
43
+ /**
44
+ * customer.subscription.deleted
45
+ * Fired when a subscription is canceled/deleted
46
+ */
47
+ "customer.subscription.deleted": async (event, fastify, stripe) => {
48
+ const subscription = event.data.object;
49
+ fastify.log.info({
50
+ event: "subscription.deleted",
51
+ subscriptionId: subscription.id,
52
+ customerId: subscription.customer,
53
+ canceledAt: subscription.canceled_at,
54
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
55
+ });
56
+ },
57
+
58
+ /**
59
+ * customer.subscription.trial_will_end
60
+ * Fired 3 days before trial ends
61
+ */
62
+ "customer.subscription.trial_will_end": async (event, fastify, stripe) => {
63
+ const subscription = event.data.object;
64
+ fastify.log.info({
65
+ event: "subscription.trial_will_end",
66
+ subscriptionId: subscription.id,
67
+ customerId: subscription.customer,
68
+ trialEnd: subscription.trial_end,
69
+ });
70
+ },
71
+
72
+ /**
73
+ * customer.subscription.paused
74
+ * Fired when a subscription is paused
75
+ */
76
+ "customer.subscription.paused": async (event, fastify, stripe) => {
77
+ const subscription = event.data.object;
78
+ fastify.log.info({
79
+ event: "subscription.paused",
80
+ subscriptionId: subscription.id,
81
+ customerId: subscription.customer,
82
+ });
83
+ },
84
+
85
+ /**
86
+ * customer.subscription.resumed
87
+ * Fired when a subscription is resumed
88
+ */
89
+ "customer.subscription.resumed": async (event, fastify, stripe) => {
90
+ const subscription = event.data.object;
91
+ fastify.log.info({
92
+ event: "subscription.resumed",
93
+ subscriptionId: subscription.id,
94
+ customerId: subscription.customer,
95
+ });
96
+ },
97
+
98
+ // ========== Invoice Events ==========
99
+
100
+ /**
101
+ * invoice.created
102
+ * Fired when invoice is created
103
+ */
104
+ "invoice.created": async (event, fastify, stripe) => {
105
+ const invoice = event.data.object;
106
+ fastify.log.info({
107
+ event: "invoice.created",
108
+ invoiceId: invoice.id,
109
+ customerId: invoice.customer,
110
+ amount: invoice.amount_due,
111
+ status: invoice.status,
112
+ });
113
+ },
114
+
115
+ /**
116
+ * invoice.finalized
117
+ * Fired when invoice is finalized and ready to pay
118
+ */
119
+ "invoice.finalized": async (event, fastify, stripe) => {
120
+ const invoice = event.data.object;
121
+ fastify.log.info({
122
+ event: "invoice.finalized",
123
+ invoiceId: invoice.id,
124
+ customerId: invoice.customer,
125
+ amount: invoice.amount_due,
126
+ });
127
+ },
128
+
129
+ /**
130
+ * invoice.paid
131
+ * Fired when invoice payment succeeds
132
+ */
133
+ "invoice.paid": async (event, fastify, stripe) => {
134
+ const invoice = event.data.object;
135
+ fastify.log.info({
136
+ event: "invoice.paid",
137
+ invoiceId: invoice.id,
138
+ customerId: invoice.customer,
139
+ subscriptionId: invoice.subscription,
140
+ amount: invoice.amount_paid,
141
+ });
142
+ },
143
+
144
+ /**
145
+ * invoice.payment_failed
146
+ * Fired when invoice payment fails
147
+ */
148
+ "invoice.payment_failed": async (event, fastify, stripe) => {
149
+ const invoice = event.data.object;
150
+ fastify.log.warn({
151
+ event: "invoice.payment_failed",
152
+ invoiceId: invoice.id,
153
+ customerId: invoice.customer,
154
+ subscriptionId: invoice.subscription,
155
+ amount: invoice.amount_due,
156
+ attemptCount: invoice.attempt_count,
157
+ });
158
+ },
159
+
160
+ /**
161
+ * invoice.upcoming
162
+ * Fired a few days before invoice is created
163
+ */
164
+ "invoice.upcoming": async (event, fastify, stripe) => {
165
+ const invoice = event.data.object;
166
+ fastify.log.info({
167
+ event: "invoice.upcoming",
168
+ customerId: invoice.customer,
169
+ subscriptionId: invoice.subscription,
170
+ amount: invoice.amount_due,
171
+ periodEnd: invoice.period_end,
172
+ });
173
+ },
174
+
175
+ // ========== Payment Events ==========
176
+
177
+ /**
178
+ * payment_intent.succeeded
179
+ * Fired when payment succeeds
180
+ */
181
+ "payment_intent.succeeded": async (event, fastify, stripe) => {
182
+ const paymentIntent = event.data.object;
183
+ fastify.log.info({
184
+ event: "payment_intent.succeeded",
185
+ paymentIntentId: paymentIntent.id,
186
+ customerId: paymentIntent.customer,
187
+ amount: paymentIntent.amount,
188
+ currency: paymentIntent.currency,
189
+ });
190
+ },
191
+
192
+ /**
193
+ * payment_intent.payment_failed
194
+ * Fired when payment fails
195
+ */
196
+ "payment_intent.payment_failed": async (event, fastify, stripe) => {
197
+ const paymentIntent = event.data.object;
198
+ fastify.log.warn({
199
+ event: "payment_intent.payment_failed",
200
+ paymentIntentId: paymentIntent.id,
201
+ customerId: paymentIntent.customer,
202
+ amount: paymentIntent.amount,
203
+ lastPaymentError: paymentIntent.last_payment_error?.message,
204
+ });
205
+ },
206
+
207
+ // ========== Customer Events ==========
208
+
209
+ /**
210
+ * customer.created
211
+ * Fired when customer is created
212
+ */
213
+ "customer.created": async (event, fastify, stripe) => {
214
+ const customer = event.data.object;
215
+ fastify.log.info({
216
+ event: "customer.created",
217
+ customerId: customer.id,
218
+ email: customer.email,
219
+ });
220
+ },
221
+
222
+ /**
223
+ * customer.updated
224
+ * Fired when customer is updated
225
+ */
226
+ "customer.updated": async (event, fastify, stripe) => {
227
+ const customer = event.data.object;
228
+ const previousAttributes = event.data.previous_attributes;
229
+ fastify.log.info({
230
+ event: "customer.updated",
231
+ customerId: customer.id,
232
+ changes: previousAttributes,
233
+ });
234
+ },
235
+
236
+ /**
237
+ * customer.deleted
238
+ * Fired when customer is deleted
239
+ */
240
+ "customer.deleted": async (event, fastify, stripe) => {
241
+ const customer = event.data.object;
242
+ fastify.log.info({
243
+ event: "customer.deleted",
244
+ customerId: customer.id,
245
+ });
246
+ },
247
+
248
+ // ========== Payment Method Events ==========
249
+
250
+ /**
251
+ * payment_method.attached
252
+ * Fired when payment method is attached to customer
253
+ */
254
+ "payment_method.attached": async (event, fastify, stripe) => {
255
+ const paymentMethod = event.data.object;
256
+ fastify.log.info({
257
+ event: "payment_method.attached",
258
+ paymentMethodId: paymentMethod.id,
259
+ customerId: paymentMethod.customer,
260
+ type: paymentMethod.type,
261
+ });
262
+ },
263
+
264
+ /**
265
+ * payment_method.detached
266
+ * Fired when payment method is detached from customer
267
+ */
268
+ "payment_method.detached": async (event, fastify, stripe) => {
269
+ const paymentMethod = event.data.object;
270
+ fastify.log.info({
271
+ event: "payment_method.detached",
272
+ paymentMethodId: paymentMethod.id,
273
+ type: paymentMethod.type,
274
+ });
275
+ },
276
+
277
+ // ========== Checkout Events ==========
278
+
279
+ /**
280
+ * checkout.session.completed
281
+ * Fired when checkout session is completed
282
+ */
283
+ "checkout.session.completed": async (event, fastify, stripe) => {
284
+ const session = event.data.object;
285
+ fastify.log.info({
286
+ event: "checkout.session.completed",
287
+ sessionId: session.id,
288
+ customerId: session.customer,
289
+ subscriptionId: session.subscription,
290
+ mode: session.mode,
291
+ paymentStatus: session.payment_status,
292
+ });
293
+ },
294
+
295
+ /**
296
+ * checkout.session.expired
297
+ * Fired when checkout session expires
298
+ */
299
+ "checkout.session.expired": async (event, fastify, stripe) => {
300
+ const session = event.data.object;
301
+ fastify.log.info({
302
+ event: "checkout.session.expired",
303
+ sessionId: session.id,
304
+ });
305
+ },
306
+
307
+ // ========== Charge Events ==========
308
+
309
+ /**
310
+ * charge.succeeded
311
+ * Fired when a charge succeeds
312
+ */
313
+ "charge.succeeded": async (event, fastify, stripe) => {
314
+ const charge = event.data.object;
315
+ fastify.log.info({
316
+ event: "charge.succeeded",
317
+ chargeId: charge.id,
318
+ customerId: charge.customer,
319
+ amount: charge.amount,
320
+ currency: charge.currency,
321
+ paymentMethod: charge.payment_method_details?.type,
322
+ });
323
+ },
324
+
325
+ /**
326
+ * charge.failed
327
+ * Fired when a charge fails
328
+ */
329
+ "charge.failed": async (event, fastify, stripe) => {
330
+ const charge = event.data.object;
331
+ fastify.log.error({
332
+ event: "charge.failed",
333
+ chargeId: charge.id,
334
+ customerId: charge.customer,
335
+ amount: charge.amount,
336
+ failureCode: charge.failure_code,
337
+ failureMessage: charge.failure_message,
338
+ });
339
+ },
340
+
341
+ /**
342
+ * charge.refunded
343
+ * Fired when a charge is refunded
344
+ */
345
+ "charge.refunded": async (event, fastify, stripe) => {
346
+ const charge = event.data.object;
347
+ fastify.log.info({
348
+ event: "charge.refunded",
349
+ chargeId: charge.id,
350
+ customerId: charge.customer,
351
+ amountRefunded: charge.amount_refunded,
352
+ refundCount: charge.refunds?.data?.length || 0,
353
+ });
354
+ },
355
+ };
@@ -0,0 +1,278 @@
1
+ // src/handlers/exampleHandlers.js
2
+
3
+ /**
4
+ * Example custom handlers showing how to override default handlers
5
+ * and implement business logic for subscription events
6
+ */
7
+
8
+ export const exampleHandlers = {
9
+ /**
10
+ * Handle new subscription - example with database update
11
+ */
12
+ "customer.subscription.created": async (event, fastify, stripe) => {
13
+ const subscription = event.data.object;
14
+
15
+ // Example: Update user in database
16
+ // if (fastify.prisma) {
17
+ // await fastify.prisma.user.update({
18
+ // where: { stripeCustomerId: subscription.customer },
19
+ // data: {
20
+ // subscriptionId: subscription.id,
21
+ // subscriptionStatus: subscription.status,
22
+ // planId: subscription.items.data[0]?.price.id,
23
+ // subscriptionStartDate: new Date(subscription.current_period_start * 1000),
24
+ // subscriptionEndDate: new Date(subscription.current_period_end * 1000),
25
+ // },
26
+ // });
27
+ // }
28
+
29
+ fastify.log.info({
30
+ msg: "New subscription created",
31
+ customerId: subscription.customer,
32
+ subscriptionId: subscription.id,
33
+ plan: subscription.items.data[0]?.price.product,
34
+ });
35
+ },
36
+
37
+ /**
38
+ * Handle subscription updates - track plan changes, status changes
39
+ */
40
+ "customer.subscription.updated": async (event, fastify, stripe) => {
41
+ const subscription = event.data.object;
42
+ const previousAttributes = event.data.previous_attributes || {};
43
+
44
+ // Check what changed
45
+ const statusChanged = "status" in previousAttributes;
46
+ const planChanged = "items" in previousAttributes;
47
+
48
+ if (statusChanged) {
49
+ fastify.log.info({
50
+ msg: "Subscription status changed",
51
+ subscriptionId: subscription.id,
52
+ oldStatus: previousAttributes.status,
53
+ newStatus: subscription.status,
54
+ });
55
+
56
+ // Handle specific status changes
57
+ if (subscription.status === "active" && previousAttributes.status === "trialing") {
58
+ // Trial converted to paid subscription
59
+ fastify.log.info("Trial converted to paid subscription");
60
+ }
61
+
62
+ if (subscription.status === "past_due") {
63
+ // Send email to customer about failed payment
64
+ fastify.log.warn("Subscription past due - payment failed");
65
+ }
66
+ }
67
+
68
+ if (planChanged) {
69
+ fastify.log.info({
70
+ msg: "Subscription plan changed",
71
+ subscriptionId: subscription.id,
72
+ });
73
+ }
74
+
75
+ // Example: Update database
76
+ // if (fastify.prisma) {
77
+ // await fastify.prisma.user.update({
78
+ // where: { stripeSubscriptionId: subscription.id },
79
+ // data: {
80
+ // subscriptionStatus: subscription.status,
81
+ // planId: subscription.items.data[0]?.price.id,
82
+ // subscriptionEndDate: new Date(subscription.current_period_end * 1000),
83
+ // },
84
+ // });
85
+ // }
86
+ },
87
+
88
+ /**
89
+ * Handle subscription cancellation
90
+ */
91
+ "customer.subscription.deleted": async (event, fastify, stripe) => {
92
+ const subscription = event.data.object;
93
+
94
+ fastify.log.info({
95
+ msg: "Subscription canceled",
96
+ subscriptionId: subscription.id,
97
+ customerId: subscription.customer,
98
+ canceledAt: new Date(subscription.canceled_at * 1000),
99
+ });
100
+
101
+ // Example: Update user access
102
+ // if (fastify.prisma) {
103
+ // await fastify.prisma.user.update({
104
+ // where: { stripeSubscriptionId: subscription.id },
105
+ // data: {
106
+ // subscriptionStatus: "canceled",
107
+ // subscriptionId: null,
108
+ // canceledAt: new Date(),
109
+ // },
110
+ // });
111
+ // }
112
+
113
+ // Example: Send cancellation email
114
+ // if (fastify.email) {
115
+ // const customer = await stripe.customers.retrieve(subscription.customer);
116
+ // await fastify.email.send(
117
+ // customer.email,
118
+ // "We're sorry to see you go",
119
+ // "<p>Your subscription has been canceled.</p>"
120
+ // );
121
+ // }
122
+ },
123
+
124
+ /**
125
+ * Handle trial ending soon
126
+ */
127
+ "customer.subscription.trial_will_end": async (event, fastify, stripe) => {
128
+ const subscription = event.data.object;
129
+ const trialEndDate = new Date(subscription.trial_end * 1000);
130
+
131
+ fastify.log.info({
132
+ msg: "Trial ending soon",
133
+ subscriptionId: subscription.id,
134
+ trialEnd: trialEndDate,
135
+ });
136
+
137
+ // Example: Send reminder email
138
+ // const customer = await stripe.customers.retrieve(subscription.customer);
139
+ // if (fastify.email && customer.email) {
140
+ // await fastify.email.send(
141
+ // customer.email,
142
+ // "Your trial is ending soon",
143
+ // `<p>Your trial ends on ${trialEndDate.toLocaleDateString()}</p>`
144
+ // );
145
+ // }
146
+ },
147
+
148
+ /**
149
+ * Handle successful payment
150
+ */
151
+ "invoice.paid": async (event, fastify, stripe) => {
152
+ const invoice = event.data.object;
153
+
154
+ fastify.log.info({
155
+ msg: "Invoice paid successfully",
156
+ invoiceId: invoice.id,
157
+ customerId: invoice.customer,
158
+ amount: invoice.amount_paid / 100, // Convert from cents
159
+ currency: invoice.currency,
160
+ });
161
+
162
+ // Reset failed payment count
163
+ // if (fastify.prisma && invoice.subscription) {
164
+ // await fastify.prisma.user.update({
165
+ // where: { stripeSubscriptionId: invoice.subscription },
166
+ // data: {
167
+ // failedPaymentCount: 0,
168
+ // lastPaymentDate: new Date(),
169
+ // },
170
+ // });
171
+ // }
172
+
173
+ // Send receipt email
174
+ // const customer = await stripe.customers.retrieve(invoice.customer);
175
+ // if (fastify.email && customer.email) {
176
+ // await fastify.email.send(
177
+ // customer.email,
178
+ // "Payment Receipt",
179
+ // `<p>Thank you for your payment of $${invoice.amount_paid / 100}</p>`
180
+ // );
181
+ // }
182
+ },
183
+
184
+ /**
185
+ * Handle failed payment
186
+ */
187
+ "invoice.payment_failed": async (event, fastify, stripe) => {
188
+ const invoice = event.data.object;
189
+
190
+ fastify.log.error({
191
+ msg: "Invoice payment failed",
192
+ invoiceId: invoice.id,
193
+ customerId: invoice.customer,
194
+ attemptCount: invoice.attempt_count,
195
+ nextPaymentAttempt: invoice.next_payment_attempt,
196
+ });
197
+
198
+ // Track failed payments
199
+ // if (fastify.prisma && invoice.subscription) {
200
+ // await fastify.prisma.user.update({
201
+ // where: { stripeSubscriptionId: invoice.subscription },
202
+ // data: {
203
+ // failedPaymentCount: { increment: 1 },
204
+ // lastFailedPaymentDate: new Date(),
205
+ // },
206
+ // });
207
+ // }
208
+
209
+ // Send payment failure email
210
+ // const customer = await stripe.customers.retrieve(invoice.customer);
211
+ // if (fastify.email && customer.email) {
212
+ // await fastify.email.send(
213
+ // customer.email,
214
+ // "Payment Failed",
215
+ // "<p>Your payment failed. Please update your payment method.</p>"
216
+ // );
217
+ // }
218
+ },
219
+
220
+ /**
221
+ * Handle upcoming invoice (send reminder)
222
+ */
223
+ "invoice.upcoming": async (event, fastify, stripe) => {
224
+ const invoice = event.data.object;
225
+ const billingDate = new Date(invoice.period_end * 1000);
226
+
227
+ fastify.log.info({
228
+ msg: "Upcoming invoice",
229
+ customerId: invoice.customer,
230
+ amount: invoice.amount_due / 100,
231
+ billingDate,
232
+ });
233
+
234
+ // Send upcoming charge notification
235
+ // const customer = await stripe.customers.retrieve(invoice.customer);
236
+ // if (fastify.email && customer.email) {
237
+ // await fastify.email.send(
238
+ // customer.email,
239
+ // "Upcoming Charge Notification",
240
+ // `<p>Your card will be charged $${invoice.amount_due / 100} on ${billingDate.toLocaleDateString()}</p>`
241
+ // );
242
+ // }
243
+ },
244
+
245
+ /**
246
+ * Handle completed checkout session
247
+ */
248
+ "checkout.session.completed": async (event, fastify, stripe) => {
249
+ const session = event.data.object;
250
+
251
+ if (session.mode === "subscription") {
252
+ fastify.log.info({
253
+ msg: "Checkout completed - subscription",
254
+ sessionId: session.id,
255
+ customerId: session.customer,
256
+ subscriptionId: session.subscription,
257
+ });
258
+
259
+ // Provision access to user
260
+ // if (fastify.prisma) {
261
+ // await fastify.prisma.user.update({
262
+ // where: { stripeCustomerId: session.customer },
263
+ // data: {
264
+ // subscriptionId: session.subscription,
265
+ // onboardingComplete: true,
266
+ // },
267
+ // });
268
+ // }
269
+ } else if (session.mode === "payment") {
270
+ fastify.log.info({
271
+ msg: "Checkout completed - one-time payment",
272
+ sessionId: session.id,
273
+ customerId: session.customer,
274
+ paymentIntentId: session.payment_intent,
275
+ });
276
+ }
277
+ },
278
+ };
@@ -0,0 +1,8 @@
1
+ // src/handlers/index.js
2
+
3
+ /**
4
+ * Export all handlers for easy importing
5
+ */
6
+
7
+ export { defaultHandlers } from './defaultHandlers.js';
8
+ export { exampleHandlers } from './exampleHandlers.js';
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // src/index.js
2
+
3
+ /**
4
+ * Main exports for xStripe plugin
5
+ */
6
+
7
+ export { default } from './xStripe.js';
8
+ export { default as xStripe } from './xStripe.js';
9
+ export * from './handlers/index.js';
10
+ export * as helpers from './utils/helpers.js';