Skip to content

Commit e425a99

Browse files
committed
stripe webhook for payment link recognition
1 parent 874a882 commit e425a99

File tree

1 file changed

+90
-0
lines changed

1 file changed

+90
-0
lines changed

src/api/routes/stripe.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
DatabaseInsertError,
2323
InternalServerError,
2424
UnauthenticatedError,
25+
ValidationError,
2526
} from "common/errors/index.js";
2627
import { Modules } from "common/modules.js";
2728
import { AppRoles } from "common/roles.js";
@@ -32,6 +33,7 @@ import {
3233
} from "common/types/stripe.js";
3334
import { FastifyPluginAsync } from "fastify";
3435
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
36+
import stripe, { Stripe } from "stripe";
3537

3638
const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
3739
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
@@ -165,6 +167,94 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
165167
reply.status(201).send({ id: linkId, link: url });
166168
},
167169
);
170+
fastify.post(
171+
"/stripe",
172+
{
173+
config: { rawBody: true },
174+
schema: withTags(["Stripe"], {
175+
summary:
176+
"Stripe webhook handler to track when Stripe payment links are used.",
177+
hide: true,
178+
}),
179+
},
180+
async (request, reply) => {
181+
let event: Stripe.Event;
182+
if (!request.rawBody) {
183+
throw new ValidationError({ message: "Could not get raw body." });
184+
}
185+
try {
186+
const sig = request.headers["stripe-signature"];
187+
if (!sig || typeof sig !== "string") {
188+
throw new Error("Missing or invalid Stripe signature");
189+
}
190+
const secretApiConfig =
191+
(await getSecretValue(
192+
fastify.secretsManagerClient,
193+
genericConfig.ConfigSecretName,
194+
)) || {};
195+
if (!secretApiConfig) {
196+
throw new InternalServerError({
197+
message: "Could not connect to Stripe.",
198+
});
199+
}
200+
event = stripe.webhooks.constructEvent(
201+
request.rawBody,
202+
sig,
203+
secretApiConfig.stripe_endpoint_secret as string,
204+
);
205+
} catch (err: unknown) {
206+
if (err instanceof BaseError) {
207+
throw err;
208+
}
209+
throw new ValidationError({
210+
message: "Stripe webhook could not be validated.",
211+
});
212+
}
213+
switch (event.type) {
214+
case "checkout.session.completed":
215+
if (event.data.object.payment_link) {
216+
const paymentLinkId = event.data.object.payment_link.toString();
217+
if (!paymentLinkId) {
218+
return reply
219+
.code(200)
220+
.send({ handled: false, requestId: request.id });
221+
}
222+
const response = await fastify.dynamoClient.send(
223+
new QueryCommand({
224+
TableName: genericConfig.StripeLinksDynamoTableName,
225+
IndexName: "LinkIdIndex",
226+
KeyConditionExpression: "linkId = :linkId",
227+
ExpressionAttributeValues: {
228+
":linkId": { S: paymentLinkId },
229+
},
230+
}),
231+
);
232+
if (!response) {
233+
throw new DatabaseFetchError({
234+
message: "Could not check for payment link in table.",
235+
});
236+
}
237+
if (!response.Count || response.Count !== 1) {
238+
return reply.status(200).send({
239+
handled: false,
240+
requestId: request.id,
241+
});
242+
}
243+
return reply.status(200).send({
244+
handled: true,
245+
requestId: request.id,
246+
});
247+
}
248+
return reply
249+
.code(200)
250+
.send({ handled: false, requestId: request.id });
251+
252+
default:
253+
request.log.warn(`Unhandled event type: ${event.type}`);
254+
}
255+
return reply.code(200).send({ handled: false, requestId: request.id });
256+
},
257+
);
168258
};
169259

170260
export default stripeRoutes;

0 commit comments

Comments
 (0)