-
Notifications
You must be signed in to change notification settings - Fork 476
Payment transactions #990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Payment transactions #990
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. WalkthroughReplaces the legacy AdminTransaction shape with a typed Transaction schema and entry-based model; adds transaction-builder functions; updates the backend transactions route, shared SDK/types, dashboard table rendering, and end-to-end tests to consume and return the new Transaction objects. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Route as GET /internal/payments/transactions
participant DB as Database
participant Builder as Transaction Builder
participant Schema as transactionSchema
Client->>Route: GET /transactions?type=...&cursor=...
Route->>DB: fetch subscriptions, one-time purchases, item-qty-changes (parallel)
DB-->>Route: raw rows (subs / otps / iqcs)
Route->>Route: merge & sort rows -> TransactionRow[]
Route->>Builder: buildTransaction(row) for each row
Builder-->>Route: Transaction
Route->>Schema: validate Transaction[]
Schema-->>Route: validated
Route-->>Client: { transactions: Transaction[], next_cursor }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
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 Overview
Greptile Summary
This PR refactors the payment transaction API to use dedicated builder functions, improving code organization and maintainability.
Key Changes
- Refactored transaction construction: Extracted transaction building logic from the route handler into dedicated builder functions (
buildSubscriptionTransaction,buildOneTimePurchaseTransaction,buildItemQuantityChangeTransaction) - Added comprehensive tests: New test file covers edge cases including missing product snapshots, test mode handling, and money amount multiplication
- Improved type safety: Better separation between internal
TransactionSourcetypes and externalTransactiontypes - Frontend integration: Added transaction table component with filtering by type and customer type
- Money arithmetic: Implemented proper decimal handling for multi-currency amounts using BigInt for precision
Architecture Improvements
The refactoring follows good separation of concerns:
- Route handler manages pagination and API concerns
- Builder functions handle transaction object construction
- Money arithmetic is isolated and well-tested
- Type definitions are centralized in shared package
Confidence Score: 4/5
- This PR is safe to merge with minimal risk - the refactoring improves code quality and has comprehensive test coverage
- Score reflects solid refactoring with good test coverage. Minor concern about pagination efficiency when filtering after merge-sort, but unlikely to cause issues in practice
- Pay attention to
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx- the pagination logic filters transactions after fetching and merging, which may need optimization for large datasets
Important Files Changed
File Analysis
| Filename | Score | Overview |
|---|---|---|
| apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx | 4/5 | Refactored to use builder functions for transaction construction; improved maintainability and separation of concerns |
| apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts | 5/5 | New file with comprehensive test coverage; implements transaction building logic with proper money arithmetic |
| apps/dashboard/src/components/data-table/transaction-table.tsx | 4/5 | Frontend component for displaying transactions with filtering and pagination; uses Map for customer data lookup |
Sequence Diagram
sequenceDiagram
participant Client as Dashboard Client
participant API as Transaction API Route
participant Builder as Transaction Builder
participant DB as Prisma Database
participant Interface as Admin Interface
Client->>Interface: listTransactions(params)
Interface->>API: GET /internal/payments/transactions
Note over API: Parse cursor (sub|iqc|otp)
Note over API: Validate limit (1-200)
API->>DB: Find subscription cursor
API->>DB: Find item change cursor
API->>DB: Find purchase cursor
par Fetch all transaction sources
API->>DB: findMany(subscriptions)
API->>DB: findMany(itemQuantityChanges)
API->>DB: findMany(oneTimePurchases)
end
DB-->>API: Return records
loop For each source type
API->>Builder: buildSubscriptionTransaction
Builder-->>API: Transaction object
API->>Builder: buildItemQuantityChangeTransaction
Builder-->>API: Transaction object
API->>Builder: buildOneTimePurchaseTransaction
Builder-->>API: Transaction object
end
Note over API: Sort by createdAt DESC, id DESC
Note over API: Filter by transaction type
Note over API: Slice to page limit
Note over API: Build next cursor
API-->>Interface: {transactions, next_cursor}
Interface-->>Client: {transactions, nextCursor}
Client->>Client: Display in TransactionTable
9 files reviewed, 1 comment
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx
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
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx(4 hunks)apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.test.ts(1 hunks)apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts(1 hunks)apps/backend/src/lib/email-rendering.tsx(2 hunks)apps/dashboard/src/components/data-table/transaction-table.tsx(2 hunks)packages/stack-shared/src/interface/admin-interface.ts(3 hunks)packages/stack-shared/src/interface/crud/transactions.ts(1 hunks)packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts(3 hunks)packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.test.tsapps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.tsapps/backend/src/lib/email-rendering.tsxapps/dashboard/src/components/data-table/transaction-table.tsxpackages/template/src/lib/stack-app/apps/implementations/admin-app-impl.tspackages/stack-shared/src/interface/admin-interface.tsapps/backend/src/app/api/latest/internal/payments/transactions/route.tsxpackages/template/src/lib/stack-app/apps/interfaces/admin-app.tspackages/stack-shared/src/interface/crud/transactions.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
When writing tests, prefer .toMatchInlineSnapshot over other selectors where possible
Files:
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.test.ts
apps/{dashboard,dev-launchpad}/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
For blocking alerts and errors in the UI, never use toast; use alerts instead
Files:
apps/dashboard/src/components/data-table/transaction-table.tsx
apps/{dashboard,dev-launchpad}/**/*.{css,tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Animations: keep hover/click transitions snappy; do not delay actions with pre-hover transitions; apply transitions after the action (e.g., fade-out on hover end)
Files:
apps/dashboard/src/components/data-table/transaction-table.tsx
packages/template/**
📄 CodeRabbit inference engine (AGENTS.md)
When changes are needed for stack or js packages, make them in packages/template instead
Files:
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.tspackages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
🧠 Learnings (1)
📚 Learning: 2025-10-20T22:25:40.427Z
Learnt from: CR
PR: stack-auth/stack-auth#0
File: AGENTS.md:0-0
Timestamp: 2025-10-20T22:25:40.427Z
Learning: Applies to apps/backend/src/app/api/latest/**/route.ts : In backend API routes, use the custom route handler system to ensure consistent API responses
Applied to files:
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx
🧬 Code graph analysis (8)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.test.ts (2)
apps/backend/src/lib/tenancies.tsx (1)
Tenancy(47-47)apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (3)
buildSubscriptionTransaction(173-216)buildOneTimePurchaseTransaction(218-261)buildItemQuantityChangeTransaction(263-291)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (3)
packages/stack-shared/src/utils/currency-constants.tsx (2)
Currency(3-7)SUPPORTED_CURRENCIES(9-45)packages/stack-shared/src/utils/strings.tsx (1)
typedToLowercase(15-18)apps/backend/src/lib/tenancies.tsx (1)
Tenancy(47-47)
apps/dashboard/src/components/data-table/transaction-table.tsx (4)
packages/stack-shared/src/interface/crud/transactions.ts (4)
TransactionEntry(176-176)Transaction(206-206)TransactionType(189-189)TRANSACTION_TYPES(178-187)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp(29-44)packages/stack-ui/src/components/data-table/cells.tsx (2)
AvatarCell(45-52)TextCell(7-43)packages/stack-ui/src/components/data-table/data-table.tsx (1)
DataTableManualPagination(174-238)
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (2)
packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
createCache(29-34)packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionType(189-189)Transaction(206-206)
packages/stack-shared/src/interface/admin-interface.ts (1)
packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionType(189-189)Transaction(206-206)
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx (2)
packages/stack-shared/src/interface/crud/transactions.ts (3)
TRANSACTION_TYPES(178-187)transactionSchema(191-204)Transaction(206-206)apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (3)
buildSubscriptionTransaction(173-216)buildItemQuantityChangeTransaction(263-291)buildOneTimePurchaseTransaction(218-261)
packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts (1)
packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionType(189-189)Transaction(206-206)
packages/stack-shared/src/interface/crud/transactions.ts (4)
packages/stack-shared/src/utils/currency-constants.tsx (3)
SUPPORTED_CURRENCIES(9-45)Currency(3-7)MoneyAmount(1-1)packages/stack-shared/src/utils/errors.tsx (1)
throwErr(10-19)packages/stack-shared/src/schema-fields.ts (8)
yupString(187-190)yupObject(247-251)yupNumber(191-194)customerTypeSchema(547-547)productSchema(569-592)yupUnion(257-281)yupArray(213-216)yupBoolean(195-198)packages/stack-shared/src/utils/objects.tsx (1)
typedFromEntries(281-283)
⏰ 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). (12)
- GitHub Check: all-good
- GitHub Check: Vercel Agent Review
- GitHub Check: lint_and_build (latest)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: docker
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: Security Check
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.test.ts
Outdated
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.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: 1
♻️ Duplicate comments (1)
apps/dashboard/src/components/data-table/transaction-table.tsx (1)
131-139: USD-only handling remains unresolved.This issue was already flagged in a previous review. The function still only displays USD amounts and shows '—' for all other currencies, which blanks the amount column for non-USD tenants. Please refer to the previous review comment for the proposed solution.
🧹 Nitpick comments (2)
apps/dashboard/src/components/data-table/transaction-table.tsx (2)
51-58: Consider simplifying the redundant check.The condition at line 56 returns
'other'when aproductGrantexists without subscription or purchase IDs, but line 57 also returns'other'as the default. Since both paths lead to the same result, line 56 can be removed.Apply this diff:
function deriveSourceType(transaction: Transaction): SourceType { if (transaction.entries.some(isItemQuantityChangeEntry)) return 'item_quantity_change'; const productGrant = transaction.entries.find(isProductGrantEntry); if (productGrant?.subscription_id) return 'subscription'; if (productGrant?.one_time_purchase_id) return 'one_time'; - if (productGrant) return 'other'; return 'other'; }
86-89: Improve type safety in the default fallback.The
as anycast suppresses type checking. SincetransactionTypeis typed asTransactionType | null, the default case should only handlenull. Consider using a type-safe fallback.Apply this diff:
default: { - return { label: (transactionType as any) ?? '—', Icon: CircleHelp }; + return { label: transactionType ?? 'Unknown', Icon: CircleHelp }; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/dashboard/src/components/data-table/transaction-table.tsx(2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
apps/dashboard/src/components/data-table/transaction-table.tsx
apps/{dashboard,dev-launchpad}/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
For blocking alerts and errors in the UI, never use toast; use alerts instead
Files:
apps/dashboard/src/components/data-table/transaction-table.tsx
apps/{dashboard,dev-launchpad}/**/*.{css,tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Animations: keep hover/click transitions snappy; do not delay actions with pre-hover transitions; apply transitions after the action (e.g., fade-out on hover end)
Files:
apps/dashboard/src/components/data-table/transaction-table.tsx
🧬 Code graph analysis (1)
apps/dashboard/src/components/data-table/transaction-table.tsx (4)
packages/stack-shared/src/interface/crud/transactions.ts (4)
TransactionEntry(176-176)Transaction(206-206)TransactionType(189-189)TRANSACTION_TYPES(178-187)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp(29-44)packages/stack-ui/src/components/data-table/cells.tsx (2)
AvatarCell(45-52)TextCell(7-43)packages/stack-ui/src/components/data-table/data-table.tsx (1)
DataTableManualPagination(174-238)
⏰ 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). (12)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: setup-tests
- GitHub Check: lint_and_build (latest)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: restart-dev-and-test
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: all-good
- GitHub Check: Vercel Agent Review
- GitHub Check: docker
- GitHub Check: Security Check
🔇 Additional comments (1)
apps/dashboard/src/components/data-table/transaction-table.tsx (1)
172-172: Verify the test mode amount display behavior.Test mode transactions display
'Test mode'instead of the actual amount. Please confirm whether this is the intended UX or if both the test mode indicator and the amount should be shown (e.g.,'$10.00 (Test mode)').
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx
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: 1
♻️ Duplicate comments (3)
apps/dashboard/src/components/data-table/transaction-table.tsx (2)
131-139: Fix amount display for non-USD charges.This issue was flagged in a previous review and remains unresolved.
pickChargedAmountDisplayonly returns USD amounts; all other currencies render as'—', which blanks the amount column for non-USD tenants.
192-218: Fix critical type mismatch in transaction filtering logic.This issue was flagged in a previous review and remains unresolved. The column returns
SourceTypevalues but the toolbar populates the filter dropdown withTransactionTypevalues fromTRANSACTION_TYPES. These value sets don't overlap, so filtering will never match and always return empty results.apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (1)
125-146: Net amount is forced to USD "0" for non‑USD charges.This issue was flagged in a previous review and remains unresolved. If a purchase is denominated solely in a non-USD currency (e.g., EUR), this code emits
net_amount: { USD: "0" }, causing downstream reporting to falsely record zero revenue.This is compounded by the schema constraint in
packages/stack-shared/src/interface/crud/transactions.ts(lines 90-92) that only allows USD innet_amount.
🧹 Nitpick comments (1)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (1)
263-291: Unused parameter in buildItemQuantityChangeTransaction.The
tenancyparameter is accepted but never used in the function body. Consider removing it if it's not needed, or add a comment explaining why it's reserved for future use.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.test.ts(1 hunks)apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts(1 hunks)apps/dashboard/src/components/data-table/transaction-table.tsx(2 hunks)apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts(3 hunks)packages/stack-shared/src/interface/crud/transactions.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.test.ts
🧰 Additional context used
🧬 Code graph analysis (3)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (5)
packages/stack-shared/src/schema-fields.ts (1)
productSchema(569-592)apps/backend/src/lib/payments.tsx (1)
productToInlineProduct(427-443)packages/stack-shared/src/utils/currency-constants.tsx (2)
Currency(3-7)SUPPORTED_CURRENCIES(9-45)packages/stack-shared/src/utils/strings.tsx (1)
typedToLowercase(15-18)apps/backend/src/lib/tenancies.tsx (1)
Tenancy(47-47)
apps/dashboard/src/components/data-table/transaction-table.tsx (6)
packages/stack-shared/src/interface/crud/transactions.ts (4)
TransactionEntry(176-176)Transaction(206-206)TransactionType(189-189)TRANSACTION_TYPES(178-187)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp(29-44)packages/stack-ui/src/components/data-table/cells.tsx (2)
AvatarCell(45-52)TextCell(7-43)packages/stack-ui/src/components/ui/tooltip.tsx (3)
Tooltip(40-40)TooltipTrigger(40-40)TooltipContent(40-40)packages/stack-ui/src/components/data-table/data-table.tsx (1)
DataTableManualPagination(174-238)packages/stack-ui/src/components/ui/select.tsx (5)
Select(160-160)SelectTrigger(160-160)SelectValue(160-160)SelectContent(160-160)SelectItem(160-160)
packages/stack-shared/src/interface/crud/transactions.ts (4)
packages/stack-shared/src/utils/currency-constants.tsx (3)
SUPPORTED_CURRENCIES(9-45)Currency(3-7)MoneyAmount(1-1)packages/stack-shared/src/utils/errors.tsx (1)
throwErr(10-19)packages/stack-shared/src/schema-fields.ts (8)
yupString(187-190)yupObject(247-251)yupNumber(191-194)customerTypeSchema(547-547)inlineProductSchema(606-631)yupUnion(257-281)yupArray(213-216)yupBoolean(195-198)packages/stack-shared/src/utils/objects.tsx (1)
typedFromEntries(281-283)
⏰ 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). (12)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: docker
- GitHub Check: restart-dev-and-test
- GitHub Check: lint_and_build (latest)
- GitHub Check: all-good
- GitHub Check: setup-tests
- GitHub Check: Vercel Agent Review
- GitHub Check: Security Check
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
♻️ Duplicate comments (1)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (1)
124-145: Net amount is forced to USD "0" for non‑USD charges.This issue was already identified in a previous review. When a purchase is denominated solely in EUR or another non-USD currency, this code still emits
net_amount: { USD: "0" }, falsely recording zero revenue. Thenet_amountshould reflect the actual charged currencies instead of fabricating zero USD values.
🧹 Nitpick comments (2)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (2)
30-52: Consider validating the fallback structure.The
as ProductSnapshotcast on line 41 bypasses type checking. While the structure appears complete, ifProductSnapshotevolves, this could lead to runtime type mismatches.Consider using a schema validator or ensuring all fields match the expected type explicitly:
return { display_name: options.displayName, customer_type: options.customerType, prices: {}, stackable: false, server_only: false, included_items: {}, + client_metadata: null, + client_read_only_metadata: null, + server_metadata: null, - } as ProductSnapshot; + };
172-260: Consider extracting shared logic from subscription and one-time purchase builders.
buildSubscriptionTransactionandbuildOneTimePurchaseTransactionshare nearly identical logic (~45 lines of duplication), differing only in which ID field they pass tocreateProductGrantEntry. Extracting common logic would improve maintainability.Consider creating a shared helper:
function buildPurchaseTransaction(options: { entity: Subscription | OneTimePurchase, idField: { subscriptionId?: string } | { oneTimePurchaseId?: string }, }): Transaction { const { entity, idField } = options; const customerType = typedToLowercase(entity.customerType); const product = entity.product as InferType<typeof productSchema> | null; const productSnapshot = ensureProductSnapshot(product, customerType); const selectedPrice = product ? resolveSelectedPriceFromProduct(product, entity.priceId ?? null) : null; const quantity = entity.quantity; const chargedAmount = buildChargedAmount(selectedPrice, quantity); const testMode = entity.creationSource === "TEST_MODE"; const entries: TransactionEntry[] = [ createProductGrantEntry({ customerType, customerId: entity.customerId, productId: entity.productId ?? null, product: productSnapshot, priceId: entity.priceId ?? null, quantity, ...idField, }), ]; const moneyTransfer = createMoneyTransferEntry({ customerType, customerId: entity.customerId, chargedAmount, skip: testMode, }); if (moneyTransfer) { entries.push(moneyTransfer); } return { id: entity.id, created_at_millis: entity.createdAt.getTime(), effective_at_millis: entity.createdAt.getTime(), type: "purchase", entries, adjusted_by: [], test_mode: testMode, }; } export function buildSubscriptionTransaction(options: { subscription: Subscription }): Transaction { return buildPurchaseTransaction({ entity: options.subscription, idField: { subscriptionId: options.subscription.id }, }); } export function buildOneTimePurchaseTransaction(options: { purchase: OneTimePurchase }): Transaction { return buildPurchaseTransaction({ entity: options.purchase, idField: { oneTimePurchaseId: options.purchase.id }, }); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (6)
packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionEntry(176-176)Transaction(206-206)packages/stack-shared/src/schema-fields.ts (1)
productSchema(569-592)apps/backend/src/lib/payments.tsx (1)
productToInlineProduct(427-443)packages/stack-shared/src/utils/currency-constants.tsx (2)
Currency(3-7)SUPPORTED_CURRENCIES(9-45)packages/stack-shared/src/utils/strings.tsx (1)
typedToLowercase(15-18)apps/backend/src/lib/tenancies.tsx (1)
Tenancy(47-47)
⏰ 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). (5)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: all-good
- GitHub Check: Vercel Agent Review
- GitHub Check: Security Check
🔇 Additional comments (6)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (6)
1-8: LGTM!Imports are well-organized and appropriate for the transaction builder functionality.
10-28: LGTM!Type definitions provide appropriate flexibility for handling various product and price configurations.
54-63: LGTM!The function correctly handles edge cases and null guards. The
as anycast on line 61 is acceptable for field stripping in this context.
65-122: LGTM!The money multiplication logic correctly handles decimal arithmetic using BigInt for precision, and properly manages signs and currency decimals. The charged amount builder appropriately iterates through supported currencies.
147-170: LGTM!The product grant entry structure is correct and includes all necessary fields for tracking product grants in transactions.
262-290: LGTM!The item quantity change transaction builder is straightforward and correctly constructs a transaction with a single
item_quantity_changeentry.
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
🧹 Nitpick comments (1)
packages/stack-shared/src/interface/admin-interface.ts (1)
598-611: Method signature correctly updated to new Transaction model.The method signature, query construction, and return type are all correctly updated to use
TransactionandTransactionType. The implementation properly handles optional parameters and builds the query string as needed.Consider extracting
customerTypeto a type definition similar toTransactionTypefor consistency:In
packages/stack-shared/src/interface/crud/transactions.ts, add:export const CUSTOMER_TYPES = ['user', 'team', 'custom'] as const; export type CustomerType = (typeof CUSTOMER_TYPES)[number];Then update this method signature:
-async listTransactions(params?: { cursor?: string, limit?: number, type?: TransactionType, customerType?: 'user' | 'team' | 'custom' }): Promise<{ transactions: Transaction[], nextCursor: string | null }> +async listTransactions(params?: { cursor?: string, limit?: number, type?: TransactionType, customerType?: CustomerType }): Promise<{ transactions: Transaction[], nextCursor: string | null }>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
packages/stack-shared/src/interface/admin-interface.ts(3 hunks)packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts(3 hunks)packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
🧰 Additional context used
🧬 Code graph analysis (2)
packages/stack-shared/src/interface/admin-interface.ts (1)
packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionType(111-111)Transaction(128-128)
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (2)
packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
createCache(29-34)packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionType(111-111)Transaction(128-128)
⏰ 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). (11)
- GitHub Check: Vercel Agent Review
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: docker
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: build (22.x)
- GitHub Check: all-good
- GitHub Check: setup-tests
- GitHub Check: lint_and_build (latest)
🔇 Additional comments (4)
packages/stack-shared/src/interface/admin-interface.ts (1)
11-11: LGTM! Import updated to use new Transaction model.The import correctly switches from
AdminTransactiontoTransactionandTransactionType, aligning with the PR's refactor to a typed transaction schema.packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (3)
7-7: LGTM! Import correctly references new Transaction types.The import properly brings in
TransactionandTransactionTypefrom the shared package, aligning with the refactor to a unified transaction model.
77-79: Cache key correctly updated with new TransactionType.The cache tuple properly includes
TransactionType | undefinedand maintains parameter order alignment with thelistTransactionsmethod signature, ensuring correct cache behavior for different query combinations.
594-604: LGTM! Transaction methods consistently updated.Both
listTransactionsanduseTransactionsare correctly updated to use the newTransactionandTransactionTypetypes. Cache access patterns properly align with the cache definition, and return types are consistent across both the async method and React hook.
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.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.
needs more tests for all the new behavior
N2D4
left a comment
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.
code looks good besides the missing tests! can you make a video with all the different types of transactions and edge cases in the UI?
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.
This PR is being reviewed by Cursor Bugbot
Details
You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx
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
♻️ Duplicate comments (2)
apps/dashboard/src/components/data-table/transaction-table.tsx (2)
192-356: Restore functional type filtering
Thesource_typecolumn still returnsSourceTypevalues while the toolbar hands itTransactionTypevalues and the API filter expectsTransactionType. As soon as you pick a type, the table accessor never matches, so the UI shows an empty grid even though the backend returns data. We need this column (and all related wiring) to operate directly ontransaction.typeso the table, toolbar, and API stay in sync.@@ - { - id: 'source_type', - accessorFn: (transaction) => summaryById.get(transaction.id)?.sourceType ?? 'other', + { + id: 'type', + accessorFn: (transaction) => transaction.type, header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Type" />, cell: ({ row }) => { const summary = summaryById.get(row.original.id); @@ - const newFilters: { cursor?: string, limit?: number, type?: TransactionType, customerType?: 'user' | 'team' | 'custom' } = { + const newFilters: { cursor?: string, limit?: number, type?: TransactionType, customerType?: 'user' | 'team' | 'custom' } = { cursor: options.cursor, limit: options.limit, - type: options.columnFilters.find(f => f.id === 'source_type')?.value as any, + type: options.columnFilters.find(f => f.id === 'type')?.value as any, customerType: options.columnFilters.find(f => f.id === 'customer')?.value as any, }; @@ defaultVisibility={{ - source_type: true, + type: true, customer: true, amount: true, detail: true, created_at_millis: true, }} defaultColumnFilters={[ - { id: 'source_type', value: undefined }, + { id: 'type', value: undefined }, { id: 'customer', value: undefined }, ]} @@ - toolbarRender={(table) => { - const selectedType = table.getColumn('source_type')?.getFilterValue() as TransactionType | undefined; + toolbarRender={(table) => { + const typeColumn = table.getColumn('type'); + const selectedType = typeColumn?.getFilterValue() as TransactionType | undefined; @@ - <Select - value={selectedType ?? ''} - onValueChange={(v) => table.getColumn('source_type')?.setFilterValue(v === '__clear' ? undefined : v)} + <Select + value={selectedType ?? ''} + onValueChange={(v) => typeColumn?.setFilterValue(v === '__clear' ? undefined : v)} >
131-139: Show real amounts for every currency
pickChargedAmountDisplaystill hardcodes USD, so non-USD tenants see"Non USD amount"and USD entries with undefined amounts render as"$undefined". Please surface whichever currency actually has a value and fall back to'—'only when none do.function pickChargedAmountDisplay(entry: MoneyTransferEntry | undefined): string { if (!entry) return '—'; const chargedAmount = entry.charged_amount as Record<string, string | undefined>; - if ("USD" in chargedAmount) { - return `$${chargedAmount.USD}`; - } - // TODO: Handle other currencies - return 'Non USD amount'; + const [currency, amount] = + Object.entries(chargedAmount).find(([, value]) => typeof value === 'string' && value.length > 0) ?? []; + if (!currency || !amount) { + return '—'; + } + return currency === 'USD' ? `$${amount}` : `${currency} ${amount}`; }
🧹 Nitpick comments (3)
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts (3)
5-8: Consider stronger typing for payment configuration.Using
Record<string, any>forextraProductsandextraItemsloses type safety and could hide configuration errors.Consider defining explicit types that match the product and item schemas used in the actual payment configuration, or at minimum use
Record<string, unknown>to force type narrowing at usage sites.
13-37: Consider extracting base payment configurations to module-level constants.The
baseProductsandbaseItemsare defined inline, which could lead to duplication if these configurations are needed in other test files or helper functions.Extract these to module-level constants at the top of the file:
+const BASE_PRODUCTS = { + "sub-product": { + displayName: "Sub Product", + customerType: "user", + serverOnly: false, + stackable: false, + prices: { + monthly: { USD: "1000", interval: [1, "month"] }, + }, + includedItems: {}, + }, + "otp-product": { + displayName: "One-Time Product", + customerType: "user", + serverOnly: false, + stackable: false, + prices: { + single: { USD: "5000" }, + }, + includedItems: {}, + }, +}; + +const BASE_ITEMS = { + credits: { displayName: "Credits", customerType: "user" }, +}; + async function setupProjectWithPaymentsConfig(options: PaymentsConfigOptions = {}) { await Project.createAndSwitch(); await PaymentsHelper.setup(); - const baseProducts = { - "sub-product": { ... }, - "otp-product": { ... }, - }; - const baseItems = { - credits: { displayName: "Credits", customerType: "user" }, - }; await Project.updateConfig({ payments: { testMode: true, products: { - ...baseProducts, + ...BASE_PRODUCTS, ...(options.extraProducts ?? {}), }, items: { - ...baseItems, + ...BASE_ITEMS, ...(options.extraItems ?? {}), }, }, }); }
332-395: LGTM with optional readability improvement.Excellent test coverage for customer_type filtering across multiple sources (purchases and item changes).
The nested
every()assertions on lines 384-386 and 394 could be more readable:- expect(teamResponse.body.transactions.every((tx: any) => - tx.entries.every((entry: any) => entry.customer_type === "team") - )).toBe(true); + const allTeamEntries = teamResponse.body.transactions + .flatMap((tx: any) => tx.entries) + .every((entry: any) => entry.customer_type === "team"); + expect(allTeamEntries).toBe(true);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/dashboard/src/components/data-table/transaction-table.tsx(2 hunks)apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/dashboard/src/components/data-table/transaction-table.tsx (3)
packages/stack-shared/src/interface/crud/transactions.ts (4)
TransactionEntry(98-98)Transaction(128-128)TransactionType(111-111)TRANSACTION_TYPES(100-109)packages/stack-ui/src/components/data-table/cells.tsx (2)
AvatarCell(45-52)TextCell(7-43)packages/stack-ui/src/components/data-table/data-table.tsx (1)
DataTableManualPagination(174-238)
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts (2)
apps/e2e/tests/backend/backend-helpers.ts (1)
niceBackendFetch(109-173)apps/e2e/tests/helpers.ts (1)
it(12-12)
⏰ 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). (11)
- GitHub Check: Vercel Agent Review
- GitHub Check: restart-dev-and-test
- GitHub Check: build (22.x)
- GitHub Check: all-good
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: docker
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
🔇 Additional comments (7)
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts (7)
53-76: LGTM!The refactoring to support multiple customer types (user/team/custom) is clean and maintains backward compatibility through the wrapper function.
113-156: LGTM!The snapshot correctly reflects the new transaction model with the entries-based structure, including all expected fields like
adjusted_by,effective_at_millis, and nested product metadata.
175-210: LGTM!The one-time purchase snapshot correctly uses
one_time_purchase_idinstead ofsubscription_idwhile maintaining the same entry-based structure.
229-251: LGTM!The item quantity change snapshot correctly uses the
item_quantity_changeentry type with appropriate fields and themanual-item-quantity-changetransaction type.
297-330: LGTM!Good test coverage for the new transaction type filtering feature, verifying that each filter correctly returns only matching transactions.
397-473: LGTM!Good test coverage for server-granted subscriptions. The snapshot correctly shows
price_id: nullsince these grants bypass the payment flow.
1-1: Test coverage has improved but consider additional scenarios.The new tests added (filtering by type, filtering by customer_type, and server-granted subscriptions) address some of the previous feedback. However, consider adding tests for:
- Multiple entries within a single transaction
- The
adjusted_byfield being populated (when one transaction adjusts another)- Error cases with invalid filter parameters
- Pagination combined with filtering
- Edge cases like empty entries or null fields
Based on learnings (previous review comment by N2D4).
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: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts(3 hunks)packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (2)
packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
createCache(29-34)packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionType(111-111)Transaction(128-128)
⏰ 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). (11)
- GitHub Check: Vercel Agent Review
- GitHub Check: build (22.x)
- GitHub Check: docker
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: build (22.x)
- GitHub Check: all-good
- GitHub Check: build (22.x)
- GitHub Check: setup-tests
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: lint_and_build (latest)
| async listTransactions(params: { cursor?: string, limit?: number, type?: TransactionType, customerType?: 'user' | 'team' | 'custom' }): Promise<{ transactions: Transaction[], nextCursor: string | null }> { | ||
| const crud = Result.orThrow(await this._transactionsCache.getOrWait([params.cursor, params.limit, params.type, params.customerType] as const, "write-only")); | ||
| return crud; | ||
| } | ||
|
|
||
| // IF_PLATFORM react-like | ||
| useTransactions(params: { cursor?: string, limit?: number, type?: 'subscription' | 'one_time' | 'item_quantity_change', customerType?: 'user' | 'team' | 'custom' }): { transactions: AdminTransaction[], nextCursor: string | null } { | ||
| useTransactions(params: { cursor?: string, limit?: number, type?: TransactionType, customerType?: 'user' | 'team' | 'custom' }): { transactions: Transaction[], nextCursor: string | null } { | ||
| const data = useAsyncCache(this._transactionsCache, [params.cursor, params.limit, params.type, params.customerType] as const, "adminApp.useTransactions()"); | ||
| return data; | ||
| } |
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.
Restore safe defaults for transaction params
Both listTransactions and useTransactions now assume params is always provided. Calling either without an argument (the previous API behaviour) will dereference params when it’s undefined, raising a runtime TypeError before any type checks can help. Please keep the signature ergonomic by defaulting to an empty object and destructuring.
- async listTransactions(params: { cursor?: string, limit?: number, type?: TransactionType, customerType?: 'user' | 'team' | 'custom' }): Promise<{ transactions: Transaction[], nextCursor: string | null }> {
- const crud = Result.orThrow(await this._transactionsCache.getOrWait([params.cursor, params.limit, params.type, params.customerType] as const, "write-only"));
+ async listTransactions(params: { cursor?: string, limit?: number, type?: TransactionType, customerType?: 'user' | 'team' | 'custom' } = {}): Promise<{ transactions: Transaction[], nextCursor: string | null }> {
+ const { cursor, limit, type, customerType } = params;
+ const crud = Result.orThrow(await this._transactionsCache.getOrWait([cursor, limit, type, customerType] as const, "write-only"));
return crud;
}
// IF_PLATFORM react-like
- useTransactions(params: { cursor?: string, limit?: number, type?: TransactionType, customerType?: 'user' | 'team' | 'custom' }): { transactions: Transaction[], nextCursor: string | null } {
- const data = useAsyncCache(this._transactionsCache, [params.cursor, params.limit, params.type, params.customerType] as const, "adminApp.useTransactions()");
+ useTransactions(params: { cursor?: string, limit?: number, type?: TransactionType, customerType?: 'user' | 'team' | 'custom' } = {}): { transactions: Transaction[], nextCursor: string | null } {
+ const { cursor, limit, type, customerType } = params;
+ const data = useAsyncCache(this._transactionsCache, [cursor, limit, type, customerType] as const, "adminApp.useTransactions()");
return data;
}🤖 Prompt for AI Agents
In packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
around lines 602 to 611, both listTransactions and useTransactions assume params
is always passed and will throw if called with undefined; change the signatures
to default params = {} and destructure the four properties (cursor, limit, type,
customerType) from that object (with no-arg defaulting) and then pass those
variables into the cache/get calls instead of indexing params directly so the
methods are safe to call with no arguments.
https://www.loom.com/share/db645a1799454ec6b0234c55ee28cee9
Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Tests
Note
Introduce a new schema-driven Transaction model with builders, update the transactions API/SDK to return entry-based transactions, revamp the admin table UI, and extend tests/pagination/filters.
AdminTransactionwith unifiedTransactionmodel andTransactionEntryunion; addTRANSACTION_TYPES.effective_at_millis,entries,adjusted_by, multi-currencycharged_amount/net_amount.transactionSchema; supports filtering byTRANSACTION_TYPESand customer type.transaction-builderto construct entry-based transactions for subscriptions, one-time purchases, and manual item quantity changes (including money transfer/product grant entries and currency math).Transaction/TransactionTypeinlistTransactionsand related hooks.TRANSACTION_TYPES.effective_at_millis).Written by Cursor Bugbot for commit e77f688. This will update automatically on new commits. Configure here.