Skip to content

Conversation

@devksingh4
Copy link
Member

@devksingh4 devksingh4 commented Oct 27, 2025

Summary by CodeRabbit

  • New Features

    • New endpoint to check whether a user profile needs syncing.
    • Checkout now uses linked customer profiles for payments.
  • Bug Fixes

    • Safer profile synchronization with locking to prevent concurrent update conflicts.
    • Improved error handling when creating or retrieving customer/payment sessions.
  • Improvements

    • Identity lookup added to better populate required user fields.
    • Non-production customer records are tagged to indicate environment.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds Stripe customer creation and checkout-by-customer APIs, introduces Redis-based distributed locking to sync user profiles and conditional Stripe customer creation, adds a user-identity fetch helper and a new /isRequired route, and propagates redis/stripe/logger dependencies through sync and membership flows.

Changes

Cohort / File(s) Summary
Stripe functions
src/api/functions/stripe.ts
Added StripeCheckoutSessionCreateWithCustomerParams and StripeCustomerCreateParams types. Added createCheckoutSessionWithCustomer(...) to build Checkout Sessions for an existing customerId and createStripeCustomer(...) to create Stripe customers (supports metadata, idempotency keys, and uses isProd).
Profile synchronization
src/api/functions/sync.ts
Updated syncFullProfile signature to accept redisClient, stripeApiKey, and logger. Added Redis distributed locking (redlock-universal) around per-netId DynamoDB updates, conditional Stripe customer creation when stripeCustomerId is missing, updating DynamoDB with the new stripeCustomerId, and lock-abandonment error handling.
Identity helper
src/api/functions/identity.ts
Added UserIdentity and GetUserIdentityInputs interfaces and getUserIdentity({ netId, dynamoClient, logger }) to fetch and unmarshal a user identity from DynamoDB (returns null if absent).
Sync route & new endpoint
src/api/routes/syncIdentity.ts
Updated syncFullProfile(...) calls to pass redisClient, stripeApiKey, and logger. Changed a log message to "syncing Entra user!". Added new /isRequired route that verifies a UIUC token, fetches identity via getUserIdentity, and returns { syncRequired } based on presence of uinHash, firstName, lastName, and stripeCustomerId.
Membership route changes
src/api/routes/v2/membership.ts
Removed setPaidMembershipInTable import. Switched checkout creation to createCheckoutSessionWithCustomer(...) and now passes userData.stripeCustomerId. Captures userData from save result and returns InternalServerError if falsy. Passes redisClient, stripeApiKey, and logger into syncFullProfile.
Utilities
src/api/utils.ts
Added exported isProd constant: process.env.RunEnvironment === "prod".

Sequence Diagram

sequenceDiagram
    participant Client
    participant Membership as Membership Handler
    participant Sync as Profile Sync
    participant Redis as Redis Lock
    participant Dynamo as DynamoDB
    participant Identity as Identity Helper
    participant Stripe as Stripe API

    Client->>Membership: Request checkout
    Membership->>Sync: syncFullProfile(netId..., redisClient, stripeApiKey, logger)
    Sync->>Redis: Acquire lock for netId
    activate Redis
    alt Lock acquired
        Sync->>Dynamo: UpdateItem / Get (ALL_NEW)
        Dynamo-->>Sync: Return attributes
        alt stripeCustomerId missing
            Sync->>Stripe: createStripeCustomer(email, name, metadata)
            Stripe-->>Sync: Return customerId
            Sync->>Dynamo: Update stripeCustomerId (ALL_NEW)
            Dynamo-->>Sync: Confirm update
        end
        Sync->>Redis: Release lock
    else Lock abandoned / aborted
        Sync-->>Membership: Throw InternalServerError("lock abandoned")
    end
    deactivate Redis

    Sync-->>Membership: Return profile with stripeCustomerId
    Membership->>Stripe: createCheckoutSessionWithCustomer(customerId, items...)
    Stripe-->>Membership: Return session URL
    Membership-->>Client: Return session URL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review lock acquisition/release and abort handling in src/api/functions/sync.ts (redlock-universal usage, timeouts, signal handling).
  • Verify DynamoDB UpdateItem semantics (ReturnValues ALL_NEW) and attribute marshalling/unmarshalling in sync.ts and identity.ts.
  • Validate conditional Stripe customer creation, idempotency key handling, and metadata/env usage in src/api/functions/stripe.ts.
  • Confirm getUserIdentity correctness (consistent read, key formation) and null handling in src/api/functions/identity.ts.
  • Ensure redisClient, stripeApiKey, and logger are consistently passed from route handlers (syncIdentity.ts, v2/membership.ts) into syncFullProfile.
  • Check the new /isRequired route auth, NetID parsing, and the syncRequired criteria.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Create stripe customer IDs for users when syncing identity" accurately and specifically reflects the main functionality being introduced in this changeset. The core changes across all modified files center on adding Stripe customer ID creation logic to the identity synchronization process—particularly in sync.ts where customer creation is triggered when missing, and in dependent routes that now pass the necessary credentials. The title is concise, clear, and free of vague language, making it immediately understandable to a teammate reviewing the repository history.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1fac4 and 0438778.

