|
3 | 3 | ScanCommand, |
4 | 4 | TransactWriteItemsCommand, |
5 | 5 | UpdateItemCommand, |
| 6 | + PutItemCommand, |
6 | 7 | } from "@aws-sdk/client-dynamodb"; |
7 | 8 | import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; |
8 | 9 | import { withRoles, withTags } from "api/components/index.js"; |
@@ -715,7 +716,88 @@ Please contact Officer Board with any questions.`, |
715 | 716 | return reply |
716 | 717 | .code(200) |
717 | 718 | .send({ handled: false, requestId: request.id }); |
| 719 | + case "payment_intent.succeeded": { |
| 720 | + const intent = event.data.object as Stripe.PaymentIntent; |
718 | 721 |
|
| 722 | + const amount = intent.amount_received; |
| 723 | + const currency = intent.currency; |
| 724 | + const customerId = intent.customer?.toString(); |
| 725 | + const email = intent.receipt_email ?? intent.metadata?.billing_email; |
| 726 | + const acmOrg = intent.metadata?.acm_org; |
| 727 | + |
| 728 | + if (!customerId) { |
| 729 | + request.log.info("Skipping payment intent with no customer ID."); |
| 730 | + return reply |
| 731 | + .code(200) |
| 732 | + .send({ handled: false, requestId: request.id }); |
| 733 | + } |
| 734 | + |
| 735 | + if (!email) { |
| 736 | + request.log.warn("Missing email for payment intent."); |
| 737 | + return reply |
| 738 | + .code(200) |
| 739 | + .send({ handled: false, requestId: request.id }); |
| 740 | + } |
| 741 | + |
| 742 | + if (!acmOrg) { |
| 743 | + request.log.warn("Missing acm_org for payment intent."); |
| 744 | + return reply |
| 745 | + .code(200) |
| 746 | + .send({ handled: false, requestId: request.id }); |
| 747 | + } |
| 748 | + |
| 749 | + const normalizedEmail = email.trim(); |
| 750 | + if (!normalizedEmail.includes("@")) { |
| 751 | + request.log.warn("Invalid email format for payment intent."); |
| 752 | + return reply |
| 753 | + .code(200) |
| 754 | + .send({ handled: false, requestId: request.id }); |
| 755 | + } |
| 756 | + const [, domainPart] = normalizedEmail.split("@"); |
| 757 | + if (!domainPart) { |
| 758 | + request.log.warn( |
| 759 | + "Could not derive email domain for payment intent.", |
| 760 | + ); |
| 761 | + return reply |
| 762 | + .code(200) |
| 763 | + .send({ handled: false, requestId: request.id }); |
| 764 | + } |
| 765 | + const domain = domainPart.toLowerCase(); |
| 766 | + |
| 767 | + try { |
| 768 | + await fastify.dynamoClient.send( |
| 769 | + new PutItemCommand({ |
| 770 | + TableName: genericConfig.StripePaymentsDynamoTableName, |
| 771 | + Item: marshall({ |
| 772 | + primaryKey: `${acmOrg}#${domain}`, |
| 773 | + sortKey: event.id, |
| 774 | + amount, |
| 775 | + currency, |
| 776 | + status: "succeeded", |
| 777 | + billingEmail: normalizedEmail, |
| 778 | + createdAt: new Date().toISOString(), |
| 779 | + eventId: event.id, |
| 780 | + }), |
| 781 | + }), |
| 782 | + ); |
| 783 | + |
| 784 | + request.log.info( |
| 785 | + `Recorded successful payment ${intent.id} from ${normalizedEmail} (${amount} ${currency})`, |
| 786 | + ); |
| 787 | + |
| 788 | + return reply |
| 789 | + .status(200) |
| 790 | + .send({ handled: true, requestId: request.id }); |
| 791 | + } catch (e) { |
| 792 | + if (e instanceof BaseError) { |
| 793 | + throw e; |
| 794 | + } |
| 795 | + request.log.error(e); |
| 796 | + throw new DatabaseInsertError({ |
| 797 | + message: `Could not insert Stripe payment record: ${(e as Error).message}`, |
| 798 | + }); |
| 799 | + } |
| 800 | + } |
719 | 801 | default: |
720 | 802 | request.log.warn(`Unhandled event type: ${event.type}`); |
721 | 803 | } |
|
0 commit comments