Conversation
…account-id-from-schema
There was a problem hiding this comment.
Greptile Summary
This review covers only the changes made since the last review, not the entire PR. The most recent changes focus on addressing webhook signature validation and test improvements, along with some minor code quality enhancements.
The key updates include fixing webhook signature generation in tests by properly handling JSON serialization - the tests now create payload objects and stringify them consistently for both signature generation and request bodies. Previously, the tests were double-JSON-encoding payloads, causing signature mismatches.
Additional improvements include better error handling patterns with throwErr instead of non-null assertions, proper validation of numeric inputs in payment processing, and enhanced test coverage with snapshot assertions instead of strict equality checks for response bodies.
These changes build upon the existing one-time payments implementation by ensuring the webhook handling system works reliably with proper signature validation, which is critical for securely processing Stripe webhook events in the payment infrastructure.
Confidence score: 4/5
- This PR is safe to merge with minimal risk as the changes primarily fix test reliability and improve error handling
- Score reflects focused improvements to existing functionality rather than major architectural changes
- Pay close attention to webhook signature validation tests to ensure they accurately simulate Stripe's behavior
11 files reviewed, 5 comments
apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts
Outdated
Show resolved
Hide resolved
apps/backend/prisma/migrations/20250827200446_one_time_purchase/migration.sql
Show resolved
Hide resolved
apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts
Show resolved
Hide resolved
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (5)
apps/backend/prisma/schema.prisma (1)
798-810: Add idempotency and integrity constraints to OneTimePurchase (webhook retries, race safety).Current model lacks Stripe intent linkage and uniqueness guards; concurrent webhook retries can duplicate entitlements.
Apply:
model OneTimePurchase { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid customerId String customerType CustomerType offerId String? offer Json - quantity Int - createdAt DateTime @default(now()) + quantity Int @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // Stripe linkage for idempotency/reconciliation + stripePaymentIntentId String? creationSource PurchaseCreationSource @@id([tenancyId, id]) + // Prevent duplicate recording of the same Stripe intent per tenancy + @@unique([tenancyId, stripePaymentIntentId]) + // Enforce one OTP per customer per offer (NULL offerId allowed for inline offers) + @@unique([tenancyId, customerType, customerId, offerId]) + // Query patterns used by validations + @@index([tenancyId, customerId, createdAt]) + @@index([tenancyId, offerId]) }Optional (for scalable group rules): persist groupId alongside offer to avoid JSON scans and enable future uniqueness by group.
apps/backend/src/lib/payments.tsx (1)
164-182: Guard against malformed stored offer JSON when reading OTPs.Casting p.offer without validation can break if legacy or corrupted rows exist. Add a light runtime guard to skip bad rows.
Apply:
- for (const p of oneTimePurchases) { - const offer = p.offer as yup.InferType<typeof offerSchema>; - const inc = getOrUndefined(offer.includedItems, options.itemId); + for (const p of oneTimePurchases) { + if (!p.offer || typeof p.offer !== 'object' || !('includedItems' in (p.offer as any))) continue; + const inc = getOrUndefined((p.offer as any).includedItems ?? {}, options.itemId);Optional: import and use offerSchema.validateSync for stricter safety (requires changing the type-only import to a value import).
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (3)
57-57: Guard JSON.parse for malformed metadata.offer.Untrusted metadata can break the handler.
- const offer = JSON.parse(metadata.offer || "{}"); + let offer: unknown = {}; + try { + offer = JSON.parse(metadata.offer ?? "{}"); + } catch { + offer = {}; + }
67-77: Make one-time purchase processing idempotent using PaymentIntent ID + transaction.Without persisting payment_intent ID, retried webhooks will create duplicates. Store it and guard via unique index; perform writes in a single transaction.
- await prisma.oneTimePurchase.create({ - data: { - tenancyId: tenancy.id, - customerId: customerIdMeta, - customerType: typedToUppercase(customerTypeMeta), - offerId: metadata.offerId || null, - offer, - quantity: qty, - creationSource: "PURCHASE_PAGE", - }, - }); + await prisma.$transaction(async (tx) => { + const existing = await tx.oneTimePurchase.findFirst({ + where: { tenancyId: tenancy.id, stripePaymentIntentId: object.id }, + select: { id: true }, + }); + if (existing) return; + await tx.oneTimePurchase.create({ + data: { + tenancyId: tenancy.id, + customerId: customerIdMeta, + customerType: typedToUppercase(customerTypeMeta), + offerId: metadata.offerId || null, + offer, + quantity: qty, + creationSource: "PURCHASE_PAGE", + stripePaymentIntentId: object.id, + }, + }); + });Note: Requires the migration to add stripePaymentIntentId + unique index.
60-60: Sanitize quantity; current Math.max with Number can produce NaN and crash DB writes.Math.max(1, NaN) => NaN; Prisma insert on integer column will fail.
- const qty = Math.max(1, Number(metadata.purchaseQuantity || 1)); + const rawQty = Number(metadata.purchaseQuantity); + const qty = Number.isFinite(rawQty) && rawQty >= 1 ? Math.floor(rawQty) : 1;
🧹 Nitpick comments (5)
apps/backend/src/lib/payments.test.tsx (1)
782-883: Add tests for add-on guard and non-stackable quantity.You’re missing assertions for:
- Blocking add-on when base offer absent.
- Rejecting quantity > 1 for non-stackable offers.
Example additions:
it('blocks add-on when base offer is missing', async () => { const tenancy = createMockTenancy({ items: {}, offers: {}, groups: { g1: { displayName: 'G1' } } }); const prisma = createMockPrisma({ oneTimePurchase: { findMany: async () => [] }, subscription: { findMany: async () => [] } } as any); await expect(validatePurchaseSession({ prisma, tenancy, codeData: { tenancyId: tenancy.id, customerId: 'cust-1', offerId: 'addon', offer: { displayName: 'Addon', groupId: 'g1', customerType: 'custom', freeTrial: undefined, serverOnly: false, stackable: false, prices: 'include-by-default', includedItems: {}, isAddOnTo: { base1: true }, }, }, priceId: 'price-any', quantity: 1, })).rejects.toThrowError('This offer is an add-on to an offer that the customer does not have'); }); it('rejects quantity > 1 for non-stackable offers', async () => { const tenancy = createMockTenancy({ items: {}, offers: {}, groups: {} }); const prisma = createMockPrisma({ oneTimePurchase: { findMany: async () => [] }, subscription: { findMany: async () => [] } } as any); await expect(validatePurchaseSession({ prisma, tenancy, codeData: { tenancyId: tenancy.id, customerId: 'cust-1', offerId: 'off', offer: { displayName: 'O', groupId: undefined, customerType: 'custom', freeTrial: undefined, serverOnly: false, stackable: false, prices: 'include-by-default', includedItems: {}, isAddOnTo: false }, }, priceId: 'price-any', quantity: 2, })).rejects.toThrowError('This offer is not stackable; quantity must be 1'); });apps/backend/src/lib/payments.tsx (1)
433-442: Group-level OTP guard relies on JSON; consider persisting groupId.Reading groupId from JSON for every row is fine short-term. For scale, denormalize groupId into OneTimePurchase to avoid JSON reads and enable future uniqueness-by-group if needed.
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (1)
12-31: Separate subscription vs payment_intent events for clarity.
subscriptionChangedEventscurrently includes payment_intent.*; consider splitting arrays or renaming to avoid confusion.-const subscriptionChangedEvents = [ +const subscriptionChangedEvents = [ "checkout.session.completed", "customer.subscription.created", ... - "payment_intent.succeeded", - "payment_intent.payment_failed", - "payment_intent.canceled", ] as const; + +const paymentIntentEvents = [ + "payment_intent.succeeded", + "payment_intent.payment_failed", + "payment_intent.canceled", +] as const;Also applies to: 82-94
apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx (2)
51-63: Optional: wrap DB write in a short transaction to avoid partial writes under concurrent requests.Low risk in TEST_MODE, but a transaction reduces flakiness if multiple admins hit the endpoint simultaneously.
- await prisma.oneTimePurchase.create({ - data: { - tenancyId: auth.tenancy.id, - customerId: data.customerId, - customerType: typedToUppercase(data.offer.customerType), - offerId: data.offerId, - offer: data.offer, - quantity, - creationSource: "TEST_MODE", - }, - }); + await prisma.$transaction((tx) => + tx.oneTimePurchase.create({ + data: { + tenancyId: auth.tenancy.id, + customerId: data.customerId, + customerType: typedToUppercase(data.offer.customerType), + offerId: data.offerId, + offer: data.offer, + quantity, + creationSource: "TEST_MODE", + }, + }) + );
65-79: Cancellation + creation ordering is fine; consider transactional DB update for the create path only.External Stripe calls can’t be wrapped in Prisma transactions, but using a transaction for the subsequent Prisma create keeps DB state consistent if multiple conflicting requests happen.
Also applies to: 83-97
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/backend/prisma/migrations/20250911202523_one_time_payments/migration.sql(1 hunks)apps/backend/prisma/schema.prisma(3 hunks)apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx(3 hunks)apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx(3 hunks)apps/backend/src/lib/payments.test.tsx(3 hunks)apps/backend/src/lib/payments.tsx(2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer ES6 Map over Record when representing key–value collections
Files:
apps/backend/src/lib/payments.tsxapps/backend/src/lib/payments.test.tsxapps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsxapps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
**/*.test.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
In tests, prefer .toMatchInlineSnapshot where possible; refer to snapshot-serializer.ts for snapshot formatting and handling of non-deterministic values
Files:
apps/backend/src/lib/payments.test.tsx
apps/backend/src/app/api/latest/**
📄 CodeRabbit inference engine (AGENTS.md)
apps/backend/src/app/api/latest/**: Organize backend API routes by resource under /api/latest (e.g., auth at /api/latest/auth/, users at /api/latest/users/, teams at /api/latest/teams/, oauth providers at /api/latest/oauth-providers/)
Use the custom route handler system in the backend to ensure consistent API responses
Files:
apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsxapps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
🧬 Code graph analysis (4)
apps/backend/src/lib/payments.tsx (5)
packages/stack-shared/src/utils/strings.tsx (1)
typedToUppercase(30-33)packages/stack-shared/src/schema-fields.ts (1)
offerSchema(568-591)packages/stack-shared/src/utils/objects.tsx (3)
getOrUndefined(543-545)typedEntries(263-265)typedKeys(304-306)packages/stack-shared/src/utils/dates.tsx (1)
FAR_FUTURE_DATE(201-201)packages/stack-shared/src/utils/errors.tsx (1)
StatusError(152-261)
apps/backend/src/lib/payments.test.tsx (1)
apps/backend/src/lib/payments.tsx (2)
getItemQuantityForCustomer(138-237)validatePurchaseSession(365-457)
apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx (5)
apps/backend/src/lib/payments.tsx (1)
validatePurchaseSession(365-457)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError(69-85)packages/stack-shared/src/utils/strings.tsx (1)
typedToUppercase(30-33)apps/backend/src/lib/stripe.tsx (1)
getStripeForAccount(18-37)packages/stack-shared/src/utils/dates.tsx (1)
addInterval(197-199)
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (6)
packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError(69-85)apps/backend/src/lib/stripe.tsx (2)
getStackStripe(16-16)syncStripeSubscriptions(39-104)apps/backend/src/lib/tenancies.tsx (1)
getTenancy(68-77)apps/backend/src/prisma-client.tsx (1)
getPrismaClientForTenancy(64-66)packages/stack-shared/src/utils/arrays.tsx (1)
typedIncludes(3-5)packages/stack-shared/src/utils/strings.tsx (1)
typedToUppercase(30-33)
⏰ 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). (9)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: setup-tests
- GitHub Check: lint_and_build (latest)
- GitHub Check: docker
- GitHub Check: all-good
- GitHub Check: docker
- GitHub Check: Security Check
🔇 Additional comments (12)
apps/backend/src/lib/payments.test.tsx (4)
3-3: Import changes look correct.Bringing validatePurchaseSession into this suite is appropriate.
15-17: Mock for oneTimePurchase added correctly.The stub shape matches usage in tests.
723-779: Good coverage for OTP → ledger integration.Tests correctly assert includedItems × quantity accumulation.
885-934: Comprehensive combined-sources test reads well.Asserts interplay of OTP + manual + subscription precisely.
apps/backend/src/lib/payments.tsx (5)
385-390: Early customer existence check is a good move.Reduces wasted work and clarifies error paths.
394-395: Price lookup via Map is clean and correct.No interval requirement matches new one-time flow.
412-415: Race window for duplicate OTPs.App-level checks can be bypassed by concurrent requests/webhook retries. Enforce DB uniqueness on (tenancyId, customerType, customerId, offerId) and Stripe intent ID to close the gap.
Confirm migrations include these constraints once added to the schema.
425-428: Add-on guard logic looks correct.Filtering against existing subscriptions’ offerIds is precise.
446-453: Conflict detection updated correctly for non-interval offers.Excluding include-by-default and unrelated add-ons is appropriate.
apps/backend/prisma/migrations/20250911202523_one_time_payments/migration.sql (1)
22-23: Resolved — CustomerType enum includes CUSTOMapps/backend/prisma/schema.prisma defines CustomerType with CUSTOM (lines 739–743); no enum-change required in this migration.
apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx (2)
40-49: Good guard: fail fast when price resolution fails.Erroring early on missing selectedPrice in TEST_MODE is appropriate.
25-26: Nice: request schema validates quantity as integer ≥ 1.Aligns with server-side validation.
apps/backend/prisma/migrations/20250911202523_one_time_payments/migration.sql
Outdated
Show resolved
Hide resolved
apps/backend/prisma/migrations/20250911202523_one_time_payments/migration.sql
Outdated
Show resolved
Hide resolved
apps/backend/prisma/migrations/20250911202523_one_time_payments/migration.sql
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts (1)
57-67: Revisit 200 on invalid signature.Returning 2xx for invalid signatures can mask signature failures. If intentional, ensure no side-effects are applied on bad sigs and document rationale.
Would you like a follow-up test that asserts no ItemQuantityChange/OneTimePurchase is written when signature is invalid?
🧹 Nitpick comments (6)
apps/e2e/tests/backend/backend-helpers.ts (1)
98-98: Narrow the “metadata” skip to Stripe payloads only.Skipping every metadata object can hide snake_case regressions in our own APIs. Limit the skip to Stripe’s req/res paths.
Apply:
- if (["client_metadata", "server_metadata", "options_json", "credential", "authentication_response", "metadata"].includes(key)) continue; + // Allow external payload blobs where camelCase is expected (e.g., Stripe .data.object.metadata) + if (["client_metadata", "server_metadata", "options_json", "credential", "authentication_response"].includes(key)) continue; + if (key === "metadata" && (path === "req.body.data.object" || path === "res.body.data.object")) continue;apps/backend/prisma/schema.prisma (1)
798-812: Add updatedAt and useful indexes; consider enforcing quantity > 0.Operationally handy for reconciliation and queries; minor integrity guard.
Apply:
model OneTimePurchase { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid customerId String customerType CustomerType offerId String? offer Json quantity Int stripePaymentIntentId String? createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt creationSource PurchaseCreationSource @@id([tenancyId, id]) @@unique([tenancyId, stripePaymentIntentId]) + @@index([tenancyId, customerId, createdAt]) + @@index([tenancyId, offerId]) }Optionally add a DB check in the migration: CHECK (quantity > 0).
apps/backend/prisma/migrations/20250911230246_one_time_purchase/migration.sql (1)
5-22: Add updatedAt, query indexes, and a quantity check.Keeps parity with other models and speeds common lookups.
Apply:
CREATE TABLE "OneTimePurchase" ( "id" UUID NOT NULL, "tenancyId" UUID NOT NULL, "customerId" TEXT NOT NULL, "customerType" "CustomerType" NOT NULL, "offerId" TEXT, "offer" JSONB NOT NULL, "quantity" INTEGER NOT NULL, "stripePaymentIntentId" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "creationSource" "PurchaseCreationSource" NOT NULL, CONSTRAINT "OneTimePurchase_pkey" PRIMARY KEY ("tenancyId","id") ); -- CreateIndex CREATE UNIQUE INDEX "OneTimePurchase_tenancyId_stripePaymentIntentId_key" ON "OneTimePurchase"("tenancyId", "stripePaymentIntentId"); +CREATE INDEX "OneTimePurchase_customerId_createdAt_idx" ON "OneTimePurchase"("tenancyId","customerId","createdAt"); +CREATE INDEX "OneTimePurchase_offerId_idx" ON "OneTimePurchase"("tenancyId","offerId"); +ALTER TABLE "OneTimePurchase" ADD CONSTRAINT "OneTimePurchase_quantity_positive" CHECK ("quantity" > 0);Note: Prisma @updatedat will keep updatedAt fresh on writes via Prisma.
apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts (3)
12-25: Prefer Map for headers per guidelines.Switch to Map and Object.fromEntries at the call site.
- const headers: Record<string, string> = { "content-type": "application/json" }; + const headers = new Map<string, string>([["content-type", "application/json"]]); if (!options?.omitSignature) { let header: string; if (options?.invalidSignature) { header = `t=${timestamp},v1=dead`; } else { const hmac = createHmac("sha256", options?.secret ?? stripeWebhookSecret); hmac.update(`${timestamp}.${JSON.stringify(payload)}`); const signature = hmac.digest("hex"); header = `t=${timestamp},v1=${signature}`; } - headers["stripe-signature"] = header; + headers.set("stripe-signature", header); } return await niceBackendFetch("/api/latest/integrations/stripe/webhooks", { method: "POST", - headers, + headers: Object.fromEntries(headers), body: payload, });
41-43: Use inline snapshots for bodies in tests.Aligns with our test guideline.
- expect(res.body).toEqual({ received: true }); + expect(res.body).toMatchInlineSnapshot(` + { + "received": true, + } + `);Apply similarly to the other occurrences shown in this comment.
Also applies to: 53-55, 65-67, 86-88, 154-156, 158-160
161-167: Good dedup assertion.Validates idempotency via quantity observation. Consider also asserting that a single OneTimePurchase exists if you expose an admin read endpoint.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/backend/prisma/migrations/20250911230246_one_time_purchase/migration.sql(1 hunks)apps/backend/prisma/schema.prisma(3 hunks)apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx(3 hunks)apps/e2e/tests/backend/backend-helpers.ts(1 hunks)apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer ES6 Map over Record when representing key–value collections
Files:
apps/e2e/tests/backend/backend-helpers.tsapps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts
**/*.test.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
In tests, prefer .toMatchInlineSnapshot where possible; refer to snapshot-serializer.ts for snapshot formatting and handling of non-deterministic values
Files:
apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts
🧬 Code graph analysis (1)
apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts (2)
apps/e2e/tests/backend/backend-helpers.ts (1)
niceBackendFetch(107-171)apps/e2e/tests/helpers.ts (1)
it(10-10)
⏰ 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). (9)
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: docker
- GitHub Check: lint_and_build (latest)
- GitHub Check: docker
- GitHub Check: all-good
- GitHub Check: Security Check
🔇 Additional comments (4)
apps/backend/prisma/schema.prisma (2)
756-759: Enum rename LGTM.Values look consistent; no issues.
776-776: Subscription.creationSource type update is correct.Matches the enum rename, preserves semantics.
apps/backend/prisma/migrations/20250911230246_one_time_purchase/migration.sql (1)
1-3: Safe enum rename.ALTER TYPE … RENAME preserves data; good.
apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts (1)
133-152: Webhook metadata shape is correct for ONE_TIME flow.The PaymentIntent.id reuse drives dedup. Looks good.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/backend/src/lib/stripe.tsx (1)
91-96: Safe-parse subscription metadata to avoid webhook crashes
JSON.parse(subscription.metadata.offer)can throw if metadata is missing/malformed, taking down the sync. Use a safe parser with fallback.- offer: JSON.parse(subscription.metadata.offer), + offer: safeParseOffer(subscription.metadata.offer), @@ - offer: JSON.parse(subscription.metadata.offer), + offer: safeParseOffer(subscription.metadata.offer),Add this helper in the module (outside the shown ranges):
function safeParseOffer(input: unknown): any { if (typeof input !== "string") return {}; try { return JSON.parse(input); } catch { return {}; } }Also applies to: 101-110
♻️ Duplicate comments (2)
apps/backend/src/lib/stripe-proxy.tsx (1)
35-37: Null handling in object check — guard againstnullto avoid Proxy TypeError
typeof value === "object"matchesnull, causingcreateStripeProxy(null, …)→ runtime error. Add a null guard.- // Recurse into sub-objects - if (typeof value === "object") { + // Recurse into sub-objects (null-safe) + if (value && typeof value === "object") { return createStripeProxy(value as object, overrides, [...path, String(prop)]); }apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (1)
57-59: Harden parsing: safe JSON.parse and sanitize quantityMalformed JSON in
metadata.offeror non-numericpurchaseQuantitycan throw/produce NaN and break the webhook.- const offer = JSON.parse(metadata.offer || "{}"); - const qty = Math.max(1, Number(metadata.purchaseQuantity || 1)); + let offer: any; + try { + offer = JSON.parse(metadata.offer ?? "{}"); + } catch { + offer = {}; + } + const rawQty = Number(metadata.purchaseQuantity); + const qty = Number.isFinite(rawQty) && rawQty >= 1 ? Math.floor(rawQty) : 1;
🧹 Nitpick comments (4)
apps/backend/src/lib/stripe-proxy.tsx (2)
3-3: Consider Map over Record for overrides (guideline)Per repo guideline, prefer ES6 Map for key–value collections. Optional refactor:
type StripeOverridesMap = Map<string, Record<string, unknown>>and replacegetOrUndefinedwith directoverrides.get(key).
1-1: Filename extension nit: .ts instead of .tsxThe file has no JSX. Rename to
.tsfor clarity.apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (2)
45-47: Remove stray console.logAvoid noisy logs in hot webhook paths.
- console.log("Processing1", mockData);
91-103: Prevent unnecessary subscription sync for one‑time PaymentIntents
payment_intent.*is insubscriptionChangedEvents, so you’ll also callsyncStripeSubscriptionsfor one‑time purchases. Bail out for ONE_TIME intents to avoid extra Stripe calls.if (isSubscriptionChangedEvent(event)) { + // Skip subscription sync for one-time purchases + if ( + event.type.startsWith("payment_intent.") && + (event.data.object as any)?.metadata?.purchaseKind === "ONE_TIME" + ) { + return; + } const accountId = event.account; const customerId = event.data.object.customer;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx(3 hunks)apps/backend/src/lib/stripe-proxy.tsx(1 hunks)apps/backend/src/lib/stripe.tsx(4 hunks)apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer ES6 Map over Record when representing key–value collections
Files:
apps/backend/src/lib/stripe-proxy.tsxapps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsxapps/backend/src/lib/stripe.tsx
apps/backend/src/app/api/latest/**
📄 CodeRabbit inference engine (AGENTS.md)
apps/backend/src/app/api/latest/**: Organize backend API routes by resource under /api/latest (e.g., auth at /api/latest/auth/, users at /api/latest/users/, teams at /api/latest/teams/, oauth providers at /api/latest/oauth-providers/)
Use the custom route handler system in the backend to ensure consistent API responses
Files:
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
🧬 Code graph analysis (3)
apps/backend/src/lib/stripe-proxy.tsx (1)
packages/stack-shared/src/utils/objects.tsx (1)
getOrUndefined(543-545)
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (7)
packages/stack-shared/src/utils/errors.tsx (3)
StackAssertionError(69-85)StatusError(152-261)captureError(126-134)apps/backend/src/lib/stripe.tsx (3)
getStackStripe(18-23)getStripeForAccount(25-47)syncStripeSubscriptions(49-113)apps/backend/src/lib/tenancies.tsx (1)
getTenancy(68-77)apps/backend/src/prisma-client.tsx (1)
getPrismaClientForTenancy(64-66)packages/stack-shared/src/utils/arrays.tsx (1)
typedIncludes(3-5)packages/stack-shared/src/utils/strings.tsx (1)
typedToUppercase(30-33)packages/stack-shared/src/utils/env.tsx (1)
getEnvVariable(16-58)
apps/backend/src/lib/stripe.tsx (3)
apps/backend/src/lib/stripe-proxy.tsx (2)
StripeOverridesMap(3-3)createStripeProxy(5-42)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError(69-85)packages/stack-shared/src/utils/arrays.tsx (1)
typedIncludes(3-5)
⏰ 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). (8)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: setup-tests
- GitHub Check: lint_and_build (latest)
- GitHub Check: docker
- GitHub Check: all-good
- GitHub Check: Security Check
🔇 Additional comments (4)
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (3)
66-88: Idempotent upsert looks goodUpserting
oneTimePurchaseby(tenancyId, stripePaymentIntentId)is the right guard against webhook retries.
1-31: Route placement and handler usage look correctPath is under
/api/latest/...and uses the smart route handler system as per guidelines.
66-88: Verified — unique constraint for tenancyId + stripePaymentIntentId existsFound @@unique([tenancyId, stripePaymentIntentId]) in apps/backend/prisma/schema.prisma and CREATE UNIQUE INDEX "OneTimePurchase_tenancyId_stripePaymentIntentId_key" in apps/backend/prisma/migrations/20250911230246_one_time_purchase/migration.sql.
apps/backend/src/lib/stripe.tsx (1)
18-23: Override gating is correctThrowing in production when overrides are provided aligns with safety requirements.
Important
Add support for one-time payments alongside subscriptions, including database, API, and test updates.
OneTimePurchasetable created inmigration.sql.PurchaseCreationSourceenum replacesSubscriptionCreationSourceinschema.prisma.processStripeWebhookEvent()inroute.tsxhandles one-time purchase webhooks.purchase-session/route.tsxandtest-mode-purchase-session/route.tsxupdated for one-time purchases.purchase-session.test.tsandstripe-webhooks.test.ts.This description was created by
for 726ebe2. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
Bug Fixes
Tests