Skip to content

Commit 85735e0

Browse files
committed
payment failed webhook
1 parent 02541da commit 85735e0

File tree

1 file changed

+116
-4
lines changed

1 file changed

+116
-4
lines changed

src/api/routes/stripe.ts

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,120 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
333333
});
334334
}
335335
switch (event.type) {
336+
case "checkout.session.async_payment_failed":
337+
if (event.data.object.payment_link) {
338+
const eventId = event.id;
339+
const paymentAmount = event.data.object.amount_total;
340+
const paymentCurrency = event.data.object.currency;
341+
const { email, name } = event.data.object.customer_details || {
342+
email: null,
343+
name: null,
344+
};
345+
const paymentLinkId = event.data.object.payment_link.toString();
346+
if (!paymentLinkId || !paymentCurrency || !paymentAmount) {
347+
request.log.info("Missing required fields.");
348+
return reply
349+
.code(200)
350+
.send({ handled: false, requestId: request.id });
351+
}
352+
const response = await fastify.dynamoClient.send(
353+
new QueryCommand({
354+
TableName: genericConfig.StripeLinksDynamoTableName,
355+
IndexName: "LinkIdIndex",
356+
KeyConditionExpression: "linkId = :linkId",
357+
ExpressionAttributeValues: {
358+
":linkId": { S: paymentLinkId },
359+
},
360+
}),
361+
);
362+
if (!response) {
363+
throw new DatabaseFetchError({
364+
message: "Could not check for payment link in table.",
365+
});
366+
}
367+
if (!response.Items || response.Items?.length !== 1) {
368+
return reply.status(200).send({
369+
handled: false,
370+
requestId: request.id,
371+
});
372+
}
373+
const unmarshalledEntry = unmarshall(response.Items[0]) as {
374+
userId: string;
375+
invoiceId: string;
376+
amount?: number;
377+
priceId?: string;
378+
productId?: string;
379+
};
380+
if (!unmarshalledEntry.userId || !unmarshalledEntry.invoiceId) {
381+
return reply.status(200).send({
382+
handled: false,
383+
requestId: request.id,
384+
});
385+
}
386+
const paidInFull = paymentAmount === unmarshalledEntry.amount;
387+
const withCurrency = new Intl.NumberFormat("en-US", {
388+
style: "currency",
389+
currency: paymentCurrency.toUpperCase(),
390+
})
391+
.formatToParts(paymentAmount / 100)
392+
.map((val) => val.value)
393+
.join("");
394+
395+
// Notify link owner of failed payment
396+
let queueId;
397+
if (event.data.object.payment_status === "unpaid") {
398+
request.log.info(
399+
`Failed payment of ${withCurrency} by ${name} (${email}) for payment link ${paymentLinkId} invoice ID ${unmarshalledEntry.invoiceId}).`,
400+
);
401+
if (unmarshalledEntry.userId.includes("@")) {
402+
request.log.info(
403+
`Sending email to ${unmarshalledEntry.userId}...`,
404+
);
405+
const sqsPayload: SQSPayload<AvailableSQSFunctions.EmailNotifications> =
406+
{
407+
function: AvailableSQSFunctions.EmailNotifications,
408+
metadata: {
409+
initiator: eventId,
410+
reqId: request.id,
411+
},
412+
payload: {
413+
to: [unmarshalledEntry.userId],
414+
subject: `Payment Failed for Invoice ${unmarshalledEntry.invoiceId}`,
415+
content: `
416+
A ${paidInFull ? "full" : "partial"} payment for Invoice ${unmarshalledEntry.invoiceId} (${withCurrency} paid by ${name}, ${email}) <b>has failed.</b>
417+
418+
Please ask the payee to try again, perhaps with a different payment method, or contact Officer Board.
419+
`,
420+
callToActionButton: {
421+
name: "View Your Stripe Links",
422+
url: `${fastify.environmentConfig.UserFacingUrl}/stripe`,
423+
},
424+
},
425+
};
426+
if (!fastify.sqsClient) {
427+
fastify.sqsClient = new SQSClient({
428+
region: genericConfig.AwsRegion,
429+
});
430+
}
431+
const result = await fastify.sqsClient.send(
432+
new SendMessageCommand({
433+
QueueUrl: fastify.environmentConfig.SqsQueueUrl,
434+
MessageBody: JSON.stringify(sqsPayload),
435+
}),
436+
);
437+
queueId = result.MessageId || "";
438+
}
439+
}
440+
441+
return reply.status(200).send({
442+
handled: true,
443+
requestId: request.id,
444+
queueId: queueId || "",
445+
});
446+
}
447+
return reply
448+
.code(200)
449+
.send({ handled: false, requestId: request.id });
336450
case "checkout.session.async_payment_succeeded":
337451
case "checkout.session.completed":
338452
if (event.data.object.payment_link) {
@@ -416,7 +530,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
416530
content: `
417531
ACM @ UIUC has received intent of ${paidInFull ? "full" : "partial"} payment for Invoice ${unmarshalledEntry.invoiceId} (${withCurrency} paid by ${name}, ${email}).
418532
419-
The payee has used a payment method which does not settle funds immediately. Therefore, ACM @ UIUC is still waiting for funds to settle and no services should be performed until the funds settle.
533+
The payee has used a payment method which does not settle funds immediately. Therefore, ACM @ UIUC is still waiting for funds to settle and <b>no services should be performed until the funds settle.</b>
420534
421535
Please contact Officer Board with any questions.
422536
`,
@@ -459,9 +573,7 @@ Please contact Officer Board with any questions.
459573
subject: `Payment Recieved for Invoice ${unmarshalledEntry.invoiceId}`,
460574
content: `
461575
ACM @ UIUC has received ${paidInFull ? "full" : "partial"} payment for Invoice ${unmarshalledEntry.invoiceId} (${withCurrency} paid by ${name}, ${email}).
462-
463-
This invoice should now be considered settled.
464-
576+
${paidInFull ? "\nThis invoice should now be considered settled.\n" : ""}
465577
Please contact Officer Board with any questions.`,
466578
callToActionButton: {
467579
name: "View Your Stripe Links",

0 commit comments

Comments
 (0)