-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/customer checkout #423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,16 @@ import { isProd } from "api/utils.js"; | |
| import { InternalServerError, ValidationError } from "common/errors/index.js"; | ||
| import { capitalizeFirstLetter } from "common/types/roomRequest.js"; | ||
| import Stripe from "stripe"; | ||
| import { createLock, IoredisAdapter, type SimpleLock } from "redlock-universal"; | ||
| import { Redis } from "api/types.js"; | ||
| import { | ||
| TransactWriteItemsCommand, | ||
| QueryCommand, | ||
| UpdateItemCommand, | ||
| DynamoDBClient, | ||
| } from "@aws-sdk/client-dynamodb"; | ||
| import { genericConfig } from "common/config.js"; | ||
| import { marshall } from "@aws-sdk/util-dynamodb"; | ||
|
|
||
| export type StripeLinkCreateParams = { | ||
| invoiceId: string; | ||
|
|
@@ -325,3 +335,228 @@ export const createStripeCustomer = async ({ | |
| ); | ||
| return customer.id; | ||
| }; | ||
|
|
||
| export type checkCustomerParams = { | ||
| acmOrg: string; | ||
| emailDomain: string; | ||
| redisClient: Redis; | ||
| dynamoClient: DynamoDBClient; | ||
| customerEmail: string; | ||
| customerName: string; | ||
| stripeApiKey: string; | ||
| }; | ||
|
|
||
| export type CheckOrCreateResult = { | ||
| customerId: string; | ||
| needsConfirmation?: boolean; | ||
| current?: { name?: string | null; email?: string | null }; | ||
| incoming?: { name: string; email: string }; | ||
| }; | ||
|
|
||
| export const checkOrCreateCustomer = async ({ | ||
| acmOrg, | ||
| emailDomain, | ||
| redisClient, | ||
| dynamoClient, | ||
| customerEmail, | ||
| customerName, | ||
| stripeApiKey, | ||
| }: checkCustomerParams): Promise<CheckOrCreateResult> => { | ||
| const lock = createLock({ | ||
| adapter: new IoredisAdapter(redisClient), | ||
| key: `stripe:${acmOrg}:${emailDomain}`, | ||
| retryAttempts: 5, | ||
| retryDelay: 300, | ||
| }) as SimpleLock; | ||
|
|
||
| const pk = `${acmOrg}#${emailDomain}`; | ||
| const normalizedEmail = customerEmail.trim().toLowerCase(); | ||
|
|
||
| return await lock.using(async () => { | ||
| const checkCustomer = new QueryCommand({ | ||
| TableName: genericConfig.StripePaymentsDynamoTableName, | ||
| KeyConditionExpression: "primaryKey = :pk AND sortKey = :sk", | ||
| ExpressionAttributeValues: { | ||
| ":pk": { S: pk }, | ||
| ":sk": { S: "CUSTOMER" }, | ||
| }, | ||
| ConsistentRead: true, | ||
| }); | ||
|
|
||
| const customerResponse = await dynamoClient.send(checkCustomer); | ||
|
|
||
| if (customerResponse.Count === 0) { | ||
| const customer = await createStripeCustomer({ | ||
| email: normalizedEmail, | ||
| name: customerName, | ||
| stripeApiKey, | ||
| }); | ||
|
|
||
| const createCustomer = new TransactWriteItemsCommand({ | ||
| TransactItems: [ | ||
| { | ||
| Put: { | ||
| TableName: genericConfig.StripePaymentsDynamoTableName, | ||
| Item: marshall( | ||
| { | ||
| primaryKey: pk, | ||
| sortKey: "CUSTOMER", | ||
| stripeCustomerId: customer, | ||
| totalAmount: 0, | ||
| createdAt: new Date().toISOString(), | ||
| }, | ||
| { removeUndefinedValues: true }, | ||
| ), | ||
| ConditionExpression: | ||
| "attribute_not_exists(primaryKey) AND attribute_not_exists(sortKey)", | ||
| }, | ||
| }, | ||
| { | ||
| Put: { | ||
| TableName: genericConfig.StripePaymentsDynamoTableName, | ||
| Item: marshall( | ||
| { | ||
| primaryKey: pk, | ||
| sortKey: `EMAIL#${normalizedEmail}`, | ||
| stripeCustomerId: customer, | ||
| createdAt: new Date().toISOString(), | ||
| }, | ||
| { removeUndefinedValues: true }, | ||
| ), | ||
| ConditionExpression: | ||
| "attribute_not_exists(primaryKey) AND attribute_not_exists(sortKey)", | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
| await dynamoClient.send(createCustomer); | ||
| return { customerId: customer }; | ||
| } | ||
|
|
||
| const existingCustomerId = (customerResponse.Items![0] as any) | ||
| .stripeCustomerId.S as string; | ||
|
|
||
| const stripeClient = new Stripe(stripeApiKey); | ||
| const stripeCustomer = | ||
| await stripeClient.customers.retrieve(existingCustomerId); | ||
|
|
||
| const liveName = | ||
| "name" in stripeCustomer ? (stripeCustomer as any).name : null; | ||
| const liveEmail = | ||
| "email" in stripeCustomer ? (stripeCustomer as any).email : null; | ||
|
|
||
| const needsConfirmation = | ||
| (!!liveName && liveName !== customerName) || | ||
| (!!liveEmail && liveEmail.toLowerCase() !== normalizedEmail); | ||
|
|
||
| const ensureEmailMap = new TransactWriteItemsCommand({ | ||
| TransactItems: [ | ||
| { | ||
| Put: { | ||
| TableName: genericConfig.StripePaymentsDynamoTableName, | ||
| Item: marshall( | ||
| { | ||
| primaryKey: pk, | ||
| sortKey: `EMAIL#${normalizedEmail}`, | ||
| stripeCustomerId: existingCustomerId, | ||
| createdAt: new Date().toISOString(), | ||
| }, | ||
| { removeUndefinedValues: true }, | ||
| ), | ||
| ConditionExpression: | ||
| "attribute_not_exists(primaryKey) AND attribute_not_exists(sortKey)", | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
| try { | ||
| await dynamoClient.send(ensureEmailMap); | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
|
|
||
| if (needsConfirmation) { | ||
| return { | ||
| customerId: existingCustomerId, | ||
| needsConfirmation: true, | ||
| current: { name: liveName ?? null, email: liveEmail ?? null }, | ||
| incoming: { name: customerName, email: normalizedEmail }, | ||
| }; | ||
| } | ||
|
|
||
| return { customerId: existingCustomerId }; | ||
| }); | ||
| }; | ||
|
Comment on lines
+339
to
+489
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize email domain when computing StripePaymentsDynamoTable keys and lock key
const pk = `${acmOrg}#${emailDomain}`;
const normalizedEmail = customerEmail.trim().toLowerCase();while the To keep data and locking consistent, normalize the domain from the email and reuse it everywhere in this function, e.g.: export const checkOrCreateCustomer = async ({
acmOrg,
- emailDomain,
+ emailDomain,
redisClient,
dynamoClient,
customerEmail,
customerName,
stripeApiKey,
}: checkCustomerParams): Promise<CheckOrCreateResult> => {
- const lock = createLock({
- adapter: new IoredisAdapter(redisClient),
- key: `stripe:${acmOrg}:${emailDomain}`,
- retryAttempts: 5,
- retryDelay: 300,
- }) as SimpleLock;
-
- const pk = `${acmOrg}#${emailDomain}`;
- const normalizedEmail = customerEmail.trim().toLowerCase();
+ const normalizedEmail = customerEmail.trim().toLowerCase();
+ const [, domainPart] = normalizedEmail.split("@");
+ if (!domainPart) {
+ throw new ValidationError({
+ message: `Could not derive email domain for "${customerEmail}".`,
+ });
+ }
+ const normalizedDomain = domainPart.toLowerCase();
+
+ const lock = createLock({
+ adapter: new IoredisAdapter(redisClient),
+ key: `stripe:${acmOrg}:${normalizedDomain}`,
+ retryAttempts: 5,
+ retryDelay: 300,
+ }) as SimpleLock;
+
+ const pk = `${acmOrg}#${normalizedDomain}`;And in |
||
|
|
||
| export type InvoiceAddParams = { | ||
| acmOrg: string; | ||
| emailDomain: string; | ||
| invoiceId: string; | ||
| invoiceAmountUsd: number; | ||
| redisClient: Redis; | ||
| dynamoClient: DynamoDBClient; | ||
| contactEmail: string; | ||
| contactName: string; | ||
| stripeApiKey: string; | ||
| }; | ||
|
|
||
| export const addInvoice = async ({ | ||
| contactName, | ||
| contactEmail, | ||
| acmOrg, | ||
| invoiceId, | ||
| invoiceAmountUsd, | ||
| emailDomain, | ||
| redisClient, | ||
| dynamoClient, | ||
| stripeApiKey, | ||
| }: InvoiceAddParams): Promise<CheckOrCreateResult> => { | ||
| const pk = `${acmOrg}#${emailDomain}`; | ||
|
|
||
| const result = await checkOrCreateCustomer({ | ||
| acmOrg, | ||
| emailDomain, | ||
| redisClient, | ||
| dynamoClient, | ||
| customerEmail: contactEmail, | ||
| customerName: contactName, | ||
| stripeApiKey, | ||
| }); | ||
|
|
||
| if (result.needsConfirmation) { | ||
| return result; | ||
| } | ||
|
|
||
| const dynamoCommand = new TransactWriteItemsCommand({ | ||
| TransactItems: [ | ||
| { | ||
| Put: { | ||
| TableName: genericConfig.StripePaymentsDynamoTableName, | ||
| Item: marshall( | ||
| { | ||
| primaryKey: pk, | ||
| sortKey: `CHARGE#${invoiceId}`, | ||
| invoiceAmtUsd: invoiceAmountUsd, | ||
| createdAt: new Date().toISOString(), | ||
| }, | ||
| { removeUndefinedValues: true }, | ||
| ), | ||
| }, | ||
| Update: { | ||
| TableName: genericConfig.StripePaymentsDynamoTableName, | ||
| Key: { | ||
| primaryKey: { S: pk }, | ||
| sortKey: { S: "CUSTOMER" }, | ||
| }, | ||
| UpdateExpression: "SET totalAmount = totalAmount + :inc", | ||
| ExpressionAttributeValues: { | ||
| ":inc": { N: invoiceAmountUsd.toString() }, | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await dynamoClient.send(dynamoCommand); | ||
| return { customerId: result.customerId }; | ||
| }; | ||
|
Comment on lines
+503
to
+562
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainFix TransactWriteItems structure and make CHARGE writes idempotent The TransactItems: [
{
Put: { ... },
Update: { ... },
},
],For DynamoDB, each Additionally, the CHARGE You can fix both issues by separating the operations into two items and adding a condition on the CHARGE write, for example: - const dynamoCommand = new TransactWriteItemsCommand({
- TransactItems: [
- {
- Put: {
- TableName: genericConfig.StripePaymentsDynamoTableName,
- Item: marshall(
- {
- primaryKey: pk,
- sortKey: `CHARGE#${invoiceId}`,
- invoiceAmtUsd: invoiceAmountUsd,
- createdAt: new Date().toISOString(),
- },
- { removeUndefinedValues: true },
- ),
- },
- Update: {
- TableName: genericConfig.StripePaymentsDynamoTableName,
- Key: {
- primaryKey: { S: pk },
- sortKey: { S: "CUSTOMER" },
- },
- UpdateExpression: "SET totalAmount = totalAmount + :inc",
- ExpressionAttributeValues: {
- ":inc": { N: invoiceAmountUsd.toString() },
- },
- },
- },
- ],
- });
+ const dynamoCommand = new TransactWriteItemsCommand({
+ TransactItems: [
+ {
+ Put: {
+ TableName: genericConfig.StripePaymentsDynamoTableName,
+ Item: marshall(
+ {
+ primaryKey: pk,
+ sortKey: `CHARGE#${invoiceId}`,
+ invoiceAmtUsd: invoiceAmountUsd,
+ createdAt: new Date().toISOString(),
+ },
+ { removeUndefinedValues: true },
+ ),
+ ConditionExpression:
+ "attribute_not_exists(primaryKey) AND attribute_not_exists(sortKey)",
+ },
+ },
+ {
+ Update: {
+ TableName: genericConfig.StripePaymentsDynamoTableName,
+ Key: {
+ primaryKey: { S: pk },
+ sortKey: { S: "CUSTOMER" },
+ },
+ UpdateExpression: "SET totalAmount = totalAmount + :inc",
+ ExpressionAttributeValues: {
+ ":inc": { N: invoiceAmountUsd.toString() },
+ },
+ },
+ },
+ ],
+ });This matches DynamoDB’s API shape and ensures you don’t double-count the same invoice on retries. Length of output: 1679 🏁 Script executed: #!/bin/bash
# Search for error handling around dynamoClient.send or addInvoice calls
rg -n -A5 -B5 "dynamoClient.send|addInvoice" src/api/functions/stripe.tsLength of output: 1328 🏁 Script executed: #!/bin/bash
# Check if there are other TransactWriteItemsCommand usages in the codebase
rg -n "TransactWriteItemsCommand" --type=tsLength of output: 7010 🏁 Script executed: #!/bin/bash
# Look for error handlers around the addInvoice function calls
rg -n -C8 "addInvoice\(" src/ --type=ts -g '!stripe.ts'Length of output: 0 🌐 Web query: 💡 Result: Yes. Each TransactWriteItem in TransactWriteItems must contain exactly one of: Put, Update, Delete, or ConditionCheck. See the DynamoDB API docs. [1][2] Sources 🌐 Web query: 💡 Result: Short answer: You must not put both Put and Update in the same TransactWriteItem. Each TransactWriteItem is one request — exactly one of ConditionCheck, Put, Update or Delete — and the TransactWriteItems API requires that each item contains only one action. If you send a TransactWriteItem with multiple actions (e.g. both Put and Update) the request is invalid and will fail (API-side validation / transaction cancellation). (docs.aws.amazon.com) Citations: Length of output: 68 🏁 Script executed: #!/bin/bash
# Check what functions are exported from stripe.ts
rg -n "^export" src/api/functions/stripe.ts | head -20Length of output: 1027 🏁 Script executed: #!/bin/bash
# Verify that the malformed structure would cause AWS SDK to reject it
# by checking test expectations around TransactWriteItemsCommand failures
rg -n -B3 -A3 "TransactWriteItemsCommand.*rejects" tests/unit/stripe.test.ts | head -30Length of output: 418 Fix TransactWriteItems structure and make CHARGE writes idempotent The Additionally, the Separate the operations into two distinct const dynamoCommand = new TransactWriteItemsCommand({
TransactItems: [
{
Put: {
TableName: genericConfig.StripePaymentsDynamoTableName,
Item: marshall(
{
primaryKey: pk,
sortKey: `CHARGE#${invoiceId}`,
invoiceAmtUsd: invoiceAmountUsd,
createdAt: new Date().toISOString(),
},
{ removeUndefinedValues: true },
),
+ ConditionExpression:
+ "attribute_not_exists(primaryKey) AND attribute_not_exists(sortKey)",
},
},
+ {
Update: {
TableName: genericConfig.StripePaymentsDynamoTableName,
Key: {
primaryKey: { S: pk },
sortKey: { S: "CUSTOMER" },
},
UpdateExpression: "SET totalAmount = totalAmount + :inc",
ExpressionAttributeValues: {
":inc": { N: invoiceAmountUsd.toString() },
},
},
+ },
],
}); |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,13 +9,16 @@ import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; | |
| import { withRoles, withTags } from "api/components/index.js"; | ||
| import { buildAuditLogTransactPut } from "api/functions/auditLog.js"; | ||
| import { | ||
| addInvoice, | ||
| createStripeLink, | ||
| createCheckoutSessionWithCustomer, | ||
| deactivateStripeLink, | ||
| deactivateStripeProduct, | ||
| getPaymentMethodDescriptionString, | ||
| getPaymentMethodForPaymentIntent, | ||
| paymentMethodTypeToFriendlyName, | ||
| StripeLinkCreateParams, | ||
| InvoiceAddParams, | ||
| SupportedStripePaymentMethod, | ||
| supportedStripePaymentMethods, | ||
| } from "api/functions/stripe.js"; | ||
|
|
@@ -36,6 +39,7 @@ import { AppRoles } from "common/roles.js"; | |
| import { | ||
| invoiceLinkPostResponseSchema, | ||
| invoiceLinkPostRequestSchema, | ||
| createInvoicePostRequestSchema, | ||
| invoiceLinkGetResponseSchema, | ||
| } from "common/types/stripe.js"; | ||
| import { FastifyPluginAsync } from "fastify"; | ||
|
|
@@ -110,6 +114,68 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { | |
| reply.status(200).send(parsed); | ||
| }, | ||
| ); | ||
| fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post( | ||
| "/createInvoice", | ||
| { | ||
| schema: withRoles( | ||
| [AppRoles.STRIPE_LINK_CREATOR], | ||
| withTags(["Stripe"], { | ||
| summary: "Create a new invoice.", | ||
| body: createInvoicePostRequestSchema, | ||
| }), | ||
| ), | ||
| onRequest: fastify.authorizeFromSchema, | ||
| }, | ||
| async (request, reply) => { | ||
| const emailDomain = request.body.contactEmail.split("@").at(-1); | ||
|
|
||
| const secretApiConfig = fastify.secretConfig; | ||
| const payload: InvoiceAddParams = { | ||
| ...request.body, | ||
| emailDomain: emailDomain!, | ||
| redisClient: fastify.redisClient, | ||
| dynamoClient: fastify.dynamoClient, | ||
| stripeApiKey: secretApiConfig.stripe_secret_key as string, | ||
| }; | ||
|
|
||
| const result = await addInvoice(payload); | ||
|
|
||
| if (result.needsConfirmation) { | ||
| return reply.status(409).send({ | ||
| needsConfirmation: true, | ||
| customerId: result.customerId, | ||
| current: result.current, | ||
| incoming: result.incoming, | ||
| message: "Customer info differs. Confirm update before proceeding.", | ||
| }); | ||
| } | ||
|
|
||
| const checkoutUrl = await createCheckoutSessionWithCustomer({ | ||
| customerId: result.customerId, | ||
| stripeApiKey: secretApiConfig.stripe_secret_key as string, | ||
| items: [ | ||
| { | ||
| price: "<PRICE_ID_OR_DYNAMICALLY_CREATED_PRICE>", | ||
| quantity: 1, | ||
| }, | ||
| ], | ||
| initiator: request.username || "system", | ||
| allowPromotionCodes: true, | ||
| successUrl: `${fastify.environmentConfig.UserFacingUrl}/success`, | ||
| returnUrl: `${fastify.environmentConfig.UserFacingUrl}/cancel`, | ||
| metadata: { | ||
| acm_org: request.body.acmOrg, | ||
| billing_email: request.body.contactEmail, | ||
| invoice_id: request.body.invoiceId, | ||
| }, | ||
| }); | ||
|
|
||
| reply.status(201).send({ | ||
| id: request.body.invoiceId, | ||
| link: checkoutUrl, | ||
| }); | ||
|
Comment on lines
+153
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainReplace placeholder price and tie checkout amount to invoiceAmountUsd Right now the checkout session is created with a hard-coded placeholder price: items: [
{
price: "<PRICE_ID_OR_DYNAMICALLY_CREATED_PRICE>",
quantity: 1,
},
],This will fail at runtime and also ignores Consider wiring this to a real Stripe Price derived from the invoice amount (or a configured price), for example by:
Until this is done, I'll verify the review comment by examining the codebase. Let me search for the relevant code and understand the current implementation. Length of output: 50502 🏁 Script executed: #!/bin/bash
# Search for createStripeLink function definition
rg -n "createStripeLink" -A 10 --type tsLength of output: 1949 🏁 Script executed: #!/bin/bash
# Search for invoiceAmountUsd usage in the codebase
rg -n "invoiceAmountUsd" -B 2 -A 2 --type tsLength of output: 12237 🏁 Script executed: #!/bin/bash
# Search for price creation logic in Stripe-related files
rg -n "price:" -B 3 -A 3 --type ts -g "*stripe*"Length of output: 2472 Create a dynamic price for The placeholder price Reuse the price creation logic from 🤖 Prompt for AI Agents |
||
| }, | ||
| ); | ||
| fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post( | ||
| "/paymentLinks", | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,48 @@ export type PostInvoiceLinkRequest = z.infer< | |
| export type PostInvoiceLinkResponse = z.infer< | ||
| typeof invoiceLinkPostResponseSchema>; | ||
|
|
||
| export const createInvoicePostResponseSchema = z.object({ | ||
| id: z.string().min(1), | ||
| link: z.url() | ||
| }); | ||
|
|
||
| export const createInvoiceConflictResponseSchema = z.object({ | ||
| needsConfirmation: z.literal(true), | ||
| customerId: z.string().min(1), | ||
| current: z.object({ | ||
| name: z.string().nullable().optional(), | ||
| email: z.string().nullable().optional(), | ||
| }), | ||
| incoming: z.object({ | ||
| name: z.string().min(1), | ||
| email: z.string().email(), | ||
| }), | ||
| message: z.string().min(1), | ||
| }); | ||
|
|
||
| export const createInvoicePostResponseSchemaUnion = z.union([ | ||
| createInvoicePostResponseSchema, // success: 201 | ||
| createInvoiceConflictResponseSchema, // info mismatch: 409 | ||
| ]); | ||
|
|
||
| export type PostCreateInvoiceResponseUnion = z.infer< | ||
| typeof createInvoicePostResponseSchemaUnion | ||
| >; | ||
|
|
||
| export const createInvoicePostRequestSchema = z.object({ | ||
| invoiceId: z.string().min(1), | ||
| invoiceAmountUsd: z.number().min(50), | ||
| contactName: z.string().min(1), | ||
| contactEmail: z.email(), | ||
| acmOrg: z.string().min(1) | ||
| }); | ||
|
|
||
| export type PostCreateInvoiceRequest = z.infer< | ||
| typeof createInvoicePostRequestSchema>; | ||
|
|
||
|
|
||
| export type PostCreateInvoiceResponse = z.infer< | ||
| typeof createInvoicePostResponseSchema>; | ||
|
|
||
|
Comment on lines
+22
to
64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix Prettier/ESLint formatting in new createInvoice schemas and types Schemas and types look good, but Prettier is complaining about missing trailing commas and line breaks on these lines. This will block linting until fixed. You can resolve the reported export const createInvoicePostResponseSchema = z.object({
id: z.string().min(1),
- link: z.url()
-});
+ link: z.url(),
+});
@@
export const createInvoicePostRequestSchema = z.object({
invoiceId: z.string().min(1),
invoiceAmountUsd: z.number().min(50),
contactName: z.string().min(1),
- contactEmail: z.email(),
- acmOrg: z.string().min(1)
-});
+ contactEmail: z.email(),
+ acmOrg: z.string().min(1),
+});
@@
-export type PostCreateInvoiceRequest = z.infer<
- typeof createInvoicePostRequestSchema>;
-
-
-export type PostCreateInvoiceResponse = z.infer<
- typeof createInvoicePostResponseSchema>;
+export type PostCreateInvoiceRequest = z.infer<
+ typeof createInvoicePostRequestSchema
+>;
+
+export type PostCreateInvoiceResponse = z.infer<
+ typeof createInvoicePostResponseSchema
+>;
@@
-export type GetInvoiceLinksResponse = z.infer<
- typeof invoiceLinkGetResponseSchema>;
+export type GetInvoiceLinksResponse = z.infer<
+ typeof invoiceLinkGetResponseSchema
+>;Also applies to: 78-78 🧰 Tools🪛 ESLint[error] 24-24: Insert (prettier/prettier) [error] 42-42: Delete (prettier/prettier) [error] 55-55: Insert (prettier/prettier) [error] 59-60: Replace (prettier/prettier) [error] 63-63: Insert (prettier/prettier) 🤖 Prompt for AI Agents |
||
| export const invoiceLinkGetResponseSchema = z.array( | ||
| z.object({ | ||
|
|
@@ -33,4 +75,4 @@ export const invoiceLinkGetResponseSchema = z.array( | |
| ); | ||
|
|
||
| export type GetInvoiceLinksResponse = z.infer< | ||
| typeof invoiceLinkGetResponseSchema>; | ||
| typeof invoiceLinkGetResponseSchema>; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Address ESLint import/extensions complaints for new imports
ESLint is flagging the
.jsextensions on the new imports from"api/types.js"and"common/config.js".Depending on the project’s convention, either:
import/extensions(e.g.import { Redis } from "api/types";,import { genericConfig } from "common/config";), or.jsextensions for these path-mapped imports, in line with how other modules in this repo are handled.Please align this with whatever pattern the rest of the codebase is using so lint passes.
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 31
🏁 Script executed:
Length of output: 2800
🏁 Script executed:
Length of output: 99
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 613
🏁 Script executed:
Length of output: 4665
🏁 Script executed:
Length of output: 22578
🏁 Script executed:
Length of output: 120
🏁 Script executed:
Length of output: 53
🏁 Script executed:
Length of output: 198
🏁 Script executed:
Length of output: 1529
🏁 Script executed:
Length of output: 306
🏁 Script executed:
Length of output: 764
🏁 Script executed:
Length of output: 529
🏁 Script executed:
Length of output: 2607
🏁 Script executed:
Length of output: 550
🏁 Script executed:
Length of output: 52
Remove
.jsextensions from path-mapped imports to comply with ESLintimport/extensionsruleThe ESLint configuration (eslint.config.mjs) explicitly requires that
.jsand.tsextensions be omitted from imports:"import/extensions": ["error", "ignorePackages", { js: "never", jsx: "never", ts: "never", tsx: "never" }].The imports in stripe.ts at lines 6 and 13 violate this rule:
import { Redis } from "api/types.js";toimport { Redis } from "api/types";import { genericConfig } from "common/config.js";toimport { genericConfig } from "common/config";The pre-commit hook runs
yarn lint --fix, which will catch this during commit. Note: the entire codebase currently uses.jsextensions for these paths (127+ instances forapi/*, 132+ forcommon/*), so a widespread fix may be needed beyond this file.🧰 Tools
🪛 ESLint
[error] 6-6: Unexpected use of file extension "js" for "api/types.js"
(import/extensions)
[error] 13-13: Unexpected use of file extension "js" for "common/config.js"
(import/extensions)
🤖 Prompt for AI Agents