Skip to content

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

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
Open

Custom item customers #855

wants to merge 11 commits into from

Conversation

BilalG1
Copy link
Contributor

@BilalG1 BilalG1 commented Aug 20, 2025


Important

Add support for a new "custom" customer type across items, offers, and API endpoints, with updates to client and server applications.

  • New Features:
    • Added support for "custom" customer type in customerTypeSchema in schema-fields.ts.
    • Implemented createItemQuantityChange in admin-app-impl.ts to handle custom customer types.
    • Updated getItem and useItem methods in client-app-impl.ts and server-app-impl.ts to support custom customer types.
  • API Changes:
    • Modified item endpoints in route.ts to include customer_type parameter.
    • Updated createCheckoutUrl and updateItemQuantity methods in client-interface.ts and server-interface.ts to accept customId.
  • Breaking Changes:
    • Removed createPurchaseUrl method from admin-interface.ts.
    • Updated method signatures in client-app.ts and server-app.ts to include customId.
  • Tests:
    • Added tests for custom customer type in payments.test.ts and items.test.ts.

This description was created by Ellipsis for da6f000. You can customize this summary. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features

    • Added support for a new "custom" customer type across items, offers, quantity tracking, SDKs, and apps.
  • API Changes

    • Endpoints and payloads now include customer_type and accept non-UUID customer identifiers; validation and known errors updated to handle "custom".
  • Breaking Changes

    • Admin/SDK method signatures now accept userId/teamId/customId shapes; createPurchaseUrl removed; item quantity routes include customer_type.
  • Dashboard/UI

    • "Custom" option added to Offer/Item creation and quantity dialogs.
  • Tests

    • E2E and unit tests updated/added for custom-type flows.

Copy link

vercel bot commented Aug 20, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Aug 23, 2025 0:08am
stack-dashboard Ready Ready Preview Comment Aug 23, 2025 0:08am
stack-demo Ready Ready Preview Comment Aug 23, 2025 0:08am
stack-docs Ready Ready Preview Comment Aug 23, 2025 0:08am

Copy link
Contributor

coderabbitai bot commented Aug 20, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Adds 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

