-
Notifications
You must be signed in to change notification settings - Fork 447
Custom item customers #855
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: dev
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a "custom" customer type across DB schema, backend payments logic, routes, shared schemas/interfaces, UI, SDKs, tests, and templates; converts several customerId fields from UUID-constrained to plain string; threads customer_type through APIs and normalizes persisted customerType to uppercase. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client/SDK
participant API as Items API
participant Payments as payments lib / service
participant DB as Prisma/DB
C->>API: GET /items/{customer_type}/{customer_id}/{item_id}
API->>DB: fetch itemConfig(item_id)
alt not found
API-->>C: 404
else found
alt mismatch (req.customer_type != itemConfig.customerType)
API-->>C: KnownErrors.ItemCustomerTypeDoesNotMatch
else match
API->>Payments: getItemQuantityForCustomer(itemId, customerId, customerType)
Payments->>DB: query subscriptions & ItemQuantityChange filtering by UPPER(customerType)
DB-->>Payments: quantity
Payments-->>API: quantity
API-->>C: 200 { quantity }
end
end
sequenceDiagram
autonumber
participant C as Client/SDK
participant API as Update Quantity API
participant Payments as payments lib / service
participant DB as Prisma/DB
C->>API: POST /items/{customer_type}/{customer_id}/{item_id}/update-quantity
API->>DB: fetch itemConfig(item_id)
alt mismatch
API-->>C: KnownErrors.ItemCustomerTypeDoesNotMatch
else match
API->>Payments: ensureCustomerExists(tenancyId, UPPER(customer_type), customer_id)
Payments-->>DB: validate user/team/custom existence or throw KnownErrors.UserNotFound/TeamNotFound
Payments->>DB: tx create ItemQuantityChange { customerType: UPPER(customer_type), customerId, ... }
DB-->>Payments: committed
Payments-->>API: success
API-->>C: 204
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (2)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ 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)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
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.
Greptile Summary
This PR implements support for custom customer types in Stack's payment system, expanding the existing user/team-based model to include arbitrary custom customer entities. The changes introduce a new 'custom' customer type alongside the existing 'user' and 'team' types throughout the payment infrastructure.
Key architectural changes include:
Database Schema Updates: A new migration adds a CUSTOM
value to the CustomerType
enum and introduces a required customerType
column to the ItemQuantityChange
table. The customerId
fields in both ItemQuantityChange
and Subscription
tables are converted from UUID to TEXT to accommodate non-UUID custom identifiers.
API Route Restructuring: Payment API endpoints have been refactored from /payments/items/{customer_id}/{item_id}
to /payments/items/{customer_type}/{customer_id}/{item_id}
, making customer type explicit in the URL structure. This provides better validation and routing capabilities.
Client/Server Interface Extensions: Both client and server interfaces now support discriminated union types for item operations, allowing methods like getItem()
and updateItemQuantity()
to accept either { userId: string }
, { teamId: string }
, or { customId: string }
parameters.
Caching and State Management: New custom item caches (_serverCustomItemsCache
, _customItemCache
) have been added to handle custom customer items separately from user and team items, maintaining performance while supporting the new customer type.
Error Handling: Error constructors for ItemCustomerTypeDoesNotMatch
and OfferCustomerTypeDoesNotMatch
have been extended to include the 'custom' type, ensuring proper validation and meaningful error messages.
The implementation maintains backward compatibility while enabling use cases where payment customers don't correspond to Stack's built-in user/team entities, such as external customer management systems or custom business models.
Confidence score: 2/5
- This PR introduces breaking changes to core payment functionality with potential data migration risks and incomplete error handling
- Score reflects concerns about missing validation functions, incomplete Stripe integration for custom customers, and database migration risks with NOT NULL column additions
- Pay close attention to the database migration file and Stripe customer metadata handling in payment routes
24 files reviewed, 2 comments
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
Outdated
Show resolved
Hide resolved
apps/backend/prisma/migrations/20250820164831_custom_customer_types/migration.sql
Show resolved
Hide resolved
Review by RecurseML🔍 Review performed on b0e7706..d6397fa
✅ Files analyzed, no issues (3)• ⏭️ Files skipped (low suspicion) (20)• |
Documentation Changes RequiredBased on the analysis provided, the following changes are required in the documentation: 1.
|
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.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/dashboard/src/components/data-table/payment-item-table.tsx (1)
112-118
: Bug: Custom customer IDs are blocked by UUID validation.For customerType "custom", IDs can be arbitrary strings. The current
uuid()
requirement prevents valid custom flows.Apply this diff to validate conditionally by customerType:
const schema = yup.object({ - customerType: yup.string().oneOf(["user", "team", "custom"]).defined().label("Customer Type"), - customerId: yup.string().uuid().defined().label("Customer ID"), + customerType: yup.string().oneOf(["user", "team", "custom"]).defined().label("Customer Type"), + customerId: yup.string().when("customerType", { + is: "custom", + then: (s) => s.trim().min(1), + otherwise: (s) => s.uuid(), + }).defined().label("Customer ID"), quantity: yup.number().defined().label("Quantity"), description: yup.string().optional().label("Description"), expiresAt: yup.date().optional().label("Expires At"), });apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts (1)
108-112
: Potentially brittle: mismatch between offer.customerType and provided customer_typeThis test sets customer_type: "team" but the configured offer has customerType: "user". If the backend validates customer_type vs offer before checking customer existence (likely per PR), this may yield an OfferCustomerTypeDoesNotMatch error instead of CUSTOMER_DOES_NOT_EXIST and cause test flakiness.
Apply one of the following to align intent (“invalid customer_id”):
Option A (recommended): keep the offer as user-type and change the body’s customer_type to "user":
- customer_type: "team", + customer_type: "user",Option B: if you want to test an invalid team ID, also change the configured offer to team to avoid offer/type mismatch. For example (not a diff since outside this hunk):
// where the offer is configured above in this test customerType: "team",apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts (1)
44-47
: Avoid cross-type Stripe customer collisions and map CUSTOM correctly.Two issues:
- The search only matches on customerId, so the same ID string across types (user/team/custom) can collide. Include customerType in the search query.
- custom currently maps to TEAM in Stripe metadata. Persist CUSTOM to keep parity with your DB enum and prevent downstream mismatches.
- const stripeCustomerSearch = await stripe.customers.search({ - query: `metadata['customerId']:'${req.body.customer_id}'`, - }); + const escapedId = req.body.customer_id.replace(/'/g, "\\'"); + const stripeCustomerType = + customerType === "user" + ? CustomerType.USER + : customerType === "team" + ? CustomerType.TEAM + : CustomerType.CUSTOM; + const stripeCustomerSearch = await stripe.customers.search({ + // Match both ID and type to avoid collisions across customer scopes + query: `metadata['customerId']:'${escapedId}' AND metadata['customerType']:'${stripeCustomerType}'`, + }); let stripeCustomer = stripeCustomerSearch.data.length ? stripeCustomerSearch.data[0] : undefined; if (!stripeCustomer) { stripeCustomer = await stripe.customers.create({ metadata: { customerId: req.body.customer_id, - customerType: customerType === "user" ? CustomerType.USER : CustomerType.TEAM, + customerType: stripeCustomerType, } }); }Also applies to: 49-55
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts (1)
25-26
: Missingallow_negative
in internalupdateItemQuantity
callsThe new route schema requires the
allow_negative
query parameter, but several internal calls toupdateItemQuantity
omit it—these will now fail validation. Please update each call to includeallow_negative
set appropriately (e.g.false
for increments,true
for decrements):• packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts:516
await this._interface.updateItemQuantity(…, { delta: options.quantity /* add allow_negative */ })• packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts:733
await app._interface.updateItemQuantity(updateOptions, { delta /* add allow_negative */ })• packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts:756
await app._interface.updateItemQuantity(updateOptions, { delta: -delta /* add allow_negative */ })Adjust these to, for example:
{ delta: options.quantity, allow_negative: false } { delta, allow_negative: false } { delta: -delta, allow_negative: true }
♻️ Duplicate comments (4)
apps/backend/prisma/migrations/20250820164831_custom_customer_types/migration.sql (1)
1-16
: Adding NOT NULL column without default can fail on non-empty tables.This will error if ItemQuantityChange has rows. Use a two-step migration: add as nullable, backfill, then set NOT NULL. Also ensure backfill uses the correct customerType derived from your data.
Here’s a safe pattern to adapt:
-- AlterEnum ALTER TYPE "CustomerType" ADD VALUE 'CUSTOM'; --- AlterTable -ALTER TABLE "ItemQuantityChange" ADD COLUMN "customerType" "CustomerType" NOT NULL, -ALTER COLUMN "customerId" SET DATA TYPE TEXT; +-- AlterTable (step 1: add nullable) +ALTER TABLE "ItemQuantityChange" + ADD COLUMN "customerType" "CustomerType", + ALTER COLUMN "customerId" SET DATA TYPE TEXT; + +-- Backfill (step 2: set proper values; adjust logic to your data model) +-- Example placeholder (set all to USER if that's guaranteed safe; otherwise join against items config) +-- UPDATE "ItemQuantityChange" ic SET "customerType" = 'USER' WHERE "customerType" IS NULL; + +-- AlterTable (step 3: enforce NOT NULL) +ALTER TABLE "ItemQuantityChange" + ALTER COLUMN "customerType" SET NOT NULL; -- AlterTable ALTER TABLE "Subscription" ALTER COLUMN "customerId" SET DATA TYPE TEXT;If the table is guaranteed empty in all environments, keeping NOT NULL in a single step is fine; otherwise, prefer the two-step approach.
packages/stack-shared/src/interface/client-interface.ts (1)
1779-1801
: Use stringifyJson per code patterns (JSON.stringify is disallowed here)This file adheres to a rule to use stringifyJson from stack-shared/utils/json. The changed line still uses JSON.stringify.
Apply within the changed range:
- body: JSON.stringify({ customer_type, customer_id, ...offerBody }), + body: stringifyJson({ customer_type, customer_id, ...offerBody }),And add the import (outside the changed hunk):
// at the top alongside ReadonlyJson import { ReadonlyJson, stringifyJson } from '../utils/json';packages/stack-shared/src/interface/server-interface.ts (1)
862-867
: Encode path segments (customerType/customerId/itemId) to avoid malformed URLs and potential routing issues.The template literal inserts unencoded values into the path. If any identifier contains reserved characters (e.g., spaces, slashes, unicode), requests can break or misroute. Encode each segment or use the project’s urlString util consistently.
- `/payments/items/${customerType}/${customerId}/${itemId}/update-quantity?${queryParams.toString()}`, + `/payments/items/${encodeURIComponent(customerType)}/${encodeURIComponent(customerId)}/${encodeURIComponent(itemId)}/update-quantity?${queryParams.toString()}`,Alternatively (for consistency with the rest of this file), you can switch to urlString for the path and append the query string:
const path = urlString`/payments/items/${customerType}/${customerId}/${itemId}/update-quantity` + `?${queryParams.toString()}`; await this.sendServerRequest(path, { /* ... */ }, null);packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)
511-518
: Remove any-cast; narrow the discriminated union safely.The current approach uses (options as any).customId. Prefer standard discriminated narrowing to retain type safety.
- async createItemQuantityChange(options: ( - { userId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } | - { teamId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } | - { customId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } - )): Promise<void> { + async createItemQuantityChange(options: ( + { userId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } | + { teamId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } | + { customId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } + )): Promise<void> { await this._interface.updateItemQuantity( - { itemId: options.itemId, ...("userId" in options ? { userId: options.userId } : ("teamId" in options ? { teamId: options.teamId } : { customId: (options as any).customId })) }, + { + itemId: options.itemId, + ...("userId" in options + ? { userId: options.userId } + : ("teamId" in options + ? { teamId: options.teamId } + : { customId: options.customId })) + }, { delta: options.quantity, expires_at: options.expiresAt, description: options.description, } ); }
🧹 Nitpick comments (17)
apps/backend/prisma/schema.prisma (2)
22-22
: Confirm unrelated change to ownerTeamId.Line 22 is marked as modified, but it’s effectively the same declaration. If this was accidental churn (e.g., whitespace/formatting), consider dropping it to reduce noise. If intentional, ignore.
753-765
: Consider indexing customerType for query efficiency.Likely hot paths filter by tenancyId + customerType + customerId + itemId (+ expiresAt). Current index omits customerType and itemId, which can degrade performance with the new dimension.
Apply this diff to add a composite index:
model ItemQuantityChange { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid customerType CustomerType customerId String itemId String quantity Int description String? expiresAt DateTime? createdAt DateTime @default(now()) @@id([tenancyId, id]) - @@index([tenancyId, customerId, expiresAt]) + @@index([tenancyId, customerId, expiresAt]) + @@index([tenancyId, customerType, customerId, itemId, expiresAt]) }apps/backend/src/lib/payments.tsx (1)
58-101
: Customer-type aware quantity calculation looks correct; add DB indexes to keep queries fastThe switch to filtering by customerType (uppercased) in both Subscription and ItemQuantityChange is correct and consistent with the new enum. To prevent regressions at scale, add composite indexes aligned with these where-clauses.
Suggested indexes (pseudo-sql/Prisma):
- Subscription: (tenancyId, customerType, customerId, status)
- ItemQuantityChange: (tenancyId, customerType, customerId, itemId, expiresAt)
These match the high-selectivity filters used by getItemQuantityForCustomer.
apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts (1)
94-113
: Optionally add coverage for mismatched customer_type vs inline_offer.customer_typeA negative test asserting OfferCustomerTypeDoesNotMatch would harden the contract.
If helpful, I can draft an additional test case that sends a mismatched pair and expects the known error response.
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts (1)
58-63
: Deduplicate the “item key” union across client/serverThe exact union type for item keys is duplicated here and in the client interface. Consider extracting a shared alias (e.g., ItemKey = { itemId: string } & ({ userId: string } | { teamId: string } | { customId: string })) in customers/index.ts and importing it in both places to avoid drift.
Example:
// packages/template/src/lib/stack-app/customers/index.ts export type ItemKey = | { itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string };Then here:
- & AsyncStoreProperty< - "item", - [{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string }], - ServerItem, - false - > + & AsyncStoreProperty<"item", [ItemKey], ServerItem, false>apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts (2)
232-238
: Avoid snapshotting dynamic purchase URL — reduce flakinessInline snapshot asserts the full URL including a generated token. Even with "", the remaining random suffix may change. Prefer asserting status + URL shape (you already do with the regex below) and skip the full inline snapshot.
Replace the snapshot with a direct status assertion:
- expect(response).toMatchInlineSnapshot(` - NiceResponse { - "status": 200, - "body": { "url": "http://localhost:8101/purchase/<stripped UUID>_cngg259jnn72d55dxfzmafzan54vcw7n429evq7bfbaa0" }, - "headers": Headers { <some fields may have been hidden> }, - } - `); + expect(response.status).toBe(200);
171-201
: Nit: duplicate customer_type in nested offer_inlineYou’re specifying customer_type at both the top-level request and inside offer_inline. If the route requires both, ignore this. Otherwise consider relying on a single source of truth to avoid divergence in future edits.
If only one is required, consider removing the redundant one in the client-path negative test to simplify payload.
Also applies to: 211-231
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx (1)
171-171
: DRY up duplicated customer type literalsThe allowed values and select options are repeated for offers and items. Consider centralizing:
- A shared union literal array (e.g., const customerTypeOptions = [{ value: "user", ...}, ...]) and reuse in both forms.
- A shared yup schema (if not already in stack-shared) imported here to avoid drift.
Example:
const customerTypeOptions = [ { value: "user", label: "User" }, { value: "team", label: "Team" }, { value: "custom", label: "Custom" }, ]; // In both schemas: yup.string().oneOf(customerTypeOptions.map(o => o.value as const)).defined().label("Customer Type");Also applies to: 233-239
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts (1)
80-85
: Avoid duplicating the item key union across client/serverSame suggestion as server interface: extract a shared ItemKey type to customers/index.ts and reuse here for consistency and easier evolution.
- & AsyncStoreProperty< - "item", - [{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string }], - Item, - false - > + & AsyncStoreProperty<"item", [ItemKey], Item, false>apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts (1)
165-172
: Reduce URL construction duplication with small helpersThe file repeats building item paths and querystrings. Consider tiny helpers to reduce duplication and make future route-shape changes safer.
Example at top of file:
function itemUrl(scope: "user" | "team" | "custom", id: string, itemId: string) { return `/api/latest/payments/items/${scope}/${id}/${itemId}`; } function updateQuantityUrl(scope: "user" | "team" | "custom", id: string, itemId: string, allowNegative: boolean) { return `${itemUrl(scope, id, itemId)}/update-quantity?allow_negative=${allowNegative ? "true" : "false"}`; }Then:
await niceBackendFetch(updateQuantityUrl("user", user.userId, "test-item", false), { ... })Also applies to: 194-199, 224-229, 255-261, 424-428
packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts (1)
79-83
: Type union looks good; consider DRYing shared fields for maintainability.Options share the same item fields; extracting a base type reduces duplication and helps future changes (e.g., adding a new optional field once).
Apply this minimal change inside the existing union (optional), or introduce shared types at file scope:
- createItemQuantityChange(options: ( - { userId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } | - { teamId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } | - { customId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } - )): Promise<void>, + createItemQuantityChange(options: ( + ({ userId: string } | { teamId: string } | { customId: string }) + & { itemId: string, quantity: number, expiresAt?: string, description?: string } + )): Promise<void>,If you prefer explicit names, add these near the top of the file and then reference them:
type ItemQuantityChangeCore = { itemId: string; quantity: number; expiresAt?: string; description?: string }; type ItemOwner = { userId: string } | { teamId: string } | { customId: string }; type CreateItemQuantityChangeOptions = ItemOwner & ItemQuantityChangeCore; // ... // createItemQuantityChange(options: CreateItemQuantityChangeOptions): Promise<void>packages/stack-shared/src/interface/server-interface.ts (1)
845-846
: Remove redundant type annotation; rely on inference.Minor cleanup: the explicit type on itemId is redundant.
- const itemId: string = options.itemId; + const { itemId } = options;packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
1206-1217
: Deduplicate getItem branch logic.This can be simplified while keeping behavior intact.
- async getItem(options: { itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string }): Promise<Item> { - const session = await this._getSession(); - let crud: ItemCrud['Client']['Read']; - if ("userId" in options) { - crud = Result.orThrow(await this._userItemCache.getOrWait([session, options.userId, options.itemId], "write-only")); - } else if ("teamId" in options) { - crud = Result.orThrow(await this._teamItemCache.getOrWait([session, options.teamId, options.itemId], "write-only")); - } else { - crud = Result.orThrow(await this._customItemCache.getOrWait([session, options.customId, options.itemId], "write-only")); - } - return this._clientItemFromCrud(crud); - } + async getItem(options: { itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string }): Promise<Item> { + const session = await this._getSession(); + const [cache, ownerId] = + "userId" in options ? [this._userItemCache, options.userId] : + "teamId" in options ? [this._teamItemCache, options.teamId] : + [this._customItemCache, options.customId]; + const crud = Result.orThrow(await cache.getOrWait([session, ownerId, options.itemId] as const, "write-only")); + return this._clientItemFromCrud(crud); + }apps/e2e/tests/js/payments.test.ts (1)
192-192
: Strengthen assertion: expect the specific KnownError.Asserting the precise error type improves signal and avoids false positives.
- await expect(adminApp.createItemQuantityChange({ teamId: team.id, itemId, quantity: -1 })) - .rejects.toThrow(); + await expect( + adminApp.createItemQuantityChange({ teamId: team.id, itemId, quantity: -1 }) + ).rejects.toThrow(KnownErrors.ItemQuantityInsufficientAmount);Add this import at the top of the file:
import { KnownErrors } from "@stackframe/stack-shared";apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts (1)
20-23
: Prefer shared schema: use customerTypeSchema for a single source of truth.Inline oneOf duplicates definitions across the codebase. Importing the shared schema keeps enum values consistent with future changes.
- customer_type: yupString().oneOf(["user", "team", "custom"]).defined(), + customer_type: customerTypeSchema.defined(),Add to the existing schema-fields import:
import { adaptSchema, clientOrHigherAuthTypeSchema, yupNumber, yupObject, yupString, customerTypeSchema } from "@stackframe/stack-shared/dist/schema-fields";apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts (1)
22-24
: Add light schema validation forcustomer_id
Although we escape single quotes when constructing the Stripe query, constraining
customer_id
at the schema level helps guard against malformed or malicious inputs and avoids query errors.• File: apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
Lines: 22–24Suggested diff:
customer_type: yupString().oneOf(["user", "team", "custom"]).defined(), - customer_id: yupString().defined(), + customer_id: yupString() + .matches(/^[A-Za-z0-9_-]+$/, "customer_id may only contain letters, numbers, hyphens, and underscores") + .defined(), offer_id: yupString().optional(),apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts (1)
69-74
: Validate expires_at as an ISO date to avoid Invalid Date writes.new Date(req.body.expires_at) will produce Invalid Date for malformed input, which can bubble into runtime/DB errors. Prefer explicit date validation.
- expires_at: yupString().optional(), + // If ISO 8601 is expected, enforce it at the edge + expires_at: yupString().matches( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, + "expires_at must be an ISO 8601 UTC timestamp" + ).optional(),Alternatively, use a yup date schema if available and acceptable across the codebase.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (25)
apps/backend/prisma/migrations/20250820164831_custom_customer_types/migration.sql
(1 hunks)apps/backend/prisma/schema.prisma
(4 hunks)apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts
(4 hunks)apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts
(6 hunks)apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
(3 hunks)apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
(1 hunks)apps/backend/src/lib/payments.tsx
(3 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
(4 hunks)apps/dashboard/src/components/data-table/payment-item-table.tsx
(2 hunks)apps/e2e/tests/backend/backend-helpers.ts
(1 hunks)apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts
(8 hunks)apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts
(14 hunks)apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts
(1 hunks)apps/e2e/tests/js/payments.test.ts
(3 hunks)packages/stack-shared/src/interface/admin-interface.ts
(0 hunks)packages/stack-shared/src/interface/client-interface.ts
(3 hunks)packages/stack-shared/src/interface/server-interface.ts
(1 hunks)packages/stack-shared/src/known-errors.tsx
(2 hunks)packages/stack-shared/src/schema-fields.ts
(1 hunks)packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
(1 hunks)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
(4 hunks)packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
(6 hunks)packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
(1 hunks)packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
(2 hunks)packages/template/src/lib/stack-app/apps/interfaces/server-app.ts
(2 hunks)
💤 Files with no reviewable changes (1)
- packages/stack-shared/src/interface/admin-interface.ts
🧰 Additional context used
📓 Path-based instructions (2)
apps/backend/src/app/api/latest/**/*
📄 CodeRabbit Inference Engine (CLAUDE.md)
apps/backend/src/app/api/latest/**/*
: Main API routes are located in /apps/backend/src/app/api/latest
The project uses a custom route handler system in the backend for consistent API responses
Files:
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts
apps/backend/prisma/schema.prisma
📄 CodeRabbit Inference Engine (CLAUDE.md)
Database models use Prisma
Files:
apps/backend/prisma/schema.prisma
🧠 Learnings (1)
📚 Learning: 2025-08-04T22:25:51.260Z
Learnt from: CR
PR: stack-auth/stack-auth#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-04T22:25:51.260Z
Learning: Applies to apps/backend/prisma/schema.prisma : Database models use Prisma
Applied to files:
apps/backend/prisma/schema.prisma
🧬 Code Graph Analysis (13)
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts (2)
packages/template/src/lib/stack-app/common.ts (1)
AsyncStoreProperty
(8-10)packages/template/src/lib/stack-app/customers/index.ts (1)
ServerItem
(19-33)
apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts (3)
apps/e2e/tests/backend/backend-helpers.ts (2)
niceBackendFetch
(107-165)updateConfig
(1117-1125)docs/public/stack-auth-cli-template.py (1)
post
(20-31)packages/stack-shared/src/interface/admin-interface.ts (1)
updateConfig
(441-454)
apps/e2e/tests/js/payments.test.ts (2)
apps/e2e/tests/helpers.ts (1)
it
(10-10)apps/e2e/tests/js/js-helpers.ts (1)
createApp
(40-77)
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts (2)
packages/template/src/lib/stack-app/common.ts (1)
AsyncStoreProperty
(8-10)packages/template/src/lib/stack-app/customers/index.ts (1)
Item
(7-17)
packages/stack-shared/src/interface/server-interface.ts (2)
packages/stack-shared/src/interface/crud/items.ts (1)
ItemCrud
(24-24)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError
(69-85)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx (1)
apps/dashboard/src/components/form-fields.tsx (1)
SelectField
(229-266)
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts (3)
packages/stack-shared/src/known-errors.tsx (2)
KnownErrors
(1505-1507)KnownErrors
(1509-1625)apps/backend/src/prisma-client.tsx (1)
getPrismaClientForTenancy
(51-53)apps/backend/src/lib/payments.tsx (1)
getItemQuantityForCustomer
(58-100)
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts (1)
packages/stack-shared/src/known-errors.tsx (2)
KnownErrors
(1505-1507)KnownErrors
(1509-1625)
apps/backend/src/lib/payments.tsx (2)
packages/stack-shared/src/utils/objects.tsx (1)
getOrUndefined
(543-545)packages/stack-shared/src/utils/strings.tsx (1)
typedToUppercase
(30-33)
packages/stack-shared/src/interface/client-interface.ts (1)
packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError
(69-85)
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts (3)
packages/stack-shared/src/schema-fields.ts (1)
yupString
(187-190)packages/stack-shared/src/known-errors.tsx (2)
KnownErrors
(1505-1507)KnownErrors
(1509-1625)packages/stack-shared/src/utils/strings.tsx (1)
typedToUppercase
(30-33)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (3)
packages/template/src/lib/stack-app/apps/implementations/common.ts (2)
createCacheBySession
(29-39)useAsyncCache
(145-190)packages/stack-shared/src/interface/crud/items.ts (1)
ItemCrud
(24-24)packages/template/src/lib/stack-app/customers/index.ts (1)
Item
(7-17)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (3)
packages/template/src/lib/stack-app/apps/implementations/common.ts (2)
createCache
(22-27)useAsyncCache
(145-190)packages/stack-shared/src/interface/crud/items.ts (1)
ItemCrud
(24-24)packages/template/src/lib/stack-app/customers/index.ts (1)
ServerItem
(19-33)
🪛 Biome (2.1.2)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
[error] 1224-1224: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 1226-1226: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 1228-1228: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
[error] 1013-1013: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 1014-1014: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 1016-1016: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 1017-1017: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 1019-1019: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 1020-1020: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ 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: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: setup-tests
- GitHub Check: restart-dev-and-test
- GitHub Check: build (22.x)
- GitHub Check: all-good
- GitHub Check: docker
- GitHub Check: Security Check
🔇 Additional comments (27)
packages/stack-shared/src/schema-fields.ts (1)
547-547
: LGTM: Added "custom" to customerTypeSchema is consistent with the PR direction.Matches downstream DB enum and route handling. No further changes needed here.
apps/backend/prisma/schema.prisma (2)
718-719
: LGTM: CustomerType enum extended to include CUSTOM.This aligns with the shared schema and API updates.
735-737
: No residual UUID constraints found on Subscription.customerIdAutomated searches across all schema and code files show:
- No
@db.Uuid
usage on anycustomerId
in your *.prisma schemas- No
.uuid()
validations oncustomerId
,customer_id
, orcustomId
in TS/JS codeThe change to TEXT for
Subscription.customerId
appears safe.apps/dashboard/src/components/data-table/payment-item-table.tsx (1)
122-128
: LGTM: Payload mapping correctly emits userId/teamId/customId based on customerType.This aligns with the updated server interface.
packages/stack-shared/src/known-errors.tsx (2)
1433-1444
: LGTM: ItemCustomerTypeDoesNotMatch now supports "custom".Type unions and message payloads are correctly extended.
1476-1487
: LGTM: OfferCustomerTypeDoesNotMatch now supports "custom".Consistent with schema and API updates.
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)
85-92
: LGTM: Passing customerType="team" to getItemQuantityForCustomerCorrectly updated to the new signature; aligns with the team-admins quota check.
apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts (1)
94-113
: LGTM: Test payload includes customer_type for inline offer pathMatches the updated API contract and route validation.
apps/e2e/tests/backend/backend-helpers.ts (1)
1417-1426
: LGTM: Helper updated to send customer_type in create-purchase-urlKeeps helper aligned with the new API shape and avoids duplication across tests.
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts (1)
58-63
: Runtime parity confirmed for tri-scoped item accessThe server implementation
_StackServerAppImpl
’s methods cover all three key shapes exactly as the store property’s type:
async getItem(options: { itemId, userId } | { itemId, teamId } | { itemId, customId }): Promise<ServerItem>
(lines 995–1008)useItem(options: { itemId, userId } | { itemId, teamId } | { itemId, customId }): ServerItem
(lines 1011–1022)All cases—including
customId
—are handled. Approving these changes.apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts (1)
16-18
: customer_type added to payloads — consistent with new API contractAdding customer_type to the request body in all relevant tests is correct and matches the backend expectations. The snake_case matches the e2e helpers’ validations.
Also applies to: 56-59, 156-159, 184-201, 216-231
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx (1)
171-171
: UI accepts “custom” customer type — schema and selects updated correctly
- offerSchema customerType oneOf includes "custom"
- itemSchema customerType oneOf includes "custom"
- Both SelectField renderers expose the Custom option
Looks consistent with backend and shared schema changes.Also applies to: 213-214, 233-239
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts (1)
5-5
: Client-side item store property mirrors server-side — verifiedConfirmed: the client implementation exposes getItem/useItem and correctly handles userId, teamId and customId; the Item type is imported on the interface.
- packages/template/src/lib/stack-app/apps/interfaces/client-app.ts — import Item added (line ~5)
- packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts — caches: _userItemCache/_teamItemCache/_customItemCache (lines ~203–219); getItem implementation (lines ~1206–1216) uses _customItemCache for customId; useItem implementation (lines ~1220–1230) handles customId branch.
- packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts — server-side parity present (caches and getItem/useItem handle customId; e.g. caches ~159–175, getItem/useItem ~997–1022).
apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts (2)
18-19
: Endpoint paths updated to include customer_type segment — aligns with backend routesAll affected tests now use /items/{customer_type}/{customer_id}/{item_id}[...]. This is consistent and improves clarity across user/team/custom scopes. The admin/dashboard path update for team is also correct.
Also applies to: 56-59, 89-92, 125-128, 165-172, 194-199, 201-206, 224-229, 231-236, 255-261, 263-268, 285-289, 327-331, 359-365, 424-428, 439-453
455-489
: Great addition: coverage for custom customer type (GET and update-quantity)Good end-to-end verification of default quantity, update-quantity behavior, and subsequent aggregation for a custom customer.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (2)
215-219
: Good addition: _customItemCache aligns with “custom” scope.Cache wiring mirrors user/team caches and uses the same interface call path. Looks consistent.
1201-1202
: Verify createCheckoutUrl signature alignment end-to-end.Now passing customer type as the first argument. Ensure the underlying interface and all call sites accept (type, ownerId, offer) to prevent runtime errors.
Would you like me to run a quick repo scan to locate all createCheckoutUrl definitions/usages and confirm the updated signature is applied everywhere?
apps/e2e/tests/js/payments.test.ts (3)
51-85
: LGTM: Validates root-level getItem for team and user scopes.Good coverage of both team and user contexts, including server-side retrieval for user items.
87-107
: LGTM: Adds “custom” happy-path with admin quantity change.Nice end-to-end assertion that admin-side changes propagate to client reads for customId.
147-147
: LGTM: Updated admin quantity change to use teamId.This matches the new admin API options and keeps the test intent intact.
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts (1)
42-44
: Good: explicit customer_type vs. item-config mismatch check.Early, clear error when the requested customer scope doesn’t match the item’s configured scope. Matches KnownErrors and keeps behavior predictable.
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts (2)
49-51
: Inline type check is correct and aligned with KnownErrors.Good replacement of the helper with a direct comparison and a precise KnownErrors path.
60-61
: No action needed: customerType is uppercased internallyThe
getItemQuantityForCustomer
function usestypedToUppercase(options.customerType)
in both itssubscription.findMany
anditemQuantityChange.aggregate
queries, so passingreq.params.customer_type
(lowercase) is correctly handled.packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (4)
171-175
: Good addition: dedicated custom items cache.This mirrors user/team caches and keeps cache key structure consistent.
577-579
: Checkout URL calls updated to include owner type – aligned with new API.Passing "user"/"team" explicitly is correct per the route changes.
Also applies to: 715-717
721-737
: Item mutations correctly pass scoped identifiers and refresh the right caches.The tri-scope updateOptions construction and cache refresh logic look good. The tryDecreaseQuantity path handles insufficient balance via KnownErrors.
Also applies to: 739-748, 750-767
997-1008
: Tri-scope getItem implementation is solid.Clear branching per discriminant with consistent cache usage; returns a unified ServerItem via _serverItemFromCrud.
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
Outdated
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Outdated
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
Outdated
Show resolved
Hide resolved
apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts
Show resolved
Hide resolved
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.
Actionable comments posted: 3
🧹 Nitpick comments (6)
apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts (1)
5-27
: Consider adding at least one positive/negative case for customer_type: "custom".Since this PR adds "custom" support across the stack, adding a basic acceptance test here (valid customId path + mismatch/error path) would tighten coverage for the purchases endpoint too.
I can draft two focused tests: one success with offer_id configured for "custom", and one mismatch returning OFFER_CUSTOMER_TYPE_DOES_NOT_MATCH. Want me to add them?
Also applies to: 29-78, 134-173, 176-206, 208-239, 241-278
apps/backend/src/lib/payments.tsx (1)
103-140
: ensureCustomerExists: use UserIdDoesNotExist for users and guard “custom” IDsFor consistency and better DX:
- UserNotFound’s constructor takes zero arguments, so don’t pass customerId to it. If you want the ID in the error, swap to
throw new KnownErrors.UserIdDoesNotExist(options.customerId)
- Add an explicit guard for empty IDs when
customerType === "custom"
Changes:
• In apps/backend/src/lib/payments.tsx
@@ export async function ensureCustomerExists(options: { if (options.customerType === "user") { if (!isUuid(options.customerId)) { - throw new KnownErrors.UserNotFound(); + throw new KnownErrors.UserIdDoesNotExist(options.customerId); } const user = await options.prisma.projectUser.findUnique({ @@ if (!user) { - throw new KnownErrors.UserNotFound(); + throw new KnownErrors.UserIdDoesNotExist(options.customerId); } } else if (options.customerType === "team") { @@ - } + } else if (options.customerType === "custom") { + if (options.customerId.trim().length === 0) { + // Keep error shape consistent with other 4xx validation paths + throw new StatusError(400, "customer_id must be non-empty for custom customers"); + } + }• Add import at top if not already present:
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";apps/dashboard/src/components/data-table/payment-item-table.tsx (1)
147-151
: Updated error handling matches new backend semantics.Switching to UserNotFound/TeamNotFound is consistent with the new ensureCustomerExists flow. Consider adding a specific message for invalid/empty Custom IDs if you adopt a non-empty check server-side.
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts (1)
20-23
: Optionally enforce non-empty custom customer_id.Currently customer_id uses yupString().defined(), which allows empty strings. For "custom", that would flow through and produce changes for an empty identifier. Consider a minimal runtime check here (lighter than a schema-level conditional):
if (!itemConfig) { throw new KnownErrors.ItemNotFound(req.params.item_id); } + if (req.params.customer_type === "custom" && req.params.customer_id.trim().length === 0) { + throw new StatusError(400, "customer_id must be non-empty for custom customers"); + }You’d need to import StatusError:
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";If you’d prefer schema-level validation, we can add a conditional yup.when on customer_type.
Also applies to: 42-44
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts (2)
20-23
: End-to-end: customer_type wiring and persistence look solid; validate expires_at to avoid invalid dates.
- The route schema and mismatch check are correct.
- ensureCustomerExists and uppercasing for persistence match the DB enum.
- One potential footgun: expires_at is a free-form string. If it’s malformed, new Date(...) yields Invalid Date, which can lead to DB errors or bad data.
Strengthen the body schema or add a runtime guard:
Schema-level (minimal change using a test):
body: yupObject({ delta: yupNumber().integer().defined(), - expires_at: yupString().optional(), + expires_at: yupString() + .test("is-iso-date", "expires_at must be a valid ISO date string", v => !v || !Number.isNaN(Date.parse(v))) + .optional(), description: yupString().optional(), }).defined(),Or runtime guard before create:
const expiresAt = req.body.expires_at ? new Date(req.body.expires_at) : null; if (expiresAt && Number.isNaN(expiresAt.getTime())) { throw new StatusError(400, "expires_at must be a valid ISO date string"); }(For this runtime approach, import StatusError from @stackframe/stack-shared/dist/utils/errors.)
Also applies to: 49-51, 53-58, 65-67, 75-80
25-26
: Nit: consider a boolean query param for allow_negative.Using "true"/"false" strings is fine, but switching to a boolean in the schema (and auto-parsing) would be a small DX improvement.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts
(4 hunks)apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts
(5 hunks)apps/backend/src/lib/payments.tsx
(4 hunks)apps/dashboard/src/components/data-table/payment-item-table.tsx
(5 hunks)apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts
(8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
apps/backend/src/app/api/latest/**/*
📄 CodeRabbit Inference Engine (CLAUDE.md)
apps/backend/src/app/api/latest/**/*
: Main API routes are located in /apps/backend/src/app/api/latest
The project uses a custom route handler system in the backend for consistent API responses
Files:
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts
🧬 Code Graph Analysis (3)
apps/dashboard/src/components/data-table/payment-item-table.tsx (1)
packages/stack-shared/src/known-errors.tsx (2)
KnownErrors
(1505-1507)KnownErrors
(1509-1625)
apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts (1)
packages/stack-shared/src/utils/uuids.tsx (1)
generateUuid
(3-8)
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts (3)
packages/stack-shared/src/schema-fields.ts (1)
yupString
(187-190)packages/stack-shared/src/known-errors.tsx (2)
KnownErrors
(1505-1507)KnownErrors
(1509-1625)apps/backend/src/prisma-client.tsx (1)
getPrismaClientForTenancy
(51-53)
⏰ 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: setup-tests
- GitHub Check: docker
- GitHub Check: lint_and_build (latest)
- GitHub Check: all-good
- GitHub Check: build (22.x)
- GitHub Check: docker
- GitHub Check: restart-dev-and-test
- GitHub Check: Security Check
🔇 Additional comments (7)
apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts (2)
16-18
: Good addition: thread customer_type through all test payloads.This keeps the tests aligned with the new API contract requiring customer_type. The payload updates look consistent across scenarios.
Also applies to: 56-59, 161-164, 189-204, 221-235, 269-272
238-239
: Assertion update LGTM.Asserting on response.body.url with a regex is appropriate and keeps the snapshot compact.
apps/backend/src/lib/payments.tsx (1)
38-56
: Inline offer normalization LGTM.Mapping inline offers to the internal offer shape (currencies via SUPPORTED_CURRENCIES, includedItems normalization) is solid. This eliminates shape drift between inline and stored offers.
apps/dashboard/src/components/data-table/payment-item-table.tsx (1)
58-59
: Prop threading to ActionsCell/CreateItemQuantityChangeDialog looks good.Passing the full item to ActionsCell and threading itemId and customerType into the dialog aligns the UI with the backend customer typing.
Also applies to: 104-106
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts (3)
20-23
: Schema: good addition of customer_type parameter.oneOf(["user","team","custom"]) aligns with the expanded enum and keeps the public API explicit.
42-44
: Explicit mismatch check is clear and correct.Raising ItemCustomerTypeDoesNotMatch when the item’s configured type differs from the request param is the right behavior.
46-51
: Customer existence check + quantity computation wiring LGTM.
- ensureCustomerExists upfront prevents wasted work.
- Passing customerType into getItemQuantityForCustomer ensures DB filters line up with the enum.
Also applies to: 52-58
apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts
Show resolved
Hide resolved
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.
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)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
1308-1321
: React hook useRouter is still called conditionally — violates hooks rules.NextNavigation.useRouter must be invoked unconditionally inside this hook-like method.
Apply this refactor to call the hook once and branch afterward:
// IF_PLATFORM react-like useNavigate(): (to: string) => void { - if (typeof this._redirectMethod === "object") { - return this._redirectMethod.useNavigate(); - } else if (this._redirectMethod === "window") { - return (to: string) => window.location.assign(to); - // IF_PLATFORM next - } else if (this._redirectMethod === "nextjs") { - const router = NextNavigation.useRouter(); - return (to: string) => router.push(to); - // END_PLATFORM - } else { - return (to: string) => { }; - } + // IF_PLATFORM next + // Call the hook unconditionally to satisfy React hook rules. + const router = NextNavigation.useRouter(); + // END_PLATFORM + if (typeof this._redirectMethod === "object") { + return this._redirectMethod.useNavigate(); + } + if (this._redirectMethod === "window") { + return (to: string) => window.location.assign(to); + } + // IF_PLATFORM next + if (this._redirectMethod === "nextjs") { + return (to: string) => router.push(to); + } + // END_PLATFORM + return (_to: string) => {}; } // END_PLATFORM
♻️ Duplicate comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
1220-1229
: Conditional hook issue fixed — useAsyncCache is now called unconditionally.This addresses the prior Biome error on conditional hooks in useItem.
🧹 Nitpick comments (3)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
605-609
: Wording nit: reference the actual tokenStore values.Error message currently suggests tokenStore value 'cookies', while the code uses 'cookie' and 'nextjs-cookie'.
Apply this small wording tweak:
- throw new Error("Cannot call this function on a Stack app without a persistent token store. Make sure the tokenStore option on the constructor is set to a non-null value when initializing Stack.\n\nStack uses token stores to access access tokens of the current user. For example, on web frontends it is commonly the string value 'cookies' for cookie storage."); + throw new Error("Cannot call this function on a Stack app without a persistent token store. Make sure the tokenStore option on the constructor is set to a non-null value when initializing Stack.\n\nStack uses token stores to access access tokens of the current user. For example, on web frontends the tokenStore is commonly 'cookie' or 'nextjs-cookie'.");packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)
517-523
: Minor readability refactor: extract owner object before calling updateItemQuantity.Current nested spread is correct but a bit dense to read.
Apply this small refactor:
- await this._interface.updateItemQuantity( - { itemId: options.itemId, ...("userId" in options ? { userId: options.userId } : ("teamId" in options ? { teamId: options.teamId } : { customId: options.customId })) }, + const owner = + "userId" in options ? { userId: options.userId } : + "teamId" in options ? { teamId: options.teamId } : + { customId: options.customId }; + await this._interface.updateItemQuantity( + { itemId: options.itemId, ...owner }, { delta: options.quantity, expires_at: options.expiresAt, description: options.description, } );packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)
1011-1035
: Add missing dependencies to useMemo to avoid stale closures.The returned ServerItem captures type and id inside useMemo but only depends on result. Include id and type to be safe when the discriminator changes.
Apply this minimal fix:
- return useMemo(() => this._serverItemFromCrud({ type, id }, result), [result]); + return useMemo(() => this._serverItemFromCrud({ type, id }, result), [result, id, type]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
(3 hunks)apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts
(8 hunks)packages/stack-shared/src/interface/client-interface.ts
(4 hunks)packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
(1 hunks)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
(9 hunks)packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
(7 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts
- apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
- packages/stack-shared/src/interface/client-interface.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (3)
packages/template/src/lib/stack-app/apps/implementations/common.ts (2)
createCacheBySession
(29-39)useAsyncCache
(145-190)packages/template/src/lib/stack-app/apps/interfaces/client-app.ts (2)
StackClientApp
(35-87)StackClientApp
(102-102)packages/template/src/lib/stack-app/customers/index.ts (1)
Item
(7-17)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (3)
packages/template/src/lib/stack-app/apps/implementations/common.ts (2)
createCache
(22-27)useAsyncCache
(145-190)packages/template/src/lib/stack-app/customers/index.ts (1)
ServerItem
(19-33)packages/stack-shared/src/utils/caches.tsx (1)
AsyncCache
(67-122)
🪛 Biome (2.1.2)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
[error] 1315-1315: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ 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). (4)
- GitHub Check: build (22.x)
- GitHub Check: all-good
- GitHub Check: build (22.x)
- GitHub Check: Security Check
🔇 Additional comments (9)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (3)
216-220
: Custom item cache wiring looks correct (session-scoped, id+itemId key).The new _customItemCache follows the same pattern as user/team caches and calls interface.getItem with { customId, itemId }. No concerns.
1202-1203
: createCheckoutUrl now passes owner type — aligned with new API.Forwarding the owner type ("user" | "team") to the interface matches the breaking change in the PR objectives.
1207-1218
: Top-level getItem union overload — good addition.Selecting the correct cache by discriminator and returning the mapped Item is consistent and keeps the fetch centralized.
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (2)
511-515
: createItemQuantityChange signature updated — matches tri-scope API.Accepting {userId|teamId|customId, itemId, quantity...} aligns with backend changes. Looks good.
511-515
: No remaining usages ofcreatePurchaseUrl
detectedRan a full scan across
.ts
and.tsx
files with:rg -nP -C2 '\bcreatePurchaseUrl\s*\(' --glob '*.ts' --glob '*.tsx'No matches found—no further migration needed.
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (4)
172-176
: Server custom items cache added — consistent with user/team caches.The cache key and interface call for custom items look correct.
579-580
: createCheckoutUrl now includes owner type on server — aligned with interface changes.Passing "user"/"team" explicitly matches the updated backend signature.
Also applies to: 717-718
722-770
: ServerItem construction with tri-scope owner and cache refresh paths — LGTM.
- updateItemQuantity is called with the correct composite id.
- Cache refresh targets the correct owner cache per branch.
No functional issues spotted.
998-1009
: Top-level getItem for server with tri-scope discriminator — looks good.Centralizes server-side retrieval and returns a ServerItem bound to the correct owner context.
@@ -76,6 +77,12 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex | |||
}, | |||
} | |||
& AsyncStoreProperty<"project", [], Project, false> | |||
& AsyncStoreProperty< | |||
"item", | |||
[{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string }], |
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.
can we maybe call the "customId" customCustomerId everywhere?
[{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string }], | |
[{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customCustomerId: string }], |
@@ -54,6 +55,12 @@ export type StackServerApp<HasTokenStore extends boolean = boolean, ProjectId ex | |||
& AsyncStoreProperty<"user", [id: string], ServerUser | null, false> | |||
& Omit<AsyncStoreProperty<"users", [], ServerUser[], true>, "listUsers" | "useUsers"> | |||
& AsyncStoreProperty<"teams", [], ServerTeam[], true> | |||
& AsyncStoreProperty< | |||
"item", | |||
[{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string }], |
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.
[{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string }], | |
[{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customCustomerId: string }], |
Important
Add support for a new "custom" customer type across items, offers, and API endpoints, with updates to client and server applications.
customerTypeSchema
inschema-fields.ts
.createItemQuantityChange
inadmin-app-impl.ts
to handle custom customer types.getItem
anduseItem
methods inclient-app-impl.ts
andserver-app-impl.ts
to support custom customer types.route.ts
to includecustomer_type
parameter.createCheckoutUrl
andupdateItemQuantity
methods inclient-interface.ts
andserver-interface.ts
to acceptcustomId
.createPurchaseUrl
method fromadmin-interface.ts
.client-app.ts
andserver-app.ts
to includecustomId
.payments.test.ts
anditems.test.ts
.This description was created by
for da6f000. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
API Changes
Breaking Changes
Dashboard/UI
Tests