📒 Files selected for processing (3)
  • src/api/functions/identity.ts (1 hunks)
  • src/api/functions/stripe.ts (4 hunks)
  • src/api/routes/syncIdentity.ts (4 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 27, 2025

💰 Infracost report

Monthly estimate generated

This comment will be updated when code changes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/routes/v2/membership.ts (1)

124-133: Validate Stripe secret before invoking downstream Stripe calls.

Prevent late failures with a clear error.

-        const savePromise = syncFullProfile({
+        if (!fastify.secretConfig.stripe_secret_key) {
+          request.log.error("Missing Stripe secret key.");
+          throw new InternalServerError({ message: "Stripe is not configured." });
+        }
+        const savePromise = syncFullProfile({
           uinHash,
           firstName: givenName,
           lastName: surname,
           netId,
           dynamoClient: fastify.dynamoClient,
           redisClient: fastify.redisClient,
           stripeApiKey: fastify.secretConfig.stripe_secret_key,
           logger: request.log,
         });
🧹 Nitpick comments (5)
src/api/functions/sync.ts (2)

82-82: Avoid logging PII (full email) at info level.

Log NetID or a hash instead; keep full email to debug level only if necessary.

-      logger.info(`Created new Stripe customer for ${userId}.`);
+      logger.info({ netId }, "Created new Stripe customer.");

5-11: ESLint import/extensions warnings.

Either drop “.js” in TS imports or configure eslint-plugin-import for Node16/TypeScript resolution so ESM TS paths ending in .js are allowed.

src/api/functions/stripe.ts (1)

1-1: ESLint import/extensions warning.

Same note as other files: either remove “.js” in TS imports or configure the resolver to accept ESM TS with .js suffix.

src/api/routes/v2/membership.ts (2)

14-17: Drop unused import.

createCheckoutSession isn’t used.

-import {
-  createCheckoutSession,
-  createCheckoutSessionWithCustomer,
-} from "api/functions/stripe.js";
+import { createCheckoutSessionWithCustomer } from "api/functions/stripe.js";

159-177: Pass the already‑validated Stripe key; rely on required customerId.

Minor cleanup once customerId is required in the helper.

-          await createCheckoutSessionWithCustomer({
+          await createCheckoutSessionWithCustomer({
             successUrl: "https://acm.illinois.edu/paid",
             returnUrl: "https://acm.illinois.edu/membership",
-            customerId: userData.stripeCustomerId,
-            stripeApiKey: fastify.secretConfig.stripe_secret_key as string,
+            customerId: userData.stripeCustomerId,
+            stripeApiKey: fastify.secretConfig.stripe_secret_key as string,
             items: [

(Keep as-is; this note is to align with the stricter helper type.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 40eb9eb and 2d1fac4.

📒 Files selected for processing (5)
  • src/api/functions/stripe.ts (4 hunks)
  • src/api/functions/sync.ts (2 hunks)
  • src/api/routes/syncIdentity.ts (2 hunks)
  • src/api/routes/v2/membership.ts (3 hunks)
  • src/api/utils.ts (1 hunks)
🧰 Additional context used
🪛 ESLint
src/api/functions/sync.ts

[error] 5-5: Unexpected use of file extension "js" for "api/types.js"

(import/extensions)


[error] 6-6: Unexpected use of file extension "js" for "common/config.js"

(import/extensions)


[error] 8-8: Unexpected use of file extension "js" for "./stripe.js"

(import/extensions)


[error] 9-9: Unexpected use of file extension "js" for "common/errors/index.js"

(import/extensions)

src/api/functions/stripe.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/qWpMiHHrJd'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 1-1: Unexpected use of file extension "js" for "api/utils.js"

(import/extensions)

src/api/routes/v2/membership.ts

[error] 17-17: Unexpected use of file extension "js" for "api/functions/stripe.js"

(import/extensions)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Unit Tests
  • GitHub Check: Build Application
🔇 Additional comments (2)
src/api/utils.ts (1)

46-46: LGTM.

isProd is clear and side‑effect free.

src/api/routes/syncIdentity.ts (1)

129-129: Log phrasing change acknowledged.

Message update is fine.

Comment on lines +133 to +159
}: StripeCheckoutSessionCreateWithCustomerParams): Promise<string> => {
const stripe = new Stripe(stripeApiKey);
const payload: Stripe.Checkout.SessionCreateParams = {
success_url: successUrl || "",
cancel_url: returnUrl || "",
payment_method_types: ["card"],
line_items: items.map((item) => ({
price: item.price,
quantity: item.quantity,
})),
mode: "payment",
customer: customerId,
metadata: {
...(metadata || {}),
initiator,
},
allow_promotion_codes: allowPromotionCodes,
custom_fields: customFields,
};
const session = await stripe.checkout.sessions.create(payload);
if (!session.url) {
throw new InternalServerError({
message: "Could not create Stripe checkout session.",
});
}
return session.url;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add a runtime guard for missing customerId.

Defensive check to avoid silent bad requests to Stripe.

 export const createCheckoutSessionWithCustomer = async ({
   successUrl,
   returnUrl,
   stripeApiKey,
   customerId,
   items,
   initiator,
   allowPromotionCodes,
   customFields,
   metadata,
 }: StripeCheckoutSessionCreateWithCustomerParams): Promise<string> => {
+  if (!customerId) {
+    throw new ValidationError({ message: "Missing Stripe customerId." });
+  }
   const stripe = new Stripe(stripeApiKey);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}: StripeCheckoutSessionCreateWithCustomerParams): Promise<string> => {
const stripe = new Stripe(stripeApiKey);
const payload: Stripe.Checkout.SessionCreateParams = {
success_url: successUrl || "",
cancel_url: returnUrl || "",
payment_method_types: ["card"],
line_items: items.map((item) => ({
price: item.price,
quantity: item.quantity,
})),
mode: "payment",
customer: customerId,
metadata: {
...(metadata || {}),
initiator,
},
allow_promotion_codes: allowPromotionCodes,
custom_fields: customFields,
};
const session = await stripe.checkout.sessions.create(payload);
if (!session.url) {
throw new InternalServerError({
message: "Could not create Stripe checkout session.",
});
}
return session.url;
};
}: StripeCheckoutSessionCreateWithCustomerParams): Promise<string> => {
if (!customerId) {
throw new ValidationError({ message: "Missing Stripe customerId." });
}
const stripe = new Stripe(stripeApiKey);
const payload: Stripe.Checkout.SessionCreateParams = {
success_url: successUrl || "",
cancel_url: returnUrl || "",
payment_method_types: ["card"],
line_items: items.map((item) => ({
price: item.price,
quantity: item.quantity,
})),
mode: "payment",
customer: customerId,
metadata: {
...(metadata || {}),
initiator,
},
allow_promotion_codes: allowPromotionCodes,
custom_fields: customFields,
};
const session = await stripe.checkout.sessions.create(payload);
if (!session.url) {
throw new InternalServerError({
message: "Could not create Stripe checkout session.",
});
}
return session.url;
};
🤖 Prompt for AI Agents
In src/api/functions/stripe.ts around lines 133 to 159, add a runtime guard that
validates customerId before constructing the Stripe payload and calling
stripe.checkout.sessions.create; if customerId is missing or empty, immediately
throw a descriptive error (e.g., BadRequest or a clear InternalServerError)
indicating that customerId is required so the code does not send a malformed
request to Stripe.

Comment on lines +33 to +38
const lock = createLock({
adapter: new IoredisAdapter(redisClient),
key: `userSync:${netId}`,
retryAttempts: 5,
retryDelay: 300,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Set an explicit lock TTL and reduce work under the lock.

Add a clear duration to avoid silent expiry during network calls. Also check for lock loss both before and after external calls.

   const lock = createLock({
     adapter: new IoredisAdapter(redisClient),
     key: `userSync:${netId}`,
+    duration: 15000, // 15s; tune based on p95 of the section below
     retryAttempts: 5,
     retryDelay: 300,
   });

And add a second abort check just before the final Dynamo write (see diff further below).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const lock = createLock({
adapter: new IoredisAdapter(redisClient),
key: `userSync:${netId}`,
retryAttempts: 5,
retryDelay: 300,
});
const lock = createLock({
adapter: new IoredisAdapter(redisClient),
key: `userSync:${netId}`,
duration: 15000, // 15s; tune based on p95 of the section below
retryAttempts: 5,
retryDelay: 300,
});
🤖 Prompt for AI Agents
In src/api/functions/sync.ts around lines 33 to 38, the created lock is missing
an explicit TTL and too much work runs while the lock is held; set a clear TTL
on the lock (e.g. ttl in ms) so it cannot silently expire during network calls,
minimize the work done inside the locked section by moving long external/network
calls out of the critical region, and add checks for lock ownership both
immediately after the external calls return and again just before the final
Dynamo write (abort if the lock was lost) so we never proceed with the final
write when we no longer hold the lock.

Comment on lines +70 to +83
if (!stripeCustomerId) {
if (signal.aborted) {
throw new InternalServerError({
message:
"Checked on lock before creating Stripe customer, we've lost the lock!",
});
}
const newStripeCustomerId = await createStripeCustomer({
email: userId,
name: `${firstName} ${lastName}`,
stripeApiKey,
});
logger.info(`Created new Stripe customer for ${userId}.`);
const newInfo = await dynamoClient.send(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent duplicate Stripe customers: use an idempotency key when creating the customer.

If the lock expires mid‑flow, concurrent runs can each create a Stripe customer. Protect the Stripe call with a deterministic idempotency key tied to the user.

-      const newStripeCustomerId = await createStripeCustomer({
-        email: userId,
-        name: `${firstName} ${lastName}`,
-        stripeApiKey,
-      });
+      const idempotencyKey = `acm-create-stripe-customer:${userId}`;
+      const newStripeCustomerId = await createStripeCustomer({
+        email: userId,
+        name: `${firstName} ${lastName}`,
+        stripeApiKey,
+        metadata: { userId },
+        idempotencyKey,
+      });

Companion changes are proposed in src/api/functions/stripe.ts to accept and pass the idempotency key.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/api/functions/sync.ts around lines 70 to 83, the Stripe customer creation
is vulnerable to duplicates if the lock expires; generate a deterministic
idempotency key (e.g., HMAC/SHA256 of the user id or user id + tenant) and pass
it into createStripeCustomer as an extra argument, then forward that key to
stripe.ts so the Stripe API request includes it in the Idempotency-Key header;
keep the existing lock/abort check and use the deterministic key for all
concurrent attempts for the same user to prevent duplicate customers.

Comment on lines +83 to +99
const newInfo = await dynamoClient.send(
new UpdateItemCommand({
TableName: genericConfig.UserInfoTable,
Key: {
id: { S: userId },
},
UpdateExpression: "SET #stripeCustomerId = :stripeCustomerId",
ExpressionAttributeNames: {
"#stripeCustomerId": "stripeCustomerId",
},
ExpressionAttributeValues: {
":stripeCustomerId": { S: newStripeCustomerId },
},
ReturnValues: "ALL_NEW",
}),
);
return newInfo && newInfo.Attributes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Make the stripeCustomerId write conditional; re‑read on conflict.

Ensure only the first writer sets the field; others should not clobber and should read the winning value.

-      const newInfo = await dynamoClient.send(
-        new UpdateItemCommand({
+      if (signal.aborted) {
+        throw new InternalServerError({ message: "Lost lock before persisting Stripe customer ID." });
+      }
+      try {
+        const newInfo = await dynamoClient.send(
+          new UpdateItemCommand({
             TableName: genericConfig.UserInfoTable,
             Key: {
               id: { S: userId },
             },
             UpdateExpression: "SET #stripeCustomerId = :stripeCustomerId",
             ExpressionAttributeNames: {
               "#stripeCustomerId": "stripeCustomerId",
             },
             ExpressionAttributeValues: {
               ":stripeCustomerId": { S: newStripeCustomerId },
             },
+            ConditionExpression: "attribute_not_exists(#stripeCustomerId)",
             ReturnValues: "ALL_NEW",
-        }),
-      );
-      return newInfo && newInfo.Attributes
-        ? unmarshall(newInfo.Attributes)
-        : updateResult && updateResult.Attributes
-          ? unmarshall(updateResult.Attributes)
-          : undefined;
+          }),
+        );
+        return newInfo && newInfo.Attributes
+          ? unmarshall(newInfo.Attributes)
+          : updateResult && updateResult.Attributes
+            ? unmarshall(updateResult.Attributes)
+            : undefined;
+      } catch (err: any) {
+        // Another writer won the race; fetch the latest record.
+        const latest = await dynamoClient.send(
+          new UpdateItemCommand({
+            TableName: genericConfig.UserInfoTable,
+            Key: { id: { S: userId } },
+            // no-op update to get ALL_NEW without needing an extra import
+            UpdateExpression: "SET #updatedAt = :updatedAt",
+            ExpressionAttributeNames: { "#updatedAt": "updatedAt" },
+            ExpressionAttributeValues: { ":updatedAt": { S: new Date().toISOString() } },
+            ReturnValues: "ALL_NEW",
+          }),
+        );
+        return latest && latest.Attributes ? unmarshall(latest.Attributes) : undefined;
+      }

Note: If preferred, switch to GetItem for a pure read.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/api/functions/sync.ts around lines 83 to 99, the UpdateItem
unconditionally overwrites stripeCustomerId; change it to a conditional write
that only sets stripeCustomerId when it does not yet exist (e.g. use
ConditionExpression like "attribute_not_exists(#stripeCustomerId)" or check for
NULL) so only the first writer succeeds; on ConditionalCheckFailedException
re-read the item (GetItem) and return the existing stripeCustomerId/Attributes
instead of clobbering them; ensure errors are handled and the successful path
returns the updated Attributes.

Comment on lines +112 to 115
redisClient: fastify.redisClient,
stripeApiKey: fastify.secretConfig.stripe_secret_key,
logger: request.log,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate Stripe secret before calling downstream.

Fail fast with a clear error if stripe_secret_key is unset.

         await syncFullProfile({
+          ...(fastify.secretConfig.stripe_secret_key
+            ? {}
+            : (() => {
+                request.log.error("Missing Stripe secret key.");
+                throw new InternalServerError({ message: "Stripe is not configured." });
+              })()),
           uinHash,
           firstName: givenName,
           lastName: surname,
           netId,
           dynamoClient: fastify.dynamoClient,
           redisClient: fastify.redisClient,
           stripeApiKey: fastify.secretConfig.stripe_secret_key,
           logger: request.log,
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
redisClient: fastify.redisClient,
stripeApiKey: fastify.secretConfig.stripe_secret_key,
logger: request.log,
});
await syncFullProfile({
...(fastify.secretConfig.stripe_secret_key
? {}
: (() => {
request.log.error("Missing Stripe secret key.");
throw new InternalServerError({ message: "Stripe is not configured." });
})()),
uinHash,
firstName: givenName,
lastName: surname,
netId,
dynamoClient: fastify.dynamoClient,
redisClient: fastify.redisClient,
stripeApiKey: fastify.secretConfig.stripe_secret_key,
logger: request.log,
});
🤖 Prompt for AI Agents
In src/api/routes/syncIdentity.ts around lines 112 to 115, the code passes
fastify.secretConfig.stripe_secret_key into downstream code without validating
it; add an early check for the presence (non-empty) of stripe_secret_key before
calling the downstream constructor, and if it is missing log an error and fail
fast by returning a clear error response (e.g., throw
fastify.httpErrors.internal('Stripe secret key not configured') or
reply.code(500).send(...)) so the route never proceeds with an undefined/empty
key.

Comment on lines +145 to 151
const userData = await savePromise;
if (!userData) {
request.log.error(
"Was expecting to get a user data save, but we didn't!",
);
throw new InternalServerError({});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Also ensure stripeCustomerId exists before checkout.

If the customer create/persist failed, fail fast.

         const userData = await savePromise;
         if (!userData) {
           request.log.error(
             "Was expecting to get a user data save, but we didn't!",
           );
           throw new InternalServerError({});
         }
+        if (!userData.stripeCustomerId) {
+          request.log.error("Missing stripeCustomerId on user record.");
+          throw new InternalServerError({ message: "Unable to initialize Stripe customer." });
+        }
🤖 Prompt for AI Agents
In src/api/routes/v2/membership.ts around lines 145 to 151, after awaiting
savePromise you must verify that the persisted userData contains a
stripeCustomerId and fail fast if it does not; update the block so that if
userData is falsy or userData.stripeCustomerId is missing you log a descriptive
error (include user id/email/context from the request), and throw an
InternalServerError (or appropriate HTTP error) to stop checkout flow instead of
proceeding.

devksingh4 and others added 3 commits October 26, 2025 22:22
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@devksingh4 devksingh4 merged commit cfe9bb8 into main Oct 27, 2025
8 checks passed
@devksingh4 devksingh4 deleted the dsingh14/customer-tracking branch October 27, 2025 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants