Skip to content

Commit 7df5f73

Browse files
committed
updated to update customer info and also send metadata for payment succeeded webhook
1 parent f2aeaf5 commit 7df5f73

File tree

3 files changed

+148
-9
lines changed

3 files changed

+148
-9
lines changed

src/api/functions/stripe.ts

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,13 @@ export type checkCustomerParams = {
346346
stripeApiKey: string;
347347
};
348348

349+
export type CheckOrCreateResult = {
350+
customerId: string;
351+
needsConfirmation?: boolean;
352+
current?: { name?: string | null; email?: string | null };
353+
incoming?: { name: string; email: string };
354+
};
355+
349356
export const checkOrCreateCustomer = async ({
350357
acmOrg,
351358
emailDomain,
@@ -354,7 +361,7 @@ export const checkOrCreateCustomer = async ({
354361
customerEmail,
355362
customerName,
356363
stripeApiKey,
357-
}: checkCustomerParams): Promise<string> => {
364+
}: checkCustomerParams): Promise<CheckOrCreateResult> => {
358365
const lock = createLock({
359366
adapter: new IoredisAdapter(redisClient),
360367
key: `stripe:${acmOrg}:${emailDomain}`,
@@ -363,6 +370,7 @@ export const checkOrCreateCustomer = async ({
363370
}) as SimpleLock;
364371

365372
const pk = `${acmOrg}#${emailDomain}`;
373+
const normalizedEmail = customerEmail.trim().toLowerCase();
366374

367375
return await lock.using(async () => {
368376
const checkCustomer = new QueryCommand({
@@ -379,10 +387,11 @@ export const checkOrCreateCustomer = async ({
379387

380388
if (customerResponse.Count === 0) {
381389
const customer = await createStripeCustomer({
382-
email: customerEmail,
390+
email: normalizedEmail,
383391
name: customerName,
384392
stripeApiKey,
385393
});
394+
386395
const createCustomer = new TransactWriteItemsCommand({
387396
TransactItems: [
388397
{
@@ -398,15 +407,84 @@ export const checkOrCreateCustomer = async ({
398407
},
399408
{ removeUndefinedValues: true },
400409
),
410+
ConditionExpression:
411+
"attribute_not_exists(primaryKey) AND attribute_not_exists(sortKey)",
412+
},
413+
},
414+
{
415+
Put: {
416+
TableName: genericConfig.StripePaymentsDynamoTableName,
417+
Item: marshall(
418+
{
419+
primaryKey: pk,
420+
sortKey: `EMAIL#${normalizedEmail}`,
421+
stripeCustomerId: customer,
422+
createdAt: new Date().toISOString(),
423+
},
424+
{ removeUndefinedValues: true },
425+
),
426+
ConditionExpression:
427+
"attribute_not_exists(primaryKey) AND attribute_not_exists(sortKey)",
401428
},
402429
},
403430
],
404431
});
405432
await dynamoClient.send(createCustomer);
406-
return customer;
433+
return { customerId: customer };
407434
}
408435

409-
return customerResponse.Items![0].stripeCustomerId.S!;
436+
const existingCustomerId = (customerResponse.Items![0] as any)
437+
.stripeCustomerId.S as string;
438+
439+
const stripeClient = new Stripe(stripeApiKey);
440+
const stripeCustomer =
441+
await stripeClient.customers.retrieve(existingCustomerId);
442+
443+
const liveName =
444+
"name" in stripeCustomer ? (stripeCustomer as any).name : null;
445+
const liveEmail =
446+
"email" in stripeCustomer ? (stripeCustomer as any).email : null;
447+
448+
const needsConfirmation =
449+
(!!liveName && liveName !== customerName) ||
450+
(!!liveEmail && liveEmail.toLowerCase() !== normalizedEmail);
451+
452+
const ensureEmailMap = new TransactWriteItemsCommand({
453+
TransactItems: [
454+
{
455+
Put: {
456+
TableName: genericConfig.StripePaymentsDynamoTableName,
457+
Item: marshall(
458+
{
459+
primaryKey: pk,
460+
sortKey: `EMAIL#${normalizedEmail}`,
461+
stripeCustomerId: existingCustomerId,
462+
createdAt: new Date().toISOString(),
463+
},
464+
{ removeUndefinedValues: true },
465+
),
466+
ConditionExpression:
467+
"attribute_not_exists(primaryKey) AND attribute_not_exists(sortKey)",
468+
},
469+
},
470+
],
471+
});
472+
try {
473+
await dynamoClient.send(ensureEmailMap);
474+
} catch (e) {
475+
// ignore
476+
}
477+
478+
if (needsConfirmation) {
479+
return {
480+
customerId: existingCustomerId,
481+
needsConfirmation: true,
482+
current: { name: liveName ?? null, email: liveEmail ?? null },
483+
incoming: { name: customerName, email: normalizedEmail },
484+
};
485+
}
486+
487+
return { customerId: existingCustomerId };
410488
});
411489
};
412490

@@ -432,10 +510,10 @@ export const addInvoice = async ({
432510
redisClient,
433511
dynamoClient,
434512
stripeApiKey,
435-
}: InvoiceAddParams): Promise<string> => {
513+
}: InvoiceAddParams): Promise<CheckOrCreateResult> => {
436514
const pk = `${acmOrg}#${emailDomain}`;
437515

438-
const customerId = await checkOrCreateCustomer({
516+
const result = await checkOrCreateCustomer({
439517
acmOrg,
440518
emailDomain,
441519
redisClient,
@@ -445,6 +523,10 @@ export const addInvoice = async ({
445523
stripeApiKey,
446524
});
447525

526+
if (result.needsConfirmation) {
527+
return result;
528+
}
529+
448530
const dynamoCommand = new TransactWriteItemsCommand({
449531
TransactItems: [
450532
{
@@ -476,5 +558,5 @@ export const addInvoice = async ({
476558
});
477559

478560
await dynamoClient.send(dynamoCommand);
479-
return customerId;
561+
return { customerId: result.customerId };
480562
};

src/api/routes/stripe.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { buildAuditLogTransactPut } from "api/functions/auditLog.js";
1111
import {
1212
addInvoice,
1313
createStripeLink,
14+
createCheckoutSessionWithCustomer,
1415
deactivateStripeLink,
1516
deactivateStripeProduct,
1617
getPaymentMethodDescriptionString,
@@ -137,9 +138,42 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
137138
stripeApiKey: secretApiConfig.stripe_secret_key as string,
138139
};
139140

140-
const stripeCustomer = await addInvoice(payload);
141+
const result = await addInvoice(payload);
141142

142-
reply.status(201).send({ link: "<dummy link here>" });
143+
if (result.needsConfirmation) {
144+
return reply.status(409).send({
145+
needsConfirmation: true,
146+
customerId: result.customerId,
147+
current: result.current,
148+
incoming: result.incoming,
149+
message: "Customer info differs. Confirm update before proceeding.",
150+
});
151+
}
152+
153+
const checkoutUrl = await createCheckoutSessionWithCustomer({
154+
customerId: result.customerId,
155+
stripeApiKey: secretApiConfig.stripe_secret_key as string,
156+
items: [
157+
{
158+
price: "<PRICE_ID_OR_DYNAMICALLY_CREATED_PRICE>",
159+
quantity: 1,
160+
},
161+
],
162+
initiator: request.username || "system",
163+
allowPromotionCodes: true,
164+
successUrl: `${fastify.environmentConfig.UserFacingUrl}/success`,
165+
returnUrl: `${fastify.environmentConfig.UserFacingUrl}/cancel`,
166+
metadata: {
167+
acm_org: request.body.acmOrg,
168+
billing_email: request.body.contactEmail,
169+
invoice_id: request.body.invoiceId,
170+
},
171+
});
172+
173+
reply.status(201).send({
174+
id: request.body.invoiceId,
175+
link: checkoutUrl,
176+
});
143177
},
144178
);
145179
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post(

src/common/types/stripe.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@ export const createInvoicePostResponseSchema = z.object({
2424
link: z.url()
2525
});
2626

27+
export const createInvoiceConflictResponseSchema = z.object({
28+
needsConfirmation: z.literal(true),
29+
customerId: z.string().min(1),
30+
current: z.object({
31+
name: z.string().nullable().optional(),
32+
email: z.string().nullable().optional(),
33+
}),
34+
incoming: z.object({
35+
name: z.string().min(1),
36+
email: z.string().email(),
37+
}),
38+
message: z.string().min(1),
39+
});
40+
41+
export const createInvoicePostResponseSchemaUnion = z.union([
42+
createInvoicePostResponseSchema, // success: 201
43+
createInvoiceConflictResponseSchema, // info mismatch: 409
44+
]);
45+
46+
export type PostCreateInvoiceResponseUnion = z.infer<
47+
typeof createInvoicePostResponseSchemaUnion
48+
>;
49+
2750
export const createInvoicePostRequestSchema = z.object({
2851
invoiceId: z.string().min(1),
2952
invoiceAmountUsd: z.number().min(50),

0 commit comments

Comments
 (0)