Skip to content

Commit 922d017

Browse files
Stripe webhook updates for ACH transfer (#357)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Automatic recording and acknowledgement of successful Stripe payments (captures amount, currency, status, billing email, timestamp, and event identifier). * **Chores** * Added a configuration option to specify where Stripe payment records are stored. * **Continued** * Continued support for bank transfer funding and automatic tracking of bank-transfer transactions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent d139d0f commit 922d017

File tree

2 files changed

+84
-0
lines changed

2 files changed

+84
-0
lines changed

src/api/routes/stripe.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ScanCommand,
44
TransactWriteItemsCommand,
55
UpdateItemCommand,
6+
PutItemCommand,
67
} from "@aws-sdk/client-dynamodb";
78
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
89
import { withRoles, withTags } from "api/components/index.js";
@@ -715,7 +716,88 @@ Please contact Officer Board with any questions.`,
715716
return reply
716717
.code(200)
717718
.send({ handled: false, requestId: request.id });
719+
case "payment_intent.succeeded": {
720+
const intent = event.data.object as Stripe.PaymentIntent;
718721

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+
}
719801
default:
720802
request.log.warn(`Unhandled event type: ${event.type}`);
721803
}

src/common/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export type GenericConfigType = {
4141
CacheDynamoTableName: string;
4242
LinkryDynamoTableName: string;
4343
StripeLinksDynamoTableName: string;
44+
StripePaymentsDynamoTableName: string;
4445
EntraSecretName: string;
4546
UpcomingEventThresholdSeconds: number;
4647
AwsRegion: string;
@@ -84,6 +85,7 @@ export const commChairsGroupId = "105e7d32-7289-435e-a67a-552c7f215507";
8485
const genericConfig: GenericConfigType = {
8586
EventsDynamoTableName: "infra-core-api-events",
8687
StripeLinksDynamoTableName: "infra-core-api-stripe-links",
88+
StripePaymentsDynamoTableName: "infra-core-api-stripe-payments",
8789
CacheDynamoTableName: "infra-core-api-cache",
8890
LinkryDynamoTableName: "infra-core-api-linkry",
8991
EntraSecretName: "infra-core-api-entra",

0 commit comments

Comments
 (0)