@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.
- package/.dockerignore +62 -0
- package/.env.example +116 -0
- package/API.md +574 -0
- package/CHANGELOG.md +96 -0
- package/EXAMPLES.md +883 -0
- package/LICENSE +15 -0
- package/MIGRATION.md +374 -0
- package/QUICK_START.md +179 -0
- package/README.md +331 -0
- package/SECURITY.md +465 -0
- package/TESTING.md +357 -0
- package/index.d.ts +309 -0
- package/package.json +53 -0
- package/server/app.js +557 -0
- package/src/handlers/defaultHandlers.js +355 -0
- package/src/handlers/exampleHandlers.js +278 -0
- package/src/handlers/index.js +8 -0
- package/src/index.js +10 -0
- package/src/utils/helpers.js +220 -0
- package/src/webhooks/webhooks.js +72 -0
- package/src/xStripe.js +45 -0
- package/test/handlers.test.js +959 -0
- package/test/xStripe.integration.test.js +409 -0
|
@@ -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
|
+
};
|
package/src/index.js
ADDED