Cohort / File(s) Summary
Database migration & schema
apps/backend/prisma/migrations/.../migration.sql, apps/backend/prisma/schema.prisma
Add enum value CUSTOM; add NOT NULL ItemQuantityChange.customerType; change ItemQuantityChange.customerId and Subscription.customerId from UUID DB type to plain text.
Backend payments routes (get/update item quantity)
apps/backend/src/app/api/latest/payments/items/.../route.ts, .../update-quantity/route.ts
Add customer_type route param (`"user"
Backend purchase URL route
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
Add customer_type in body; relax customer_id to plain string; inline offer/customer-type validation with KnownErrors; remove ensureOfferCustomerTypeMatches usage.
Payments library
apps/backend/src/lib/payments.tsx
Remove helpers (ensureItemCustomerTypeMatches, ensureOfferCustomerTypeMatches, getCustomerType); require customerType in getItemQuantityForCustomer; add ensureCustomerExists; normalize customerType to uppercase in DB queries; support custom.
Team-invites flow
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
Pass customerType: "team" into getItemQuantityForCustomer when computing admin limits.
Dashboard UI & dialogs
apps/dashboard/.../page-client.tsx, apps/dashboard/.../payment-item-table.tsx
Add "Custom" option in customer-type selects/schemas; change quantity-change dialog to accept customerType and map payload to `{ userId
E2E & JS tests
apps/e2e/tests/backend/*, apps/e2e/tests/js/*
Update request bodies/paths to include customer_type; change item endpoints to /items/{customer_type}/{customer_id}/{item_id}; adjust admin quantity-change calls to use teamId/customId; add custom-type tests and update snapshots/timeouts.
Shared schemas & known-errors
packages/stack-shared/src/schema-fields.ts, packages/stack-shared/src/known-errors.tsx
Extend customerTypeSchema to include "custom"; expand Item/OfferCustomerTypeDoesNotMatch constructors to accept "custom".
Shared interfaces (client/server/admin)
packages/stack-shared/src/interface/*
Client: getItem becomes discriminated union ({itemId,userId}
Template app implementations & interfaces
packages/template/src/lib/stack-app/...
Client/server add getItem/useItem supporting user/team/custom with new custom caches; admin createItemQuantityChange updated to union `{ userId

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I’m a rabbit in the code-lined glen, I nibble types and hop through trees,
A "custom" burrow joins the "user" and "team" with ease.
UUIDs loosen, routes now wear threefold hats,
Quantities update, tests cheer, and caches pat their spats.
Hooray — I twitch my whiskers at these tidy new paths. 🐇✨

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 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.

📥 Commits

Reviewing files that changed from the base of the PR and between da6f000 and 917f7a8.

📒 Files selected for processing (2)
  • packages/stack-shared/src/interface/admin-interface.ts (0 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • packages/stack-shared/src/interface/admin-interface.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
⏰ 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: docker
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: all-good
  • GitHub Check: setup-tests
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: docker
  • GitHub Check: Security Check
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch custom-item-customers

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@greptile-apps greptile-apps bot left a 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

Edit Code Review Bot Settings | Greptile

Copy link

recurseml bot commented Aug 20, 2025

Review by RecurseML

🔍 Review performed on b0e7706..d6397fa

Severity Location Issue
Medium apps/e2e/tests/js/payments.test.ts:147 REST API parameter should use snake_case naming convention
Medium apps/e2e/tests/js/payments.test.ts:192 REST API parameter should use snake_case naming convention
Medium apps/e2e/tests/js/payments.test.ts:103 REST API parameter should use snake_case naming convention
Medium apps/e2e/tests/js/payments.test.ts:84 REST API parameter should use snake_case naming convention
Medium apps/e2e/tests/js/payments.test.ts:72 REST API parameter should use snake_case naming convention
Medium apps/e2e/tests/js/payments.test.ts:101 REST API parameter should use snake_case naming convention
Medium apps/e2e/tests/js/payments.test.ts:104 REST API parameter should use snake_case naming convention
Medium packages/stack-shared/src/interface/client-interface.ts:1795 Direct JSON.stringify usage violates code pattern guidelines
✅ Files analyzed, no issues (3)

packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts

⏭️ Files skipped (low suspicion) (20)

apps/backend/prisma/migrations/20250820164831_custom_customer_types/migration.sql
apps/backend/prisma/schema.prisma
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts
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/purchases/create-purchase-url/route.ts
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
apps/backend/src/lib/payments.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
apps/dashboard/src/components/data-table/payment-item-table.tsx
apps/e2e/tests/backend/backend-helpers.ts
apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts
apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts
packages/stack-shared/src/interface/admin-interface.ts
packages/stack-shared/src/interface/server-interface.ts
packages/stack-shared/src/known-errors.tsx
packages/stack-shared/src/schema-fields.ts
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts

Need help? Join our Discord

Copy link

patched-codes bot commented Aug 20, 2025

Documentation Changes Required

Based on the analysis provided, the following changes are required in the documentation:

1. ./docs/templates/sdk/objects/stack-app.mdx

StackClientApp Section

  1. Update the Table of Contents to include the new item property.
  2. Add a new CollapsibleMethodSection to document the usage of the item property.
  3. Include examples demonstrating how to retrieve an item using different identifier options (userId, teamId, or customId).
  4. Explain that the AsyncStoreProperty allows developers to access Item objects containing displayName, quantity, and nonNegativeQuantity properties.

StackServerApp Section

  1. Update the Table of Contents around line 530 to include:
    getItem({itemId, userId/teamId/customId}): Promise<ServerItem>; //$stack-link-to:#stackserverappgetitem
    // NEXT_LINE_PLATFORM react-like
    ⤷ useItem({itemId, userId/teamId/customId}): ServerItem; //$stack-link-to:#stackserverappuseitem
    
  2. Add a new section after the Team Management section explaining:
    • How to retrieve items by ID using different identifiers (userId, teamId, or customId)
    • Methods available on ServerItem (increaseQuantity, decreaseQuantity, tryDecreaseQuantity)
    • Include examples of retrieving and manipulating items

2. Other Documentation

  • The createItemQuantityChange method in the StackAdminApp interface has been modified to accept different types of identifiers (userId, teamId, or customId) instead of just customerId. However, there is currently no existing documentation for this method.
  • The createPurchaseUrl method has been removed, but there is no existing documentation to update.

Please ensure these changes are reflected in the relevant documentation files. If there are no existing documents for the StackAdminApp interface and its methods, consider creating new documentation to cover these features.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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_type

This 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: Missing allow_negative in internal updateItemQuantity calls

The new route schema requires the allow_negative query parameter, but several internal calls to updateItemQuantity omit it—these will now fail validation. Please update each call to include allow_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 fast

The 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_type

A 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/server

The 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 flakiness

Inline 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_inline

You’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 literals

The 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/server

Same 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 helpers

The 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 for customer_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–24

Suggested 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.

📥 Commits

Reviewing files that changed from the base of the PR and between b0e7706 and 5e5c0ae.

📒 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.customerId

Automated searches across all schema and code files show:

  • No @db.Uuid usage on any customerId in your *.prisma schemas
  • No .uuid() validations on customerId, customer_id, or customId in TS/JS code

The 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 getItemQuantityForCustomer

Correctly 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 path

Matches 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-url

Keeps 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 access

The 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 contract

Adding 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 — verified

Confirmed: 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 routes

All 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 internally

The getItemQuantityForCustomer function uses typedToUppercase(options.customerType) in both its subscription.findMany and itemQuantityChange.aggregate queries, so passing req.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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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” IDs

For 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 5e5c0ae and 1018ee5.

📒 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 1018ee5 and da6f000.

📒 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 of createPurchaseUrl detected

Ran 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 }],
Copy link
Contributor

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?

Suggested change
[{ 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 }],
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
[{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customId: string }],
[{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customCustomerId: string }],

@N2D4 N2D4 assigned BilalG1 and unassigned N2D4 Aug 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants