From 2ee6c0b6f5aa47ff5443adc7e938d808498f2764 Mon Sep 17 00:00:00 2001 From: Aditya Kshirsagar Date: Mon, 27 Oct 2025 15:12:32 -0500 Subject: [PATCH 1/5] implemented webhook handling for customer_cash_balance_transaction.created event --- src/api/routes/stripe.ts | 52 ++++++++++++++++++++++++++++++++++++++++ src/common/config.ts | 2 ++ 2 files changed, 54 insertions(+) diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index edbba46c..30856f78 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -3,6 +3,7 @@ import { ScanCommand, TransactWriteItemsCommand, UpdateItemCommand, + PutItemCommand, } from "@aws-sdk/client-dynamodb"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; import { withRoles, withTags } from "api/components/index.js"; @@ -332,6 +333,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { sig, secretApiConfig.stripe_links_endpoint_secret as string, ); + // event = JSON.parse(request.rawBody.toString()); <-- this is for testing without a stripe account via Curl } catch (err: unknown) { if (err instanceof BaseError) { throw err; @@ -715,7 +717,57 @@ Please contact Officer Board with any questions.`, return reply .code(200) .send({ handled: false, requestId: request.id }); + case "customer_cash_balance_transaction.created": { + const txn = event.data.object as any; + if (txn.funding_method === "bank_transfer") { + const customerId = txn.customer?.toString() ?? "UNKNOWN"; + const amount = txn.net_amount; + const currency = txn.currency; + const status = txn.status; + const eventId = event.id; + + request.log.info( + `Received ACH push ${status} txn ${txn.id} for ${customerId} (${amount} ${currency})`, + ); + + await fastify.dynamoClient.send( + new PutItemCommand({ + TableName: genericConfig.StripePaymentsDynamoTableName, + Item: marshall({ + primaryKey: `CUSTOMER#${customerId}`, + sortKey: `PAY#${txn.id}`, + amount, + currency, + status, + createdAt: Date.now(), + eventId, + }), + }), + ); + + // if (status === "succeeded") { + // await fastify.dynamoClient.send( + // new UpdateItemCommand({ + // TableName: genericConfig.StripePaymentsDynamoTableName, + // Key: marshall({ + // primaryKey: `CUSTOMER#${customerId}`, + // sortKey: "SUMMARY", + // }), + // UpdateExpression: "ADD totalPaid :amount SET lastUpdated = :ts", + // ExpressionAttributeValues: marshall({ + // ":amount": amount, + // ":ts": Date.now(), + // }), + // }) + // ); + // } + } + + return reply + .status(200) + .send({ handled: true, requestId: request.id }); + } default: request.log.warn(`Unhandled event type: ${event.type}`); } diff --git a/src/common/config.ts b/src/common/config.ts index eaeb17f6..e4543c14 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -40,6 +40,7 @@ export type GenericConfigType = { CacheDynamoTableName: string; LinkryDynamoTableName: string; StripeLinksDynamoTableName: string; + StripePaymentsDynamoTableName: string; EntraSecretName: string; UpcomingEventThresholdSeconds: number; AwsRegion: string; @@ -83,6 +84,7 @@ export const commChairsGroupId = "105e7d32-7289-435e-a67a-552c7f215507"; const genericConfig: GenericConfigType = { EventsDynamoTableName: "infra-core-api-events", StripeLinksDynamoTableName: "infra-core-api-stripe-links", + StripePaymentsDynamoTableName: "infra-core-api-stripe-payments", CacheDynamoTableName: "infra-core-api-cache", LinkryDynamoTableName: "infra-core-api-linkry", EntraSecretName: "infra-core-api-entra", From babe292bf10c7b27acd7cbe6077925c09d19dcb1 Mon Sep 17 00:00:00 2001 From: Aditya Kshirsagar Date: Mon, 27 Oct 2025 19:45:11 -0500 Subject: [PATCH 2/5] removed webhook secret --- src/api/routes/stripe.ts | 71 ++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index 30856f78..8e29ddcd 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -333,7 +333,6 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { sig, secretApiConfig.stripe_links_endpoint_secret as string, ); - // event = JSON.parse(request.rawBody.toString()); <-- this is for testing without a stripe account via Curl } catch (err: unknown) { if (err instanceof BaseError) { throw err; @@ -717,52 +716,38 @@ Please contact Officer Board with any questions.`, return reply .code(200) .send({ handled: false, requestId: request.id }); - case "customer_cash_balance_transaction.created": { - const txn = event.data.object as any; + case "payment_intent.succeeded": { + const intent = event.data.object as Stripe.PaymentIntent; - if (txn.funding_method === "bank_transfer") { - const customerId = txn.customer?.toString() ?? "UNKNOWN"; - const amount = txn.net_amount; - const currency = txn.currency; - const status = txn.status; - const eventId = event.id; - - request.log.info( - `Received ACH push ${status} txn ${txn.id} for ${customerId} (${amount} ${currency})`, - ); + const amount = intent.amount_received; + const currency = intent.currency; + const customerId = intent.customer?.toString() ?? "UNKNOWN"; + const email = + intent.receipt_email ?? + intent.metadata?.billing_email ?? + "unknown@example.com"; + const acmOrg = intent.metadata?.acm_org ?? "ACM@UIUC"; + const domain = email.split("@")[1] ?? "unknown.com"; - await fastify.dynamoClient.send( - new PutItemCommand({ - TableName: genericConfig.StripePaymentsDynamoTableName, - Item: marshall({ - primaryKey: `CUSTOMER#${customerId}`, - sortKey: `PAY#${txn.id}`, - amount, - currency, - status, - createdAt: Date.now(), - eventId, - }), + await fastify.dynamoClient.send( + new PutItemCommand({ + TableName: genericConfig.StripePaymentsDynamoTableName, + Item: marshall({ + primaryKey: `${acmOrg}#${domain}`, + sortKey: `customer`, + amount, + currency, + status: "succeeded", + billingEmail: email, + createdAt: Date.now(), + eventId: event.id, }), - ); + }), + ); - // if (status === "succeeded") { - // await fastify.dynamoClient.send( - // new UpdateItemCommand({ - // TableName: genericConfig.StripePaymentsDynamoTableName, - // Key: marshall({ - // primaryKey: `CUSTOMER#${customerId}`, - // sortKey: "SUMMARY", - // }), - // UpdateExpression: "ADD totalPaid :amount SET lastUpdated = :ts", - // ExpressionAttributeValues: marshall({ - // ":amount": amount, - // ":ts": Date.now(), - // }), - // }) - // ); - // } - } + request.log.info( + `Recorded successful payment ${intent.id} from ${email} (${amount} ${currency})`, + ); return reply .status(200) From 34389efee5dc262cf841af562c3b278de9fe42c9 Mon Sep 17 00:00:00 2001 From: Aditya Kshirsagar Date: Fri, 31 Oct 2025 20:23:38 -0500 Subject: [PATCH 3/5] fixed sortkey for payments table --- src/api/routes/stripe.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index 8e29ddcd..38a6369f 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -727,19 +727,21 @@ Please contact Officer Board with any questions.`, intent.metadata?.billing_email ?? "unknown@example.com"; const acmOrg = intent.metadata?.acm_org ?? "ACM@UIUC"; - const domain = email.split("@")[1] ?? "unknown.com"; + const domain = email.includes("@") + ? email.split("@")[1] + : "unknown.com"; await fastify.dynamoClient.send( new PutItemCommand({ TableName: genericConfig.StripePaymentsDynamoTableName, Item: marshall({ primaryKey: `${acmOrg}#${domain}`, - sortKey: `customer`, + sortKey: event.id, amount, currency, status: "succeeded", billingEmail: email, - createdAt: Date.now(), + createdAt: new Date().toISOString(), eventId: event.id, }), }), From 54d91eb5b5867cd18c1e653830e305b2b9c3bedb Mon Sep 17 00:00:00 2001 From: Aditya Kshirsagar Date: Mon, 3 Nov 2025 17:08:13 -0600 Subject: [PATCH 4/5] added put error handling and general error handling --- src/api/routes/stripe.ts | 91 +++++++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index 38a6369f..9fa3ce55 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -721,39 +721,72 @@ Please contact Officer Board with any questions.`, const amount = intent.amount_received; const currency = intent.currency; - const customerId = intent.customer?.toString() ?? "UNKNOWN"; - const email = - intent.receipt_email ?? - intent.metadata?.billing_email ?? - "unknown@example.com"; - const acmOrg = intent.metadata?.acm_org ?? "ACM@UIUC"; - const domain = email.includes("@") - ? email.split("@")[1] - : "unknown.com"; + const customerId = intent.customer?.toString(); + const email = intent.receipt_email ?? intent.metadata?.billing_email; + const acmOrg = intent.metadata?.acm_org; - await fastify.dynamoClient.send( - new PutItemCommand({ - TableName: genericConfig.StripePaymentsDynamoTableName, - Item: marshall({ - primaryKey: `${acmOrg}#${domain}`, - sortKey: event.id, - amount, - currency, - status: "succeeded", - billingEmail: email, - createdAt: new Date().toISOString(), - eventId: event.id, + if (!customerId) { + request.log.info("Skipping payment intent with no customer ID."); + return reply + .code(200) + .send({ handled: false, requestId: request.id }); + } + + if (!email) { + request.log.warn("Missing email for payment intent."); + return reply + .code(200) + .send({ handled: false, requestId: request.id }); + } + + if (!acmOrg) { + request.log.warn("Missing acm_org for payment intent."); + return reply + .code(200) + .send({ handled: false, requestId: request.id }); + } + + if (!email.includes("@")) { + request.log.warn("Invalid email format for payment intent."); + return reply + .code(200) + .send({ handled: false, requestId: request.id }); + } + const domain = email.split("@")[1]; + + try { + await fastify.dynamoClient.send( + new PutItemCommand({ + TableName: genericConfig.StripePaymentsDynamoTableName, + Item: marshall({ + primaryKey: `${acmOrg}#${domain}`, + sortKey: event.id, + amount, + currency, + status: "succeeded", + billingEmail: email, + createdAt: new Date().toISOString(), + eventId: event.id, + }), }), - }), - ); + ); - request.log.info( - `Recorded successful payment ${intent.id} from ${email} (${amount} ${currency})`, - ); + request.log.info( + `Recorded successful payment ${intent.id} from ${email} (${amount} ${currency})`, + ); - return reply - .status(200) - .send({ handled: true, requestId: request.id }); + return reply + .status(200) + .send({ handled: true, requestId: request.id }); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error(e); + throw new DatabaseInsertError({ + message: `Could not insert Stripe payment record: ${(e as Error).message}`, + }); + } } default: request.log.warn(`Unhandled event type: ${event.type}`); From aee1aa4d33d6e85f1f0f83d5c0954d7bbc73f912 Mon Sep 17 00:00:00 2001 From: Aditya Kshirsagar Date: Mon, 3 Nov 2025 17:28:59 -0600 Subject: [PATCH 5/5] added coderabbit email normalization stuff --- src/api/routes/stripe.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index 9fa3ce55..f69676b3 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -746,13 +746,23 @@ Please contact Officer Board with any questions.`, .send({ handled: false, requestId: request.id }); } - if (!email.includes("@")) { + const normalizedEmail = email.trim(); + if (!normalizedEmail.includes("@")) { request.log.warn("Invalid email format for payment intent."); return reply .code(200) .send({ handled: false, requestId: request.id }); } - const domain = email.split("@")[1]; + const [, domainPart] = normalizedEmail.split("@"); + if (!domainPart) { + request.log.warn( + "Could not derive email domain for payment intent.", + ); + return reply + .code(200) + .send({ handled: false, requestId: request.id }); + } + const domain = domainPart.toLowerCase(); try { await fastify.dynamoClient.send( @@ -764,7 +774,7 @@ Please contact Officer Board with any questions.`, amount, currency, status: "succeeded", - billingEmail: email, + billingEmail: normalizedEmail, createdAt: new Date().toISOString(), eventId: event.id, }), @@ -772,7 +782,7 @@ Please contact Officer Board with any questions.`, ); request.log.info( - `Recorded successful payment ${intent.id} from ${email} (${amount} ${currency})`, + `Recorded successful payment ${intent.id} from ${normalizedEmail} (${amount} ${currency})`, ); return reply