@@ -22,6 +22,7 @@ import {
2222 DatabaseInsertError ,
2323 InternalServerError ,
2424 UnauthenticatedError ,
25+ ValidationError ,
2526} from "common/errors/index.js" ;
2627import { Modules } from "common/modules.js" ;
2728import { AppRoles } from "common/roles.js" ;
@@ -32,6 +33,7 @@ import {
3233} from "common/types/stripe.js" ;
3334import { FastifyPluginAsync } from "fastify" ;
3435import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi" ;
36+ import stripe , { Stripe } from "stripe" ;
3537
3638const 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
170260export default stripeRoutes ;
0 commit comments