-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathindex.ts
150 lines (136 loc) · 3.78 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { DBAdapter } from '@cardstack/runtime-common';
import { handlePaymentSucceeded } from './payment-succeeded';
import { handleCheckoutSessionCompleted } from './checkout-session-completed';
import Stripe from 'stripe';
import { handleSubscriptionDeleted } from './subscription-deleted';
export type StripeEvent = {
id: string;
type: string;
data: {
object: {
id: string;
[key: string]: any;
};
};
};
export type StripeInvoicePaymentSucceededWebhookEvent = StripeEvent & {
object: 'event';
type: 'invoice.payment_succeeded';
data: {
object: {
id: string;
object: 'invoice';
amount_paid: number;
billing_reason:
| 'subscription_create'
| 'subscription_cycle'
| 'subscription_update';
period_start: number;
period_end: number;
subscription: string;
customer: string;
lines: {
data: Array<{
amount: number;
description: string;
price: {
product: string;
};
period: {
start: number;
end: number;
};
}>;
};
};
};
};
export type StripeSubscriptionDeletedWebhookEvent = StripeEvent & {
object: 'event';
type: 'customer.subscription.deleted';
data: {
object: {
id: string; // stripe subscription id
canceled_at: number;
current_period_end: number;
current_period_start: number;
customer: string;
cancellation_details: {
comment: string | null;
feedback: string;
reason:
| 'cancellation_requested'
| 'payment_failure'
| 'payment_disputed';
};
};
};
};
export type StripeCheckoutSessionCompletedWebhookEvent = StripeEvent & {
object: 'event';
type: 'checkout.session.completed';
data: {
object: {
id: string;
object: 'checkout.session';
client_reference_id: string;
customer: string;
metadata:
| {
credit_reload_amount: string;
}
| {};
};
};
};
// Make sure Stripe customer portal is configured with the following settings:
// Cancel at end of billing period: CHECKED
// Customers can switch plans: CHECKED
// Prorate subscription changes: CHECKED
// Invoice immediately (when prorating): CHECKED
// When switching to a cheaper subscription -> WAIT UNTIL END OF BILLING PERIOD TO UPDATE
export default async function stripeWebhookHandler(
dbAdapter: DBAdapter,
request: Request,
): Promise<Response> {
let signature = request.headers.get('stripe-signature');
if (!signature) {
throw new Error('No Stripe signature found in request headers');
}
if (!process.env.STRIPE_WEBHOOK_SECRET) {
throw new Error('STRIPE_WEBHOOK_SECRET is not set');
}
let event: StripeEvent;
try {
event = Stripe.webhooks.constructEvent(
await request.text(),
signature,
process.env.STRIPE_WEBHOOK_SECRET,
) as StripeEvent;
} catch (error) {
throw new Error(`Error verifying webhook signature: ${error}`);
}
let type = event.type;
switch (type) {
// These handlers should eventually become jobs which workers will process asynchronously
case 'invoice.payment_succeeded':
await handlePaymentSucceeded(
dbAdapter,
event as StripeInvoicePaymentSucceededWebhookEvent,
);
break;
case 'customer.subscription.deleted': // canceled by the user, or expired due to payment failure, or payment dispute
await handleSubscriptionDeleted(
dbAdapter,
event as StripeSubscriptionDeletedWebhookEvent,
);
break;
case 'checkout.session.completed':
await handleCheckoutSessionCompleted(
dbAdapter,
event as StripeCheckoutSessionCompletedWebhookEvent,
);
break;
}
return new Response('ok');
}