-
Notifications
You must be signed in to change notification settings - Fork 491
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
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, Prisma schema, backend payments logic, routes, SDKs, dashboard UI, templates and tests; threads Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant SDK as Client/SDK
participant API as Items API
participant Payments as payments lib
participant DB as Prisma/DB
SDK->>API: GET /payments/items/{customer_type}/{customer_id}/{item_id}
API->>DB: fetch itemConfig(item_id)
alt item not found
API-->>SDK: 404
else item found
alt req.customer_type != itemConfig.customerType
API-->>SDK: KnownErrors.ItemCustomerTypeDoesNotMatch (400)
else match
API->>Payments: getItemQuantityForCustomer(itemId, customerId, customerType)
Payments->>DB: query subscriptions & ItemQuantityChange where customerType = UPPER(customerType) and customerId = ...
DB-->>Payments: aggregated quantity
Payments-->>API: quantity
API-->>SDK: 200 { quantity }
end
end
sequenceDiagram
autonumber
participant SDK as Client/SDK
participant API as Update Quantity API
participant Payments as payments lib
participant DB as Prisma/DB
SDK->>API: POST /payments/items/{customer_type}/{customer_id}/{item_id}/update-quantity
API->>DB: fetch itemConfig(item_id)
alt mismatch
API-->>SDK: KnownErrors.ItemCustomerTypeDoesNotMatch
else match
API->>Payments: ensureCustomerExists(tenancyId, customerType, customerId)
Payments->>DB: validate existence (user/team/custom) or throw KnownErrors.UserNotFound/TeamNotFound
Payments->>DB: tx create ItemQuantityChange { customerType: UPPER(customer_type), customerId, delta, ... }
DB-->>Payments: committed
Payments-->>API: success
API-->>SDK: 204
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
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. ✨ 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_negativein internalupdateItemQuantitycallsThe new route schema requires the
allow_negativequery parameter, but several internal calls toupdateItemQuantityomit it—these will now fail validation. Please update each call to includeallow_negativeset appropriately (e.g.falsefor increments,truefor 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_idAlthough we escape single quotes when constructing the Stripe query, constraining
customer_idat 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.tsxapps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.tsapps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.tsapps/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.Uuidusage on anycustomerIdin your *.prisma schemas- No
.uuid()validations oncustomerId,customer_id, orcustomIdin TS/JS codeThe change to TEXT for
Subscription.customerIdappears 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
getItemQuantityForCustomerfunction usestypedToUppercase(options.customerType)in both itssubscription.findManyanditemQuantityChange.aggregatequeries, 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
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
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.tsapps/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 ofcreatePurchaseUrldetectedRan a full scan across
.tsand.tsxfiles 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.
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
Outdated
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts
Outdated
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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/stack-shared/src/interface/client-interface.ts (1)
1785-1789: Audit and update allcreateCheckoutUrlcall sites for new signatureThe
createCheckoutUrlmethod signature was changed to include four parameters(customer_type, customer_id, offerIdOrInline, session). The following call sites still use the old one-argument form and must be updated to avoid runtime errors:
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
Line 130:const checkoutUrl = await props.team.createCheckoutUrl("team");→ Update to:
const checkoutUrl = await props.team.createCheckoutUrl( "team", props.team.id, offerId, session );apps/dashboard/src/components/payments/create-checkout-dialog.tsx
Line 34:const result = await Result.fromPromise(customer.createCheckoutUrl(data.offerId));→ Update to:
const result = await Result.fromPromise(customer.createCheckoutUrl( data.customerType, data.customerId, data.offerId, session ));Line 62 (form submit):
onSubmit={values => createCheckoutUrl(values)}→ Ensure the handler extracts and passes all four parameters.
All other wrappers in
packages/templatealready pass the correct four arguments internally, so only direct calls need correction.
♻️ Duplicate comments (5)
packages/stack-shared/src/interface/server-interface.ts (1)
866-871: Encode path segments; avoid raw template literals for user-supplied IDs.customerType/customerId/itemId can include characters (e.g., slashes) that break the path or cause route mismatches. Use the existing urlString template tag (or encodeURIComponent per segment) to prevent path injection and 404s. This was flagged earlier; elevating to a correctness/security fix.
Apply one of the following diffs:
Option A — preferred (urlString helper):
- `/payments/items/${customerType}/${customerId}/${itemId}/update-quantity?${queryParams.toString()}`, + urlString`/payments/items/${customerType}/${customerId}/${itemId}/update-quantity?${queryParams.toString()}`,Option B — explicit encoding:
- `/payments/items/${customerType}/${customerId}/${itemId}/update-quantity?${queryParams.toString()}`, + `/payments/items/${encodeURIComponent(customerType)}/${encodeURIComponent(customerId)}/${encodeURIComponent(itemId)}/update-quantity?${queryParams.toString()}`,packages/stack-shared/src/interface/client-interface.ts (3)
17-17: Good fix: useurlStringfor safe path templating (encodes segments).This import enables encoded path construction and resolves prior concerns about unencoded dynamic segments in item routes.
1776-1778: Path now encodes all dynamic segments.
urlString\/payments/items/${customerType}/${customerId}/${options.itemId}`` correctly encodes every segment. This closes the routing breakage when IDs contain reserved characters.
1796-1801: Follow code-patterns: prefer stringifyJson over JSON.stringify for request bodies.The repo’s rule requires using
stringifyJsonto guarantee deterministic serialization and safe error handling.Apply:
- body: JSON.stringify({ customer_type, customer_id, ...offerBody }), + body: stringifyJson({ customer_type, customer_id, ...offerBody }),And add/import stringifyJson (outside this hunk):
// near the top with other utils import { ReadonlyJson, stringifyJson } from '../utils/json';Check remaining direct uses in this file to avoid future bot comments:
#!/bin/bash rg -nP --glob '!**/node_modules/**' 'JSON\.stringify\(' packages/stack-shared/src/interface/client-interface.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
1224-1231: Hook order fixed: single unconditional useAsyncCache call.This addresses the previous “conditional hook” lint error for useItem.
🧹 Nitpick comments (6)
packages/stack-shared/src/interface/server-interface.ts (2)
847-863: Guard against multiple identifiers being provided simultaneously.At runtime, an object could contain more than one of userId/teamId/customCustomerId (e.g., from spread-merging). Today the first matching branch wins silently. Add a defensive check to ensure exactly one is set; fail fast with a clear error.
Apply this diff within this block:
- if ("userId" in options) { + const idsProvided = ["userId", "teamId", "customCustomerId"].filter((k) => k in options); + if (idsProvided.length !== 1) { + throw new StackAssertionError(`updateItemQuantity requires exactly one of userId, teamId, or customCustomerId (got: ${idsProvided.join(", ") || "none"})`); + } + + if ("userId" in options) { customerType = "user"; customerId = options.userId; } else if ("teamId" in options) { customerType = "team"; customerId = options.teamId; } else if ("customCustomerId" in options) { customerType = "custom"; customerId = options.customCustomerId; - } else { - throw new StackAssertionError("updateItemQuantity requires one of userId, teamId, or customCustomerId"); - } + }
869-870: Header casing consistency.The file mixes "content-type" and "Content-Type". HTTP is case-insensitive, but pick one for consistency.
Apply this tiny diff here:
- headers: { "content-type": "application/json" }, + headers: { "Content-Type": "application/json" },packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (3)
521-522: Replace nested ternary with clearer branching.The nested ternary with "in" checks is hard to scan and easy to break if extended. Use an if/else chain mirroring server-interface for readability and parity.
Apply this refactor:
- { itemId: options.itemId, ...("userId" in options ? { userId: options.userId } : ("teamId" in options ? { teamId: options.teamId } : { customCustomerId: options.customCustomerId })) }, + (() => { + if ("userId" in options) return { itemId: options.itemId, userId: options.userId }; + if ("teamId" in options) return { itemId: options.itemId, teamId: options.teamId }; + return { itemId: options.itemId, customCustomerId: options.customCustomerId }; + })(),
515-519: Support decrements via allowNegative or auto-detect.The server call honors allow_negative via a query param, but this admin method has no way to set it. If quantity can be negative (revokes/credits), calls will fail or be rejected. Either expose allowNegative?: boolean, or infer it when quantity < 0.
Apply this minimal, backward-compatible change:
- async createItemQuantityChange(options: ( - { userId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } | - { teamId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } | - { customCustomerId: string, itemId: string, quantity: number, expiresAt?: string, description?: string } - )): Promise<void> { + async createItemQuantityChange(options: ( + { userId: string, itemId: string, quantity: number, expiresAt?: string, description?: string, allowNegative?: boolean } | + { teamId: string, itemId: string, quantity: number, expiresAt?: string, description?: string, allowNegative?: boolean } | + { customCustomerId: string, itemId: string, quantity: number, expiresAt?: string, description?: string, allowNegative?: boolean } + )): Promise<void> {And include the flag in the payload mapping:
{ delta: options.quantity, expires_at: options.expiresAt, description: options.description, + allow_negative: options.allowNegative ?? (options.quantity < 0), }Note: If ItemCrud['Server']['Update'] doesn’t include allow_negative in the body and only as a query param, alternatively plumb it into the first arg and let the interface derive the query param. If you prefer that, I can provide a matching change to the interface method.
515-527: Type hygiene: consider extracting a shared scope type.Both admin and server interfaces model the same tri-scope shape. Extract a shared type (e.g., ItemQuantityScope) in stack-shared to prevent divergence.
I can prep a small PR to add:
- packages/stack-shared/src/interface/types.ts: export type ItemQuantityScope = …
- Use it in both places.
Would you like me to draft that?packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
685-697: Minor: prefer method shorthand to avoid accidentalthisrebinding.Defining methods with
function () {}relies on call-site binding; destructuring (const { isValid } = key) would breakthis. Use method shorthand to keep semantics and improve readability.Apply:
- isValid: function () { + isValid() { return this.whyInvalid() === null; }, - whyInvalid: function () { + whyInvalid() { if (this.manuallyRevokedAt) { return "manually-revoked"; } if (this.expiresAt && this.expiresAt < new Date()) { return "expired"; } return null; },
📜 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 (13)
apps/dashboard/src/components/data-table/payment-item-table.tsx(4 hunks)apps/e2e/tests/backend/backend-helpers.ts(1 hunks)apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts(14 hunks)apps/e2e/tests/js/payments.test.ts(3 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/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(5 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 skipped from review as they are similar to previous changes (9)
- packages/template/src/lib/stack-app/apps/interfaces/server-app.ts
- apps/e2e/tests/backend/backend-helpers.ts
- packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
- packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
- apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts
- apps/dashboard/src/components/data-table/payment-item-table.tsx
- packages/stack-shared/src/known-errors.tsx
- packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
- apps/e2e/tests/js/payments.test.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer ES6 Map over Record where feasible
Files:
packages/stack-shared/src/interface/client-interface.tspackages/stack-shared/src/interface/server-interface.tspackages/template/src/lib/stack-app/apps/implementations/admin-app-impl.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🧬 Code graph analysis (3)
packages/stack-shared/src/interface/client-interface.ts (2)
packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError(69-85)packages/stack-shared/src/utils/urls.tsx (1)
urlString(314-316)
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)
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)
🪛 Biome (2.1.2)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
[error] 1318-1318: 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: setup-tests
- GitHub Check: all-good
- GitHub Check: restart-dev-and-test
- GitHub Check: docker
- GitHub Check: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: Security Check
🔇 Additional comments (10)
packages/stack-shared/src/interface/server-interface.ts (3)
839-846: Public API shape looks good; union captures all three scopes.The new options-union is a sensible direction and keeps call sites explicit. Nice alignment with backend route shape.
839-871: Confirm that the backend route for/payments/items/:customerType/:customerId/:itemId/update-quantityaccepts lowercase path segmentsI wasn’t able to locate the HTTP handler or any normalization logic for
customerTypein this repo. Please verify that:
- The API route file (e.g.
apps/backend/src/app/api/payments/items/[customerType]/[customerId]/[itemId]/update-quantity/route.ts) is defined and matches lowercase segments (user,team,custom).- Any validator or Zod schema around
customerTypeallows lowercase values or explicitly converts them (e.g.req.params.customerType.toUpperCase()).- Before passing
req.params.customerTypeinto Prisma (where theCustomerTypeenum values areUSER,TEAM,CUSTOM), the string is normalized to uppercase.Ensuring case alignment will prevent 404s or enum casting errors when calling
updateItemQuantity.
839-871: AllupdateItemQuantitycalls use the new two-argument signature.
I ran both AST-based and regex searches and found no remaining invocations passing three positional arguments.packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)
515-519: API surface aligns with server-interface; good.Accepting userId|teamId|customCustomerId in a single union reduces overload churn and matches the server API evolution.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (6)
219-223: Add custom-item cache: good separation and reuse of shared getItem.The
_customItemCachemirrors user/team caches and routes through the interface. Looks correct.
264-272: OAuth scope-adding redirect path wires in new scopes correctly.Passing merged
providerScopeand using app URLs here is consistent with the flow. No issues spotted.
760-763: Team update triggers cache refresh appropriately.Updating team data and then refreshing the current user teams cache is correct.
1097-1100: Password update: refresh user cache after mutation.Cache invalidation added; good.
1205-1206: New createCheckoutUrl signature used correctly.Passing
typeand owner ID matches the updated interface contract.
608-612: Helpful error message when token store is non-persistent.Clear guidance; assertion narrowing is appropriate.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Show resolved
Hide resolved
(https://www.loom.com/share/ab23e3ed0dde44bcb791d30b3b012777?sid=2ee6dcb4-feca-4d0f-9c8d-1ae9d71ba8f7) <!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Adds management pages for payment offers and items with create, edit, and delete functionalities, and updates schemas for default values and validation. > > - **Behavior**: > - Adds `page-client.tsx` and `page.tsx` for both `items` and `offers` under `payments` to manage payment items and offers. > - Implements `DialogOpener` in `page-client.tsx` for creating new items and offers. > - Adds edit and delete functionality for items and offers in `payment-item-table.tsx` and `payment-offer-table.tsx`. > - Updates `ensureOfferIdOrInlineOffer` in `payments.tsx` to default `quantity` to 0 if undefined. > - **Schema**: > - Updates `branchPaymentsSchema` in `schema.ts` to set default `quantity` to 0 for items. > - Modifies `offerSchema` and `inlineOfferSchema` in `schema-fields.ts` to define `quantity` as required. > - **UI Components**: > - Introduces `ItemDialog` and `OfferDialog` for item and offer creation/editing. > - Adds `DialogOpener` component for handling dialog state. > - Updates `DayIntervalSelectorField` to include an unset label option. > - **Navigation**: > - Adds "Offers" and "Items" to the sidebar in `sidebar-layout.tsx` with feature flag checks. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup> for c22bf3c. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN -->
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: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
apps/backend/src/lib/stripe.tsx (1)
45-47: AllowCUSTOMcustomerType in Stripe sync and creation flowsThe current implementation rejects any Stripe customer whose
metadata.customerTypeisn’tUSERorTEAM, and in the purchase-URL route new customers withcustomer_type: "custom"are still created withmetadata.customerType = TEAM. To fully support the newCUSTOMtype end-to-end, please:
- In
apps/backend/src/lib/stripe.tsx(syncStripeSubscriptions), includeCustomerType.CUSTOMin the allowed types.- In
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts, map"custom"toCustomerType.CUSTOMwhen creating a Stripe customer.- Update error messages and tests accordingly to reflect that
CUSTOMis now a first-class customer type.Required changes:
apps/backend/src/lib/stripe.tsx (around line 45)
if (customerType !== CustomerType.USER && customerType !== CustomerType.TEAM) { throw new StackAssertionError("Stripe customer metadata has invalid customerType"); }should become:
if (customerType !== CustomerType.USER && customerType !== CustomerType.TEAM) {
- if (![CustomerType.USER, CustomerType.TEAM, CustomerType.CUSTOM].includes(customerType)) {
throw new StackAssertionError("Stripe customer metadata has invalid customerType");
}- **apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts** (around the `stripe.customers.create` call) ```diff - if (!stripeCustomer) { - stripeCustomer = await stripe.customers.create({ - metadata: { - customerId: req.body.customer_id, - customerType: customerType === "user" ? CustomerType.USER : CustomerType.TEAM, - } - }); - } + if (!stripeCustomer) { + // Map the string customerType to the Prisma enum, including CUSTOM + const stripeMetadataCustomerType = + customerType === "user" + ? CustomerType.USER + : customerType === "team" + ? CustomerType.TEAM + : CustomerType.CUSTOM; + + stripeCustomer = await stripe.customers.create({ + metadata: { + customerId: req.body.customer_id, + customerType: stripeMetadataCustomerType, + }, + }); + }
- Ensure any checkout-session creation logic that stamps subscription metadata continues to serialize and parse
CUSTOMcorrectly.- Add or update tests to cover the
CUSTOMflow (e.g., inapps/e2e/tests/js/payments.test.ts) so we verify sync, retrieval, and quantity logic for custom customers.apps/backend/prisma/migrations/20250821175509_test_mode_subscriptions/migration.sql (1)
1-13: Unsafe NOT NULL addition without default will fail on non-empty Subscription tablePostgres cannot add a NOT NULL column without a default if the table has rows. This migration will fail in environments with existing data. Provide a default, backfill, then (optionally) drop the default.
Safer multi-step migration:
/* Warnings: - Added the required column `creationSource` to the `Subscription` table without a default value. This is not possible if the table is not empty. */ -- CreateEnum CREATE TYPE "SubscriptionCreationSource" AS ENUM ('PURCHASE_PAGE', 'TEST_MODE'); -- AlterTable -ALTER TABLE "Subscription" ADD COLUMN "creationSource" "SubscriptionCreationSource" NOT NULL, -ALTER COLUMN "stripeSubscriptionId" DROP NOT NULL; +ALTER TABLE "Subscription" + ADD COLUMN "creationSource" "SubscriptionCreationSource" NOT NULL DEFAULT 'PURCHASE_PAGE', + ALTER COLUMN "stripeSubscriptionId" DROP NOT NULL; + +-- Backfill is instant due to DEFAULT; if desired, drop the DEFAULT to enforce explicit writes going forward: +ALTER TABLE "Subscription" ALTER COLUMN "creationSource" DROP DEFAULT;apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx (3)
53-61: Validate and integerize amount before sending to Stripe
Number(selectedPrice.USD) * 100can produce NaN or non-integer values. Stripe requires a positive integer in the smallest currency unit. Validate and round explicitly to avoid 4xx from Stripe or silent rounding.- items: [{ - price_data: { - currency: "usd", - unit_amount: Number(selectedPrice.USD) * 100, + items: [{ + price_data: { + currency: "usd", + // Validate and integerize price + unit_amount: (() => { + const usd = Number(selectedPrice.USD); + if (!Number.isFinite(usd) || usd <= 0) { + throw new StatusError(400, "Invalid USD price amount on offer"); + } + return Math.round(usd * 100); + })(), product: product.id, recurring: { interval_count: selectedPrice.interval[0], interval: selectedPrice.interval[1], },
47-67: Add Stripe idempotency to avoid duplicate subscriptions on retriesIf the client retries before code revocation (network glitches, reloads), you can end up with multiple subscriptions. Use the verification
codeIdas an idempotency key.- const subscription = await stripe.subscriptions.create({ + const subscription = await stripe.subscriptions.create({ customer: data.stripeCustomerId, payment_behavior: 'default_incomplete', payment_settings: { save_default_payment_method: 'on_subscription' }, expand: ['latest_invoice.confirmation_secret'], items: [{ price_data: { currency: "usd", unit_amount: Number(selectedPrice.USD) * 100, product: product.id, recurring: { interval_count: selectedPrice.interval[0], interval: selectedPrice.interval[1], }, }, quantity: 1, }], metadata: { offer: JSON.stringify(data.offer), }, - }); + }, { idempotencyKey: `purchase-session:${codeId}` });
51-52: Critical Fix Required: Correct Stripe expand path for client secretThe expansion key currently used (
latest_invoice.confirmation_secret) is invalid. According to Stripe’s documentation, you must expand thepayment_intent.client_secretfield on the latest invoice to retrieve the client secret.• File:
apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx
– Lines 51–52 and 73–77: update theexpandarray.Suggested change:
- expand: ['latest_invoice.confirmation_secret'], + expand: ['latest_invoice.payment_intent.client_secret'],Please apply this update to ensure the client secret is correctly returned.
apps/dashboard/src/components/form-fields/day-interval-selector-field.tsx (1)
29-32: Format and display"never"correctly
formatDayIntervalignores"never", so the Select won’t show it as selected. Include it explicitly.- const formatDayInterval = (interval: DayInterval | undefined): string | undefined => { - if (!interval) return undefined; + const formatDayInterval = (interval: DayInterval | "never" | undefined): string | undefined => { + if (!interval) return undefined; + if (interval === "never") return "never"; return `${interval[0]}-${interval[1]}`; }; @@ - defaultValue={formatDayInterval(field.value as DayInterval)} + defaultValue={formatDayInterval(field.value as DayInterval | "never")}Also applies to: 43-45
♻️ Duplicate comments (2)
apps/backend/src/lib/payments.tsx (1)
64-65: Ensure all getItemQuantityForCustomer call sites pass customerType.The signature now requires customerType. Please verify all invocations were updated; older sites may still omit it.
Run to list argument objects and quickly spot missing keys:
#!/bin/bash # Show all call sites and the argument object for getItemQuantityForCustomer rg -nP -C2 --type=ts '\bgetItemQuantityForCustomer\s*\(\s*\{[^}]*\}\s*\)' \ | sed -n '1,200p' # Specifically highlight lines lacking 'customerType' rg -nP --type=ts '\bgetItemQuantityForCustomer\s*\(\s*\{(?![^}]*\bcustomerType\b)' -C1apps/dashboard/src/components/data-table/payment-item-table.tsx (1)
148-154: Make customerType required; don’t silently fall back to “custom”.Leaving customerType optional means undefined flows to the “custom” branch in submit. Make it required in props and guard before submission.
type CreateItemQuantityChangeDialogProps = { open: boolean, onOpenChange: (open: boolean) => void, itemId: string, - customerType: "user" | "team" | "custom" | undefined, + customerType: "user" | "team" | "custom", }And in submit (below), add a defensive guard (see next comment).
🧹 Nitpick comments (36)
apps/dashboard/src/lib/utils.tsx (1)
24-29: Good move to safer parsing; add a narrow-to-string[] guard to avoid accidental non-string entries.parseJson shields against throws, but JSON can still be an array with non-string elements. Filter to string[] before includes to keep behavior predictable.
Apply:
- const allowedProjectIds = parseJson(getPublicEnvVar("NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS") || "[]"); - if (allowedProjectIds.status !== "ok" || !Array.isArray(allowedProjectIds.data)) { - return false; - } - return allowedProjectIds.data.includes(projectId); + const parsed = parseJson(getPublicEnvVar("NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS") || "[]"); + if (parsed.status !== "ok" || !Array.isArray(parsed.data)) { + return false; + } + const ids = parsed.data.filter((v): v is string => typeof v === "string"); + return ids.includes(projectId);Optional: normalize whitespace to reduce surprises from hand-edited envs:
- const ids = parsed.data.filter((v): v is string => typeof v === "string"); + const ids = parsed.data + .filter((v): v is string => typeof v === "string") + .map(s => s.trim());apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx (2)
80-84: Gate rendering of Connect UI until setup is complete; also avoid inline style.
- Rendering
<ConnectPayments />whenstripeAccountSetupCompleteis false can produce confusing UX or errors, depending on how your Connect instance is configured.- Prefer Tailwind classes over inline style for consistency and easier theming.
Apply this diff:
- <div className="flex justify-center"> - <div style={{ maxWidth: 1250, width: '100%' }}> - <ConnectPayments /> - </div> - </div> + {paymentsConfig.stripeAccountSetupComplete ? ( + <div className="flex justify-center"> + <div className="w-full max-w-[1250px]"> + <ConnectPayments /> + </div> + </div> + ) : ( + <div className="flex justify-center"> + <Button onClick={setupPayments}>Complete Setup</Button> + </div> + )}If the page is not already wrapped by
ConnectComponentsProvider, you’ll also need to wrap the component:// outside the selected range; illustrative only import { ConnectComponentsProvider, ConnectPayments } from "@stripe/react-connect-js"; import { loadConnectAndInitialize, type ConnectInstance } from "@stripe/connect-js"; import { useEffect, useState } from "react"; function PaymentsPanel() { const [connectInstance, setConnectInstance] = useState<ConnectInstance | null>(null); useEffect(() => { let mounted = true; (async () => { // TODO: replace with your real publisher key and client-secret fetcher const instance = await loadConnectAndInitialize({ publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, fetchClientSecret: async () => (await fetch("/api/stripe/connect/client-secret")).text(), }); if (mounted) setConnectInstance(instance); })(); return () => { mounted = false; }; }, []); if (!connectInstance) return null; // or a skeleton return ( <ConnectComponentsProvider connectInstance={connectInstance}> <ConnectPayments /> </ConnectComponentsProvider> ); }
26-30: Add error handling around payments setup redirect.A network failure or server error from
setupPayments()will currently surface as an unhandled promise rejection. Show a toast and avoid redirect attempts when the call fails.- const setupPayments = async () => { - const { url } = await stackAdminApp.setupPayments(); - window.location.href = url; - await wait(2000); - }; + const setupPayments = async () => { + try { + const { url } = await stackAdminApp.setupPayments(); + window.location.href = url; + await wait(2000); + } catch (err: any) { + toast({ + title: "Failed to start payments setup", + description: err?.message ?? "Please try again.", + variant: "destructive", + }); + } + };apps/backend/src/lib/stripe.tsx (1)
87-87: Prefer Prisma enum over string literal for creationSourceUse the generated Prisma enum to avoid drift and get compile-time checks.
-import { CustomerType } from "@prisma/client"; +import { CustomerType, SubscriptionCreationSource } from "@prisma/client"; … - creationSource: "PURCHASE_PAGE" + creationSource: SubscriptionCreationSource.PURCHASE_PAGEapps/dashboard/tailwind.config.ts (1)
80-81: Consider reduced-motion accessibility for fade-inIf these animations are applied broadly, prefer guarding via motion-safe or providing motion-reduce variants in usage to respect prefers-reduced-motion.
Example usage in components:
- className="motion-safe:animate-fade-in"
- or className="animate-fade-in motion-reduce:animate-none"
apps/backend/prisma/migrations/20250821175509_test_mode_subscriptions/migration.sql (1)
1-13: Confirm and Enforce Single TEST_MODE Subscription per TenancyWe’ve dropped NOT NULL on
stripeSubscriptionIdinapps/backend/prisma/migrations/20250821175509_test_mode_subscriptions/migration.sql(ALTER COLUMN "stripeSubscriptionId" DROP NOT NULL) while preserving the composite unique on(tenancyId, stripeSubscriptionId)in your Prisma schema (@@unique([tenancyId, stripeSubscriptionId])inapps/backend/prisma/schema.prismaat line 755). In Postgres, a UNIQUE index treats multiple NULLs as distinct, so this change will allow multiple TEST_MODE subscriptions (which rely onstripeSubscriptionId = NULL) for the same tenancy.• If you’re relying on this composite unique for Prisma upserts, ensure you never upsert with
stripeSubscriptionId: null, otherwise each upsert will insert a new TEST_MODE row.
• To enforce “one TEST_MODE subscription per tenancy” at the database level, add a partial unique index in a follow-up migration:CREATE UNIQUE INDEX "Subscription_tenancyId_testMode_key" ON "Subscription"("tenancyId") WHERE "creationSource" = 'TEST_MODE';• Alternatively, enforce this invariant in application logic (e.g. throw or upsert only if no existing TEST_MODE row exists).
By adding a targeted constraint (or guarding in code), you’ll prevent unintended duplicates of TEST_MODE subscriptions per tenancy.
apps/dashboard/src/components/dialog-opener.tsx (1)
20-22: Avoid accidental form submission by setting Button type="button"If DialogOpener is used inside a form, the Button may submit the form depending on the underlying implementation. Safer to set an explicit type.
- <Button onClick={() => setIsOpen(true)}> + <Button type="button" onClick={() => setIsOpen(true)}> {triggerLabel} </Button>apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx (3)
40-42: Return a 501 StatusError instead of throwing an assertion for unsupported one-time prices.This is a feature gap, not a server invariant violation. Using 501 communicates intent better and yields a client error payload consistent with StatusError.
Apply this diff:
- if (!selectedPrice.interval) { - throw new StackAssertionError("unimplemented; prices without an interval are currently not supported"); - } + if (!selectedPrice.interval) { + throw new StatusError(StatusError.NotImplemented, "Prices without an interval are not yet supported in test mode"); + }
50-52: Avoid micro-drift between start and end timestamps.Compute currentPeriodEnd from the same base timestamp used for currentPeriodStart.
Apply this diff:
- await prisma.subscription.create({ + const now = new Date(); + await prisma.subscription.create({ data: { tenancyId: auth.tenancy.id, customerId: data.customerId, customerType: typedToUppercase(data.offer.customerType), status: "active", offer: data.offer, - currentPeriodStart: new Date(), - currentPeriodEnd: addInterval(new Date(), selectedPrice.interval), + currentPeriodStart: now, + currentPeriodEnd: addInterval(now, selectedPrice.interval), cancelAtPeriodEnd: false, creationSource: "TEST_MODE", }, });
43-55: Consider including the selected price id in the created subscription for traceability.If your Subscription model or metadata has a field for the chosen price id, persisting
price_idhelps auditing and post-hoc reconciliation in test mode. If no such field exists, ignore this.apps/dashboard/src/components/payments/included-item-editor.tsx (1)
72-72: Offer an explicit “No repeat” unset option for UX parity with the price editor.DayIntervalSelectorField now supports unsetLabel. Since repeat is optional, exposing an unset choice clarifies intent and aligns with the price editor’s “One time”.
Apply this diff:
- <DayIntervalSelectorField control={sub.control} name={"repeat"} label="Repeat" includeNever /> + <DayIntervalSelectorField control={sub.control} name={"repeat"} label="Repeat" includeNever unsetLabel="No repeat" />apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx (1)
230-245: Broaden regex to keep nav selection active on nested routes.Current regexes only match the list pages exactly. If you later add nested routes (e.g., /payments/items/[itemId] or /payments/offers/[offerId]), the sidebar item will not appear selected. Consider allowing optional subpaths.
Apply this diff:
{ name: "Offers", href: "/payments/offers", - regex: /^\/projects\/[^\/]+\/payments\/offers$/, + regex: /^\/projects\/[^\/]+\/payments\/offers(?:\/.*)?$/, icon: SquarePen, type: 'item', requiresDevFeatureFlag: true, }, { name: "Items", href: "/payments/items", - regex: /^\/projects\/[^\/]+\/payments\/items$/, + regex: /^\/projects\/[^\/]+\/payments\/items(?:\/.*)?$/, icon: Box, type: 'item', requiresDevFeatureFlag: true, },packages/stack-shared/src/schema-fields.ts (1)
574-579: Quantity inincludedItemsis now required — verify parity with inline schema.
offerSchema.includedItems[...].quantityisdefined(), but the corresponding field ininlineOfferSchemaremains optional. If this discrepancy is unintentional, align both.Apply this diff if you want parity:
included_items: yupRecord( userSpecifiedIdSchema("itemId"), yupObject({ - quantity: yupNumber(), + quantity: yupNumber().defined(), repeat: dayIntervalOrNeverSchema.optional(), expires: yupString().oneOf(['never', 'when-purchase-expires', 'when-repeated']).optional(), }), ),If the intent is for inline offers to be looser, consider adding a comment to document the divergence.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/offers/page.tsx (1)
5-7: Optional: Type the metadataAnnotate for better editor help and consistency with Next.js typing.
-import { notFound } from "next/navigation"; +import { notFound } from "next/navigation"; +import type { Metadata } from "next"; @@ -export const metadata = { +export const metadata: Metadata = { title: "Offers", };apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx (2)
68-71: Harden code revocation (retry/log) to avoid inconsistenciesIf revocation fails after creating a subscription, the client gets a 500 and the code remains reusable. Consider best-effort revocation with logging and retry while still returning success if Stripe creation succeeded.
- await purchaseUrlVerificationCodeHandler.revokeCode({ - tenancy, - id: codeId, - }); + try { + await purchaseUrlVerificationCodeHandler.revokeCode({ tenancy, id: codeId }); + } catch (e) { + console.warn("Failed to revoke purchase verification code", { codeId, tenancyId: tenancy.id, error: (e as Error).message }); + // Optionally: enqueue a background retry here + }
65-66: Consider avoiding large metadata payloads
offer: JSON.stringify(data.offer)risks exceeding Stripe metadata limits (key/value size). Prefer storing a compact identifier (e.g., offer id/version) and fetch details server-side when needed.apps/dashboard/src/app/(main)/purchase/return/page-client.tsx (1)
94-96: Guard retry link whenpurchaseFullCodeis missingIf
purchaseFullCodeis undefined, the link points to/purchase/undefined. Hide or disable the link in that case.- <StyledLink href={`/purchase/${purchaseFullCode}`}>Click here</StyledLink> to try making your purchase again. + {purchaseFullCode ? ( + <StyledLink href={`/purchase/${purchaseFullCode}`}>Click here</StyledLink> + ) : ( + <span>Return to the purchase page</span> + )} to try making your purchase again.apps/dashboard/src/components/form-fields/day-interval-selector-field.tsx (1)
34-66: Optional: Control the Select value instead of usingdefaultValueUsing
defaultValuewith RHF can desync UI from form state after the first change. Prefer passingvalue={formatDayInterval(field.value as DayInterval | "never")}.- defaultValue={formatDayInterval(field.value as DayInterval | "never")} + value={formatDayInterval(field.value as DayInterval | "never")}packages/stack-shared/src/interface/admin-interface.ts (1)
519-529: Optional: Bubble up non-2xx errors explicitlyIf
sendAdminRequestdoesn’t throw on non-OK, consider assertingresponse.okand surfacing backend error text for better DX.- async testModePurchase(options: { price_id: string, full_code: string }): Promise<void> { - await this.sendAdminRequest( + async testModePurchase(options: { price_id: string, full_code: string }): Promise<void> { + const res = await this.sendAdminRequest( "/internal/payments/test-mode-purchase-session", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(options), }, null, ); + if (!res.ok) { + throw new Error(`testModePurchase failed: ${res.status} ${await res.text()}`); + } }apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts (3)
32-35: Tenancy lookup error type: confirm desired client semantics vs internal assertionThrowing StackAssertionError here will typically surface as a 5xx/internal assertion. If a stale/invalid code can realistically reach this handler (e.g., revoked tenancy, migration gaps), consider returning a 404/400 known error instead to avoid flagging it as an internal bug. If this path truly represents an invariant violation, keep the assertion—but please confirm that this is intentional.
Example change if you want a client-facing 404:
- if (!tenancy) { - throw new StackAssertionError(`No tenancy found for given tenancyId`); - } + if (!tenancy) { + // Consider a known error/404 here instead of an assertion + return { + statusCode: 404, + bodyType: "json", + body: "Tenancy not found for the verification code", + }; + }
39-39: customer_type no longer falls back to "user" — verify backward compatibility of older codesYou now propagate offer.customerType directly. If any historical verification codes were generated without customerType, this can become undefined and violate the response schema. If such codes are impossible, all good; otherwise consider a safe fallback.
- customer_type: offer.customerType, + customer_type: offer.customerType ?? "user",
2-2: Remove unused importsadaptSchema and clientOrHigherAuthTypeSchema are imported but unused in this file.
-import { adaptSchema, clientOrHigherAuthTypeSchema, inlineOfferSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { inlineOfferSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";apps/backend/prisma/schema.prisma (1)
758-771: Add composite index including customerType for common read-pathsItem quantity lookups often include customerType + customerId. Consider indexing (tenancyId, customerType, customerId, expiresAt) to improve selectivity vs the current (tenancyId, customerId, expiresAt).
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, expiresAt]) }apps/dashboard/src/components/payments/item-dialog.tsx (1)
88-88: Expose "Never" option for repeat interval (schema supports it)The validation allows dayIntervalOrNeverSchema but the UI doesn’t offer “Never”. Consider enabling it for consistency.
- <DayIntervalSelectorField control={form.control} name={"defaultRepeat"} label="Repeat" unsetLabel="None" /> + <DayIntervalSelectorField control={form.control} name={"defaultRepeat"} label="Repeat" unsetLabel="None" includeNever />apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts (1)
1-305: Add one E2E for the new "custom" customer typeGiven the introduction of CUSTOM end-to-end, consider adding a test mirroring the “user” path, but with a customCustomerId and a custom-typed item to ensure all layers (create URL → purchase session/test-mode → items API) work end-to-end.
Proposed test (append to this file):
+it("creates subscription in test mode for custom customer and increases included item quantity", async ({ expect }) => { + await Project.createAndSwitch(); + await Project.updateConfig({ + payments: { + stripeAccountId: "acct_test123", + items: { + "custom-item": { + displayName: "Custom Item", + customerType: "custom", + default: { quantity: 0 }, + }, + }, + offers: { + "custom-offer": { + displayName: "Custom Offer", + customerType: "custom", + serverOnly: false, + stackable: false, + prices: { + monthly: { USD: "1000", interval: [1, "month"] }, + }, + includedItems: { "custom-item": { quantity: 3 } }, + }, + }, + }, + }); + + const customId = "acme-42"; + const createUrlResponse = await niceBackendFetch("/api/latest/payments/purchases/create-purchase-url", { + method: "POST", + accessType: "client", + body: { + customer_type: "custom", + customer_id: customId, + offer_id: "custom-offer", + }, + }); + expect(createUrlResponse.status).toBe(200); + const body = createUrlResponse.body as { url: string }; + const code = body.url.match(/\/purchase\/([a-z0-9-_]+)/)?.[1]; + expect(code).toBeDefined(); + + const before = await niceBackendFetch(`/api/latest/payments/items/custom/${customId}/custom-item`, { accessType: "client" }); + expect(before.status).toBe(200); + expect(before.body.quantity).toBe(0); + + const sessionRes = await niceBackendFetch("/api/latest/internal/payments/test-mode-purchase-session", { + method: "POST", + accessType: "admin", + body: { full_code: code, price_id: "monthly" }, + }); + expect(sessionRes.status).toBe(200); + + const after = await niceBackendFetch(`/api/latest/payments/items/custom/${customId}/custom-item`, { accessType: "client" }); + expect(after.status).toBe(200); + expect(after.body.quantity).toBe(3); +});apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx (2)
43-48: Guard against missing USD amounts to avoid NaN and invalid Stripe amountsIf a price lacks USD (or contains a non-numeric string), Number(undefined) yields NaN and StripeElementsProvider receives an invalid amount. Fail closed or add a currency selection strategy.
- const currentAmount = useMemo(() => { - if (!selectedPriceId || !data?.offer?.prices) { - return 0; - } - return Number(data.offer.prices[selectedPriceId].USD) * 100; - }, [data, selectedPriceId]); + const currentAmount = useMemo(() => { + if (!selectedPriceId || !data?.offer?.prices) return 0; + const usd = data.offer.prices[selectedPriceId]?.USD; + return usd ? Math.round(Number(usd) * 100) : 0; // default to 0 if USD is absent + }, [data, selectedPriceId]);If multi-currency is planned, we should thread currency alongside amount and configure Stripe accordingly.
98-107: Bypass handler: consider disabling button until selectedPriceId is setMinor UX: to avoid a no-op click path, disable the bypass button while selectedPriceId is null/undefined.
You can pass disabled={!selectedPriceId} to the Button in BypassInfo, or early-return with a toast.
apps/dashboard/src/components/data-table/payment-offer-table.tsx (3)
51-51: Consider Map over Record for offers prop (guideline).The component currently takes offers as a Record. If feasible, prefer ES6 Map to align with codebase guidance and avoid accidental key collisions with prototype properties.
Example refactor sketch:
-export function PaymentOfferTable({ offers }: { offers: Record<string, yup.InferType<typeof branchPaymentsSchema>["offers"][string]> }) { - const data: PaymentOffer[] = Object.entries(offers).map(([id, offer]) => ({ +export function PaymentOfferTable({ offers }: { + offers: Map<string, yup.InferType<typeof branchPaymentsSchema>["offers"][string]> | Record<string, yup.InferType<typeof branchPaymentsSchema>["offers"][string]> +}) { + const entries = offers instanceof Map ? [...offers.entries()] : Object.entries(offers); + const data: PaymentOffer[] = entries.map(([id, offer]) => ({ id, ...offer, }));
34-38: Free Trial rendering: prefer readable formatter over array join.If available in UI utilities, use a readable interval formatter instead of freeTrial?.join(" "). This avoids coupling to tuple shape and keeps display consistent with other places (e.g., PriceEditor).
- cell: ({ row }) => <TextCell>{row.original.freeTrial?.join(" ") ?? ""}</TextCell>, + cell: ({ row }) => <TextCell>{row.original.freeTrial ? readableInterval(row.original.freeTrial) : ""}</TextCell>,Note: add the corresponding import if readableInterval is available in your shared utils.
96-109: Deletion should also clean up references in exclusivity groups.Offers can be referenced under payments.exclusivityGroups. Consider removing the offer id from any groups before/while deleting to prevent stale config references.
- onClick: async () => { - await project.updateConfig({ [`payments.offers.${offer.id}`]: null }); + onClick: async () => { + const cfg = await project.getConfig(); + const updates: Record<string, any> = { [`payments.offers.${offer.id}`]: null }; + for (const [groupId, group] of Object.entries(cfg.payments.exclusivityGroups ?? {})) { + if (group && Object.prototype.hasOwnProperty.call(group, offer.id)) { + updates[`payments.exclusivityGroups.${groupId}.${offer.id}`] = undefined; + } + } + await project.updateConfig(updates); toast({ title: "Offer deleted" }); },apps/dashboard/src/components/payments/offer-dialog.tsx (1)
43-46: Strengthen validation: use dayIntervalOrNeverSchema for repeat.yup.mixed() weakens validation. Reuse the shared dayIntervalOrNeverSchema to keep client validation aligned with server-side schema.
-import { offerPriceSchema, offerSchema, userSpecifiedIdSchema, yupRecord } from "@stackframe/stack-shared/dist/schema-fields"; +import { offerPriceSchema, offerSchema, userSpecifiedIdSchema, yupRecord, dayIntervalOrNeverSchema } from "@stackframe/stack-shared/dist/schema-fields"; @@ - repeat: yup.mixed().optional(), + repeat: dayIntervalOrNeverSchema.optional(),apps/backend/src/lib/payments.tsx (1)
103-140: Existence checks cover user/team; clarify custom semantics.The guard correctly validates UUID shape and existence for user/team. For custom, intentionally skipping existence checks seems aligned with the “opaque identifier” design. If empty strings should be rejected for custom IDs, consider a minimal non-empty check.
- } else if (options.customerType === "team") { + } else if (options.customerType === "team") { … - } + } else { + // customerType === "custom" + if (options.customerId.length === 0) { + throw new KnownErrors.CustomerDoesNotExist(options.customerId); + } + }apps/dashboard/src/components/data-table/payment-item-table.tsx (3)
63-63: Consider Map over Record for items prop (guideline).Similar to offers, prefer ES6 Map for items data sources when feasible to match the codebase guideline and avoid prototype pitfalls.
-export function PaymentItemTable({ items }: { items: Record<string, yup.InferType<typeof branchPaymentsSchema>["items"][string]> }) { - const data: PaymentItem[] = Object.entries(items).map(([id, item]) => ({ +export function PaymentItemTable({ items }: { + items: Map<string, yup.InferType<typeof branchPaymentsSchema>["items"][string]> | Record<string, yup.InferType<typeof branchPaymentsSchema>["items"][string]> +}) { + const entries = items instanceof Map ? [...items.entries()] : Object.entries(items); + const data: PaymentItem[] = entries.map(([id, item]) => ({ id, ...item, }));
159-164: Tighten validation for Customer ID by type and add guard.For user/team, validate UUID to reduce avoidable roundtrips; for custom, accept any non-empty string. Also guard undefined customerType (if any).
- const schema = yup.object({ - customerId: yup.string().defined().label("Customer ID"), + const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + const schema = yup.object({ + customerId: (customerType === "custom" + ? yup.string().trim().min(1, "ID is required").defined() + : yup.string().matches(uuid, "Must be a valid UUID (v4)").defined() + ).label("Customer ID"), quantity: yup.number().defined().label("Quantity"), description: yup.string().optional().label("Description"), expiresAt: yup.date().optional().label("Expires At"), }); @@ - const submit = async (values: yup.InferType<typeof schema>) => { + const submit = async (values: yup.InferType<typeof schema>) => { + if (!customerType) { + toast({ title: "Item has no customer type configured", variant: "destructive" }); + return "prevent-close" as const; + } const result = await Result.fromPromise(stackAdminApp.createItemQuantityChange({ ...(customerType === "user" ? { userId: values.customerId } : customerType === "team" ? { teamId: values.customerId } : { customCustomerId: values.customerId } ),Also applies to: 166-173
183-189: Optional: handle CustomerDoesNotExist for custom IDs.If the backend ever validates custom IDs existence, map KnownErrors.CustomerDoesNotExist to a specific message instead of the generic fallback.
- } else if (result.error instanceof KnownErrors.TeamNotFound) { + } else if (result.error instanceof KnownErrors.TeamNotFound) { toast({ title: "No team found with the given ID", variant: "destructive" }); + } else if (result.error instanceof KnownErrors.CustomerDoesNotExist) { + toast({ title: "No custom customer found with the given ID", variant: "destructive" });packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)
515-522: Minor: factor id-container mapping into a helper for reuse and exhaustiveness.A small helper improves readability and enables a never-check for future union expansions.
- await this._interface.updateItemQuantity( - { itemId: options.itemId, ...("userId" in options ? { userId: options.userId } : ("teamId" in options ? { teamId: options.teamId } : { customCustomerId: options.customCustomerId })) }, + const idContainer = + "userId" in options ? { userId: options.userId } : + "teamId" in options ? { teamId: options.teamId } : + "customCustomerId" in options ? { customCustomerId: options.customCustomerId } : + ((): never => { throw new Error("Invalid options"); })(); + await this._interface.updateItemQuantity( + { itemId: options.itemId, ...idContainer }, { delta: options.quantity, expires_at: options.expiresAt, description: options.description, } );
📜 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 (34)
apps/backend/prisma/migrations/20250821175509_test_mode_subscriptions/migration.sql(1 hunks)apps/backend/prisma/schema.prisma(3 hunks)apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx(1 hunks)apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts(3 hunks)apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx(4 hunks)apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts(3 hunks)apps/backend/src/lib/payments.tsx(4 hunks)apps/backend/src/lib/stripe.tsx(2 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/items/page-client.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/items/page.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/offers/page-client.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/offers/page.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx(2 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx(2 hunks)apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx(7 hunks)apps/dashboard/src/app/(main)/purchase/return/page-client.tsx(3 hunks)apps/dashboard/src/app/(main)/purchase/return/page.tsx(2 hunks)apps/dashboard/src/components/data-table/payment-item-table.tsx(4 hunks)apps/dashboard/src/components/data-table/payment-offer-table.tsx(3 hunks)apps/dashboard/src/components/dialog-opener.tsx(1 hunks)apps/dashboard/src/components/form-fields/day-interval-selector-field.tsx(2 hunks)apps/dashboard/src/components/payments/included-item-editor.tsx(1 hunks)apps/dashboard/src/components/payments/item-dialog.tsx(1 hunks)apps/dashboard/src/components/payments/offer-dialog.tsx(1 hunks)apps/dashboard/src/components/payments/price-editor.tsx(1 hunks)apps/dashboard/src/lib/utils.tsx(2 hunks)apps/dashboard/tailwind.config.ts(1 hunks)apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts(3 hunks)apps/e2e/tests/backend/endpoints/api/v1/payments/validate-code.test.ts(1 hunks)packages/stack-shared/src/config/schema.ts(2 hunks)packages/stack-shared/src/interface/admin-interface.ts(1 hunks)packages/stack-shared/src/schema-fields.ts(3 hunks)packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts(1 hunks)packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
- apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer ES6 Map over Record where feasible
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/offers/page.tsxapps/dashboard/tailwind.config.tsapps/dashboard/src/lib/utils.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/offers/page-client.tsxapps/e2e/tests/backend/endpoints/api/v1/payments/validate-code.test.tspackages/stack-shared/src/config/schema.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/items/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsxapps/dashboard/src/components/payments/offer-dialog.tsxapps/dashboard/src/components/form-fields/day-interval-selector-field.tsxapps/dashboard/src/components/payments/item-dialog.tsxapps/dashboard/src/components/payments/price-editor.tsxapps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsxapps/backend/src/lib/stripe.tsxapps/dashboard/src/components/dialog-opener.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/items/page.tsxapps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsxapps/dashboard/src/app/(main)/purchase/[code]/page-client.tsxpackages/stack-shared/src/interface/admin-interface.tsapps/dashboard/src/app/(main)/purchase/return/page.tsxpackages/template/src/lib/stack-app/apps/implementations/admin-app-impl.tsapps/dashboard/src/components/payments/included-item-editor.tsxapps/backend/src/app/api/latest/payments/purchases/validate-code/route.tsapps/dashboard/src/app/(main)/purchase/return/page-client.tsxapps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.tspackages/stack-shared/src/schema-fields.tsapps/dashboard/src/components/data-table/payment-offer-table.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsxapps/dashboard/src/components/data-table/payment-item-table.tsxapps/backend/src/lib/payments.tsx
apps/e2e/**
📄 CodeRabbit inference engine (CLAUDE.md)
Always add new E2E tests when you change the API or SDK interface
Files:
apps/e2e/tests/backend/endpoints/api/v1/payments/validate-code.test.tsapps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In tests, prefer .toMatchInlineSnapshot where possible
Files:
apps/e2e/tests/backend/endpoints/api/v1/payments/validate-code.test.tsapps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts
apps/backend/src/app/api/latest/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place backend API route handlers under /apps/backend/src/app/api/latest and follow RESTful, resource-based paths (auth, users, teams, oauth-providers)
Files:
apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsxapps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsxapps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts
apps/backend/src/app/api/latest/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Use the custom route handler system in the backend for consistent API responses
Files:
apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts
🧬 Code graph analysis (18)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/offers/page.tsx (2)
apps/dashboard/src/lib/utils.tsx (1)
devFeaturesEnabledForProject(20-29)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/offers/page-client.tsx (1)
PageClient(9-33)
apps/dashboard/src/lib/utils.tsx (2)
packages/stack-shared/src/utils/json.tsx (1)
parseJson(72-74)apps/dashboard/src/lib/env.tsx (1)
getPublicEnvVar(49-59)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/offers/page-client.tsx (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/items/page-client.tsx (1)
PageClient(10-36)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp(27-34)apps/dashboard/src/components/dialog-opener.tsx (1)
DialogOpener(14-27)apps/dashboard/src/components/payments/offer-dialog.tsx (1)
OfferDialog(30-117)apps/dashboard/src/components/data-table/payment-offer-table.tsx (1)
PaymentOfferTable(51-64)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/items/page-client.tsx (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/offers/page-client.tsx (1)
PageClient(9-33)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp(27-34)apps/dashboard/src/components/dialog-opener.tsx (1)
DialogOpener(14-27)apps/dashboard/src/components/payments/item-dialog.tsx (1)
ItemDialog(30-100)apps/dashboard/src/components/data-table/payment-item-table.tsx (1)
PaymentItemTable(63-76)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx (1)
docs/src/components/icons.tsx (1)
Box(334-338)
apps/dashboard/src/components/payments/offer-dialog.tsx (5)
packages/stack-shared/src/schema-fields.ts (4)
offerSchema(561-579)userSpecifiedIdSchema(426-426)yupRecord(283-322)offerPriceSchema(555-560)apps/dashboard/src/components/form-dialog.tsx (1)
FormDialog(53-140)apps/dashboard/src/components/form-fields.tsx (3)
InputField(59-97)SelectField(229-266)CheckboxField(268-303)apps/dashboard/src/components/payments/price-editor.tsx (1)
PriceEditorField(18-78)apps/dashboard/src/components/payments/included-item-editor.tsx (1)
IncludedItemEditorField(18-88)
apps/dashboard/src/components/payments/item-dialog.tsx (5)
packages/stack-shared/src/config/schema.ts (1)
branchPaymentsSchema(116-147)packages/stack-shared/src/schema-fields.ts (2)
userSpecifiedIdSchema(426-426)dayIntervalOrNeverSchema(421-421)apps/dashboard/src/components/form-dialog.tsx (1)
FormDialog(53-140)apps/dashboard/src/components/form-fields.tsx (2)
InputField(59-97)SelectField(229-266)apps/dashboard/src/components/form-fields/day-interval-selector-field.tsx (1)
DayIntervalSelectorField(12-67)
apps/dashboard/src/components/payments/price-editor.tsx (2)
apps/dashboard/src/components/form-fields.tsx (1)
InputField(59-97)apps/dashboard/src/components/form-fields/day-interval-selector-field.tsx (1)
DayIntervalSelectorField(12-67)
apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx (8)
apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx (1)
POST(9-84)apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts (1)
POST(12-57)packages/stack-shared/src/schema-fields.ts (5)
yupObject(247-251)adminAuthTypeSchema(483-483)adaptSchema(330-330)yupString(187-190)yupNumber(191-194)apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx (1)
purchaseUrlVerificationCodeHandler(5-19)packages/stack-shared/src/utils/errors.tsx (2)
StatusError(152-261)StackAssertionError(69-85)apps/backend/src/prisma-client.tsx (1)
getPrismaClientForTenancy(51-53)packages/stack-shared/src/utils/strings.tsx (1)
typedToUppercase(30-33)packages/stack-shared/src/utils/dates.tsx (1)
addInterval(197-199)
apps/dashboard/src/components/dialog-opener.tsx (1)
packages/stack-ui/src/components/ui/button.tsx (1)
Button(92-92)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/items/page.tsx (2)
apps/dashboard/src/lib/utils.tsx (1)
devFeaturesEnabledForProject(20-29)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/items/page-client.tsx (1)
PageClient(10-36)
apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx (3)
apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx (1)
purchaseUrlVerificationCodeHandler(5-19)apps/backend/src/lib/tenancies.tsx (1)
getTenancy(68-77)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError(69-85)
apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx (4)
packages/stack-shared/src/schema-fields.ts (1)
inlineOfferSchema(580-601)packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts (2)
StackAdminApp(29-87)StackAdminApp(95-95)packages/stack-shared/src/utils/promises.tsx (1)
runAsynchronouslyWithAlert(312-328)packages/stack-shared/src/utils/objects.tsx (1)
typedEntries(263-265)
apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts (3)
apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx (1)
purchaseUrlVerificationCodeHandler(5-19)apps/backend/src/lib/tenancies.tsx (1)
getTenancy(68-77)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError(69-85)
apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts (2)
apps/e2e/tests/helpers.ts (1)
it(10-10)apps/e2e/tests/backend/backend-helpers.ts (1)
niceBackendFetch(107-166)
apps/dashboard/src/components/data-table/payment-offer-table.tsx (5)
packages/stack-shared/src/config/schema.ts (1)
branchPaymentsSchema(116-147)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp(27-34)packages/stack-ui/src/components/data-table/cells.tsx (1)
ActionCell(72-123)apps/dashboard/src/components/payments/offer-dialog.tsx (1)
OfferDialog(30-117)packages/stack-ui/src/components/action-dialog.tsx (1)
ActionDialog(31-135)
apps/dashboard/src/components/data-table/payment-item-table.tsx (6)
packages/stack-shared/src/config/schema.ts (1)
branchPaymentsSchema(116-147)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp(27-34)packages/stack-ui/src/components/data-table/cells.tsx (1)
ActionCell(72-123)apps/dashboard/src/components/payments/item-dialog.tsx (1)
ItemDialog(30-100)packages/stack-ui/src/components/action-dialog.tsx (1)
ActionDialog(31-135)packages/stack-shared/src/known-errors.tsx (2)
KnownErrors(1515-1517)KnownErrors(1519-1636)
apps/backend/src/lib/payments.tsx (6)
apps/backend/src/prisma-client.tsx (1)
PrismaClientTransaction(16-16)apps/backend/src/lib/tenancies.tsx (1)
Tenancy(47-47)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/utils/uuids.tsx (1)
isUuid(25-27)packages/stack-shared/src/known-errors.tsx (2)
KnownErrors(1515-1517)KnownErrors(1519-1636)
⏰ 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: docker
- GitHub Check: all-good
- GitHub Check: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: Security Check
Important
Introduces 'custom' customer type support across API, models, and tests, updating methods and validation schemas accordingly.
schema.prisma,payments.tsx, andserver-app-impl.ts.route.tsfiles to includecustomer_typeparameter.createPurchaseUrlandupdateItemQuantitymethods to handle 'custom' type.CustomerTypeenum inschema.prisma.customerIdtype toTEXTinSubscriptionandItemQuantityChangemodels.customerTypeSchemainschema-fields.tsto include 'custom'.payments.test.tsanditems.test.tsfor 'custom' type scenarios.createPurchaseUrlmethod fromadmin-interface.ts.client-app-impl.tsandserver-app-impl.tsto support 'custom' type in item-related methods.This description was created by
for 509762d. 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