-
Notifications
You must be signed in to change notification settings - Fork 498
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
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. WalkthroughReplace legacy AdminTransaction with a typed, entry-based Transaction model and builders; update backend routes, SDKs, dashboard rendering, Stripe webhook handling, refund endpoint, Prisma schema/migrations, and tests to produce, validate, and consume 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: parallel fetch (subscriptions, one_time_purchases, item_quantity_changes, subscription_invoices)
DB-->>Route: rows per source
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 }
sequenceDiagram
participant Admin as Admin Client
participant RefundRoute as POST /internal/payments/transactions/refund
participant DB as Database
participant Stripe as Stripe API
participant Prisma as Prisma (DB write)
Admin->>RefundRoute: POST { type, id }
RefundRoute->>DB: validate target (subscription or one-time)
alt one-time purchase
RefundRoute->>Stripe: retrieve payment_intent -> create refund
Stripe-->>RefundRoute: refund success
RefundRoute->>Prisma: mark purchase refunded (refundedAt) and record adjustments
else subscription
RefundRoute->>DB: locate subscription invoice & payment intent
RefundRoute->>Stripe: create refund
Stripe-->>RefundRoute: refund success
RefundRoute->>Prisma: mark subscription refunded/canceled and record adjustments
end
RefundRoute-->>Admin: { success: true } or KnownError
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 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)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
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.
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)
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts
Show resolved
Hide resolved
<!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md -->
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: 2
♻️ Duplicate comments (7)
packages/stack-shared/src/interface/crud/transactions.ts (1)
26-28: USD-only net_amount constraint already flagged.The restriction of
net_amountto USD whilecharged_amountsupports multiple currencies has already been identified in a previous review as causing data integrity issues.apps/dashboard/src/components/data-table/transaction-table.tsx (2)
236-252: Fix type column/filter to usetransaction.typeinstead of derivedsourceType.The “Type” column currently has
id: 'source_type'and its accessor returns the derivedSourceType('subscription' | 'one_time' | 'item_quantity_change' | 'other'), while:
- The toolbar dropdown is populated from
TRANSACTION_TYPES('purchase','subscription-renewal', etc.), andonUpdateforwardsoptions.columnFilters.find(f => f.id === 'source_type')?.valueas the backendtypefilter, which expects aTransactionType.These value sets don’t overlap, so when a user picks a type filter:
- The backend gets a correct
TransactionType(e.g."purchase"), but- TanStack’s client‑side filter for the
source_typecolumn compares"purchase"against values like"subscription"/"one_time", which never match, causing the table to show no rows.To align everything and let the filter work end‑to‑end:
- Change the column id from
'source_type'to'type'.- Set its accessor to
transaction.typefor filtering/sorting.- Keep using
summary.displayTypeonly for icon/label rendering.- Update
onUpdate,defaultVisibility,defaultColumnFilters, and toolbar to reference'type'instead of'source_type'.Roughly:
- { - 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 displayType = summary?.displayType; if (!displayType) { return <TextCell size={20}>—</TextCell>; } const { Icon, label } = displayType; return ( <TextCell size={20}> <Tooltip> <TooltipTrigger asChild> <span className="flex h-6 w-6 items-center justify-center rounded-md bg-muted"> <Icon className="h-4 w-4" aria-hidden /> </span> </TooltipTrigger> <TooltipContent side="left">{label}</TooltipContent> </Tooltip> </TextCell> ); }, },- const newFilters: { cursor?: string, limit?: number, type?: TransactionType, customerType?: ... } = { - cursor: options.cursor, - limit: options.limit, - type: options.columnFilters.find(f => f.id === 'source_type')?.value as any, - customerType: options.columnFilters.find(f => f.id === 'customer')?.value as any, - }; + const newFilters: { cursor?: string, limit?: number, type?: TransactionType, customerType?: ... } = { + cursor: options.cursor, + limit: options.limit, + type: options.columnFilters.find(f => f.id === 'type')?.value as any, + customerType: options.columnFilters.find(f => f.id === 'customer')?.value as any, + };and similarly update
defaultVisibility,defaultColumnFilters, and the toolbar to calltable.getColumn('type').Also applies to: 252-353, 355-375, 378-441
142-150: MakepickChargedAmountDisplaymulti‑currency aware and robust to undefined USD amounts.
pickChargedAmountDisplaystill has two issues:
- It only checks for the presence of the
"USD"key and will render"$undefined"if that key exists but its value is missing/undefined.- For all non‑USD charges, it returns the literal string
"Non USD amount"instead of the actual amount and currency, so tenants priced solely in EUR/GBP/JPY/etc. can’t see real values.Given that
money_transferentries already carry a multi‑currencycharged_amountmap, this should pick any defined amount and label it, falling back to'—'only when no valid values exist.For example:
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 '—'; + } + if (currency === 'USD') { + return `$${amount}`; + } + return `${currency} ${amount}`; }This avoids
$undefined, correctly surfaces non‑USD values, and will stay compatible as you add more currencies.Also applies to: 172-189, 303-310
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (2)
42-56: Fix decimal handling inmultiplyMoneyAmount(currently overstates amounts for extra fractional digits).The
multiplyMoneyAmounthelper currently pads the fractional part twice but never truncates it tocurrency.decimals:const [wholePart, fractionalPart = ""] = normalized.split("."); const paddedFractional = fractionalPart.padEnd(currency.decimals, "0"); const smallestUnit = BigInt(`${wholePart || "0"}${paddedFractional.padEnd(currency.decimals, "0")}`);For a 2‑decimal currency and an input like
"1.234", this builds"1234"as the smallest unit, which later converts back to"12.34"instead of something like"1.23"/"1.24". That’s a 10× overstatement of the amount.You probably want to clamp the fractional part to
currency.decimalsdigits, then right‑pad with zeros if it’s too short, e.g.:- const [wholePart, fractionalPart = ""] = normalized.split("."); - const paddedFractional = fractionalPart.padEnd(currency.decimals, "0"); - const smallestUnit = BigInt(`${wholePart || "0"}${paddedFractional.padEnd(currency.decimals, "0")}`); + const [wholePartRaw, fractionalRaw = ""] = normalized.split("."); + const wholePart = wholePartRaw || "0"; + const fractionalNormalized = + currency.decimals === 0 + ? "" + : fractionalRaw.slice(0, currency.decimals).padEnd(currency.decimals, "0"); + const smallestUnit = BigInt(`${wholePart}${fractionalNormalized}`);The rest of the function (multiplying by an integer quantity and re‑inserting the decimal point) can stay as is; this change ensures any extra fractional digits are not misinterpreted as extra whole digits.
Also applies to: 58-75, 77-85
88-99: Makenet_amounttruthful for non‑USD charges instead of forcing{ USD: "0" }.Both
createMoneyTransferEntryandbuildSubscriptionRenewalTransactioncurrently constructnet_amountas USD only, defaulting to"0"or potentiallyundefinedwhen there is no USD component:const netUsd = options.chargedAmount.USD ?? "0"; ... net_amount: { USD: netUsd },and
net_amount: { USD: chargedAmount.USD },This causes:
- Purely non‑USD charges (e.g.
{ EUR: "5000" }) to be recorded as havingnet_amount: { USD: "0" }, effectively zeroing out revenue for those transactions.- Potentially
USD: undefinedbeing serialized for renewals when only non‑USD currencies are present.Since
charged_amountis already a per‑currency map,net_amountshould mirror real financial impact rather than fabricating a zero USD value. A simple fix is:
- Filter
charged_amountdown to actual defined string values.- Use USD when present; otherwise use the original charged currencies as
net_amount.For example in
createMoneyTransferEntry:- const chargedCurrencies = Object.keys(options.chargedAmount); - if (chargedCurrencies.length === 0) return null; - const netUsd = options.chargedAmount.USD ?? "0"; + const chargedEntries = Object.entries(options.chargedAmount) + .filter(([, amount]) => typeof amount === "string") as Array<[string, string]>; + if (chargedEntries.length === 0) return null; + const chargedAmount = Object.fromEntries(chargedEntries) as Record<string, string>; + const netAmount = chargedAmount.USD !== undefined ? { USD: chargedAmount.USD } : chargedAmount; return { type: "money_transfer", adjusted_transaction_id: null, adjusted_entry_index: null, customer_type: options.customerType, customer_id: options.customerId, - charged_amount: options.chargedAmount, - net_amount: { USD: netUsd }, + charged_amount: chargedAmount, + net_amount: netAmount, };and in
buildSubscriptionRenewalTransaction:- net_amount: { USD: chargedAmount.USD }, + net_amount: chargedAmount.USD !== undefined + ? { USD: chargedAmount.USD } + : { ...chargedAmount },This keeps
net_amountconsistent and non‑misleading for tenants billing in non‑USD currencies.Also applies to: 101-122, 292-322
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx (1)
94-101: Advance per‑source cursors based onmerged(not justpage) to keep pagination correct under filtering and avoid repeated scansRight now
lastSubId/lastIqcId/lastOtpId/lastSiIdare derived only frompage(the filtered slice). If a source’s rows are filtered out byquery.type, its cursor never advances, so that table is repeatedly scanned from the beginning on subsequent pages. Today each table maps to a single transactiontype, so results stay correct but you still over‑fetch and create a fragile coupling between “one table = one type”. If any table ever emits multiple transaction types, this will starve later matching rows and break paginatedtypeviews.You can fix both correctness (future‑proof) and efficiency by advancing cursors based on all
mergedrecords up to the last item actually returned in the page, as in the earlier bot suggestion:- const page = filtered.slice(0, limit); - let lastSubId = ""; - let lastIqcId = ""; - let lastOtpId = ""; - let lastSiId = ""; - for (const r of page) { - if (r.source === "subscription") lastSubId = r.id; - if (r.source === "item_quantity_change") lastIqcId = r.id; - if (r.source === "one_time") lastOtpId = r.id; - if (r.source === "subscription-invoice") lastSiId = r.id; - } + const page = filtered.slice(0, limit); + let lastSubId = ""; + let lastIqcId = ""; + let lastOtpId = ""; + let lastSiId = ""; + + if (page.length === limit) { + // Find the position of the last item in the page within the merged results + const lastPageItem = page[page.length - 1]; + const lastPageIndex = merged.findIndex((item) => + item.source === lastPageItem.source && + item.id === lastPageItem.id + ); + + // Advance cursors for all sources based on records processed up to this point + for (let i = 0; i <= lastPageIndex; i++) { + const r = merged[i]; + if (r.source === "subscription") lastSubId = r.id; + if (r.source === "item_quantity_change") lastIqcId = r.id; + if (r.source === "one_time") lastOtpId = r.id; + if (r.source === "subscription-invoice") lastSiId = r.id; + } + }This keeps concatenated‑cursor pagination consistent even when a filter drops some merged rows, and avoids endlessly re‑reading already‑processed records from non‑selected sources.
Also applies to: 112-201
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)
607-616: Restore safe default forparamsinlistTransactions/useTransactionsBoth methods now require a
paramsobject, soadminApp.listTransactions()oradminApp.useTransactions()with no args will throw at runtime when accessingparams.*. The previous API allowed no‑arg calls; this is a breaking ergonomics change and was already flagged in a prior review.You can fix this by defaulting
params = {}and destructuring before using the cache:- 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; } @@ - 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; }
🧹 Nitpick comments (5)
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts (1)
6-79: E2E coverage for refund flows is solid; consider a couple of small robustness tweaks.The helpers and three scenarios here (test‑mode one‑time purchase, missing subscription ID, and non‑test one‑time purchase via Stripe webhook including double‑refund protection and post‑refund product state) give very good end‑to‑end coverage of the new refund route and transaction model.
A couple of small, optional hardening ideas:
- In the “refunds non‑test mode one-time purchases” test, you already snapshot
transactionsRes.body, but also assertingtransactionsRes.status === 200(and similarly fortransactionsAfterRefund) would make failures easier to diagnose if the endpoint ever starts returning a KnownError instead of a 2xx.createTestModeTransactioncurrently picks the first transaction from/transactions; if you ever add more background transactions in this project, you might want to filter bytype === "purchase"to ensure you grab the correct one.Overall though, this file looks good.
Also applies to: 80-109, 112-135, 137-290
apps/backend/prisma/schema.prisma (1)
772-799: Schema changes line up with code; consider enforcing the “one creation invoice per subscription” invariant in the DB.The added
refundedAtfields and the newSubscriptionInvoicemodel align with how the refund route andhandleStripeInvoicePaiduse them (linking invoices to subscriptions via(tenancyId, stripeSubscriptionId)and upserting by(tenancyId, stripeInvoiceId)).Right now, the “there must be at most one creation invoice per subscription” rule is enforced only in application code (
findMany+if (length > 1) throw StackAssertionError). If you want stronger protection against data drift or concurrent writers, you could add a unique index such as:model SubscriptionInvoice { // existing fields... @@id([tenancyId, id]) @@unique([tenancyId, stripeInvoiceId]) @@unique([tenancyId, stripeSubscriptionId, isSubscriptionCreationInvoice]) }This would ensure duplicates for
isSubscriptionCreationInvoice = truecan’t be inserted even outside the main code path.Also applies to: 815-831, 859-873
apps/backend/src/lib/stripe.tsx (1)
50-60: Invoice line parsing is valid per Stripe v18.3.0; Stripe client scope works but relies on caller invariant.The concerns raised have nuanced validity:
Invoice line subscription extraction: Stripe SDK v18.3.0 does expose
parent.subscription_item_details.subscriptionon invoice line items, so the parsing at lines 125–127 is correct. No issue here.Stripe client scope: The current implementation works correctly because the webhook handler creates an account-scoped client with
getStripeForAccount({ accountId }, mockData)and then passes the sameaccountIdto bothsyncStripeSubscriptionsandhandleStripeInvoicePaid. InsidegetTenancyFromStripeAccountIdOrThrow, callingstripe.accounts.retrieve(stripeAccountId)succeeds because the account-scoped client'sstripeAccountconfig matches thestripeAccountIdparameter.However, this pattern is fragile: it silently depends on callers always passing a client and
stripeAccountIdthat match. If a refactoring mixes a platform client with a specificstripeAccountId(or vice versa), the calls would fail subtly. To harden this:
- Document the expected invariant: all three functions assume the
stripeclient's account scope (if any) matches the providedstripeAccountId.- Consider adding a runtime check in
getTenancyFromStripeAccountIdOrThrowor an assertion that the client and accountId are compatible.- Add integration tests using the mock that verify a real webhook event produces the expected
invoiceSubscriptionIdsarray (currently no tests found).apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx (1)
101-103: Verify Prisma relation filter forsubscriptionInvoice+customer_type
customerTypeFilteris{ customerType: ... }, which is passed directly assubscription: customerTypeFilterin thesubscriptionInvoice.findManywhereclause. For typical Prisma schemas, a relation field uses aRelationFilter(e.g.{ is: { customerType: ... } }for 1‑1), sosubscription: customerTypeFiltermay either be a type error or throw a runtime validation error.Please double‑check the generated
SubscriptionInvoiceWhereInput:
- If
subscriptionis a relation filter, this likely needs to be:where: { tenancyId: auth.tenancy.id, ...(siWhere ?? {}), ...(query.customer_type ? { subscription: { is: customerTypeFilter } } : {}), isSubscriptionCreationInvoice: false, },
- If your Prisma model truly exposes
subscriptionas an embedded where‑input, then this is fine but is non‑standard and worth a comment.Also applies to: 133-139
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts (1)
75-90: Avoid duplicatingsendStripeWebhooklogic already available in backend helpersThis local
sendStripeWebhookmirrors the implementation inbackend-helpers.ts, differing mainly in how the secret is sourced. To reduce duplication and keep Stripe-signature logic single-sourced, consider delegating to the shared helper instead:// backend-helpers.ts already exposes: export async function sendStripeWebhook(payload: unknown, options?: { secret?: string; ... }) // here: import { ..., sendStripeWebhook as sendStripeWebhookHelper } from "../../../../backend-helpers"; const stripeWebhookSecret = process.env.STACK_STRIPE_WEBHOOK_SECRET ?? "mock_stripe_webhook_secret"; async function sendStripeWebhook(payload: unknown) { return await sendStripeWebhookHelper(payload, { secret: stripeWebhookSecret }); }This keeps tests aligned if the signing logic changes in one place.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
apps/backend/prisma/migrations/20251107182739_subscription_invoice/migration.sql(1 hunks)apps/backend/prisma/migrations/20251107210602_one_time_payment_refunds/migration.sql(1 hunks)apps/backend/prisma/migrations/20251112215249_subscription_refunds/migration.sql(1 hunks)apps/backend/prisma/schema.prisma(3 hunks)apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx(2 hunks)apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx(1 hunks)apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx(5 hunks)apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts(1 hunks)apps/backend/src/lib/payments.tsx(1 hunks)apps/backend/src/lib/stripe.tsx(2 hunks)apps/dashboard/src/components/data-table/transaction-table.tsx(2 hunks)apps/e2e/tests/backend/backend-helpers.ts(2 hunks)apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts(1 hunks)apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts(6 hunks)apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts(8 hunks)docker/dependencies/docker.compose.yaml(1 hunks)packages/stack-shared/src/interface/admin-interface.ts(3 hunks)packages/stack-shared/src/interface/crud/transactions.ts(1 hunks)packages/stack-shared/src/known-errors.tsx(5 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(4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.
Applied to files:
apps/e2e/tests/backend/backend-helpers.ts
🧬 Code graph analysis (13)
apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx (6)
apps/backend/src/route-handlers/smart-route-handler.tsx (1)
createSmartRouteHandler(209-294)packages/stack-shared/src/schema-fields.ts (6)
yupObject(247-251)adminAuthTypeSchema(483-483)adaptSchema(330-330)yupString(187-190)yupNumber(191-194)yupBoolean(195-198)apps/backend/src/prisma-client.tsx (1)
getPrismaClientForTenancy(68-70)packages/stack-shared/src/known-errors.tsx (2)
KnownErrors(1632-1634)KnownErrors(1636-1762)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError(69-85)apps/backend/src/lib/stripe.tsx (1)
getStripeForAccount(26-48)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (5)
packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionEntry(97-97)Transaction(125-125)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)packages/stack-shared/src/schema-fields.ts (1)
productSchema(569-592)apps/backend/src/lib/payments.tsx (1)
productToInlineProduct(427-443)
apps/backend/src/lib/stripe.tsx (3)
packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError(69-85)apps/backend/src/lib/tenancies.tsx (1)
getTenancy(82-91)apps/backend/src/prisma-client.tsx (1)
getPrismaClientForTenancy(68-70)
packages/stack-shared/src/interface/crud/transactions.ts (4)
packages/stack-shared/src/utils/currency-constants.tsx (1)
SUPPORTED_CURRENCIES(9-45)packages/stack-shared/src/utils/errors.tsx (1)
throwErr(10-19)packages/stack-shared/src/schema-fields.ts (9)
yupObject(247-251)moneyAmountSchema(427-437)yupString(187-190)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)
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts (2)
apps/e2e/tests/backend/backend-helpers.ts (1)
niceBackendFetch(109-173)apps/e2e/tests/helpers.ts (1)
it(12-12)
apps/dashboard/src/components/data-table/transaction-table.tsx (5)
packages/stack-shared/src/interface/crud/transactions.ts (4)
TransactionEntry(97-97)Transaction(125-125)TransactionType(108-108)TRANSACTION_TYPES(99-106)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 (4)
AvatarCell(45-52)ActionCell(72-123)TextCell(7-43)DateCell(54-62)packages/stack-ui/src/components/action-dialog.tsx (1)
ActionDialog(31-135)packages/stack-ui/src/components/data-table/data-table.tsx (1)
DataTableManualPagination(174-238)
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx (3)
packages/stack-shared/src/interface/crud/transactions.ts (3)
TRANSACTION_TYPES(99-106)transactionSchema(110-123)Transaction(125-125)packages/stack-shared/src/utils/strings.tsx (1)
typedToUppercase(30-33)apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (4)
buildSubscriptionTransaction(161-210)buildItemQuantityChangeTransaction(263-290)buildOneTimePurchaseTransaction(212-261)buildSubscriptionRenewalTransaction(292-323)
packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts (1)
packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionType(108-108)Transaction(125-125)
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts (1)
apps/e2e/tests/backend/backend-helpers.ts (2)
niceBackendFetch(109-173)sendStripeWebhook(1499-1526)
packages/stack-shared/src/known-errors.tsx (1)
packages/stack-shared/src/index.ts (1)
KnownError(14-14)
packages/stack-shared/src/interface/admin-interface.ts (1)
packages/stack-shared/src/interface/crud/transactions.ts (2)
TransactionType(108-108)Transaction(125-125)
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (1)
apps/backend/src/lib/stripe.tsx (1)
handleStripeInvoicePaid(124-161)
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(108-108)Transaction(125-125)
⏰ 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: build (22.x)
- GitHub Check: Vercel Agent Review
- GitHub Check: all-good
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: docker
- GitHub Check: restart-dev-and-test
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: setup-tests
🔇 Additional comments (22)
docker/dependencies/docker.compose.yaml (1)
76-76: Scoped change with no external configuration impact.The
PG_META_DB_PORTchange is isolated to the supabase-meta service's internal Docker network configuration. This environment variable is only used by the supabase-meta service to connect to thedbservice, and no other services or external configuration files reference it.The port 8128 references found in
docker/server/.env.exampleandapps/backend/.envare unrelated—they refer to external host connections (host.docker.internal:8128 and localhost:8128) rather than the internal Docker network port configuration. These do not need updating.packages/stack-shared/src/interface/crud/transactions.ts (2)
62-73: LGTM! Effective mutual exclusion validation.The custom test ensures
subscription_idandone_time_purchase_idare mutually exclusive, preventing ambiguous product grant references.
89-125: Well-structured transaction schema.The entry-based Transaction model with typed unions and strict validation provides a solid foundation for the unified transaction system. The use of
InferTypeensures type safety between runtime validation and TypeScript types.packages/stack-shared/src/interface/admin-interface.ts (2)
598-611: LGTM! Method signature aligned with new Transaction model.The
listTransactionsmethod correctly supports filtering bytypeandcustomerType, and returns the newTransaction[]type.
613-626: LGTM! Clean refund method implementation.The new
refundTransactionmethod follows the established patterns in the admin interface and provides a clear API for refund operations.apps/backend/src/lib/payments.tsx (1)
678-678: LGTM! Correct filtering of refunded purchases.Excluding refunded purchases via
refundedAt: nullensures that only active purchases are counted as owned products, which aligns with the expected business logic.apps/backend/prisma/migrations/20251107210602_one_time_payment_refunds/migration.sql (1)
1-2: LGTM! Clean migration for refund tracking.The nullable
refundedAtcolumn provides backward compatibility while enabling refund state tracking for one-time purchases.apps/backend/prisma/migrations/20251112215249_subscription_refunds/migration.sql (1)
1-2: LGTM! Consistent refund tracking for subscriptions.This migration mirrors the one-time purchase refund tracking and maintains consistency across purchase types.
apps/e2e/tests/backend/backend-helpers.ts (1)
1499-1527: LGTM! Robust Stripe webhook test helper.The
sendStripeWebhookfunction correctly implements HMAC-SHA256 signature generation for Stripe webhooks and provides flexible test options for various scenarios.apps/backend/prisma/migrations/20251107182739_subscription_invoice/migration.sql (1)
1-19: LGTM! Well-designed multi-tenant invoice tracking.The
SubscriptionInvoicetable properly enforces data integrity with composite keys, unique constraints scoped to tenancy, and appropriate foreign key constraints.apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts (1)
12-15: Centralizing webhook calls viaPayments.sendStripeWebhooklooks good.Routing all Stripe webhook test flows through the shared
Payments.sendStripeWebhookhelper (with flags for invalid/missing signatures and retries) keeps the tests aligned with the rest of the e2e suite while preserving the original assertions on status, body, and idempotent behavior. I don’t see any functional regressions here.Also applies to: 24-31, 41-43, 130-137, 134-137, 238-241, 351-353, 391-393
apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx (1)
32-85: Stripe API usage is correct; error semantics are intentional per API contract.The review's two main concerns have been verified:
Error semantics (lines 39, 55): The code throws
SubscriptionInvoiceNotFoundwhen the subscription itself is missing, which appears semantically confusing. However, the e2e test atapps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts:112explicitly validates this as the intended API contract—it expectsSUBSCRIPTION_INVOICE_NOT_FOUNDwhen a non-existent subscription ID is passed. This is locked in by the existing API specification, not a bug.Stripe invoice API (line 62–71): The code correctly uses
invoice.payments?.data[]and accessespayment.payment_intent. Stripe's current API (Basil API) exposes an expandable payments array of Invoice Payment objects, with each element containing a payment object that links to the PaymentIntent. The code is not relying on a mock-only or deprecated shape—this is the standard, current Stripe API. The review's suggestion to "preferinvoice.payment_intent" would be a regression, as that pattern was deprecated in favor of the payments array.The concerns raised do not reflect actual runtime risks or code defects.
Likely an incorrect or invalid review comment.
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (2)
77-79: _transactionsCache wiring looks consistentThe transactions cache shape and dependency tuple (
[cursor, limit, type, customerType]) line up with the backend route and shared Transaction/TransactionType types.
602-605: KeeprefundTransactioncache invalidation; looks goodForwarding
type/idto the interface and invalidating_transactionsCacheensures subsequent list/use calls see updated transaction state.packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts (1)
2-3: Transaction / TransactionType wiring in admin app interface looks consistentThe
transactionsasync store now correctly exposes{ transactions: Transaction[], nextCursor }with a typedTransactionTypefilter, and the newrefundTransactionmethod and constructor overload line up with the implementation.Also applies to: 23-43, 92-93, 96-102
packages/stack-shared/src/known-errors.tsx (1)
18-23: New refund-related KnownErrors are correctly defined and exportedThe added errors for missing subscription invoices / one-time purchases, already-refunded purchases, and non-refundable test-mode purchases have consistent status codes, details, and
constructorArgsFromJsonimplementations, and are wired into theKnownErrorsmap soKnownError.fromJsoncan resolve them.Also applies to: 1495-1555, 1636-1757
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts (6)
1-97: Helper setup for payments config and purchase codes looks solid
setupProjectWithPaymentsConfigandcreatePurchaseCodeForCustomerencapsulate the boilerplate around payments configuration and purchase URL creation nicely, and thecreatePurchaseCodewrapper keeps existing user flows simple.
118-316: Core transaction snapshots and pagination test cover new Transaction shape wellThe updated snapshots for test-mode subscriptions, one-time purchases, and item quantity changes correctly assert against the new
entries/typestructure, and the concatenated-cursor pagination test still verifies multi-source pagination without depending on internal source details.
318-420: Stripe subscription creation vs renewal behavior is well coveredThe
"omits subscription-renewal entries for subscription creation invoices"test builds realistic mock Stripe events and verifies that only renewal invoices producesubscription-renewalmoney_transfer entries, while the initial purchase remains typed aspurchase. This is a good regression guard for the new subscriptionInvoice source.
422-455: Type-based filtering test validates backendquery.typebehaviorThe
"filters results by transaction type"test asserts thattype=manual-item-quantity-changeandtype=purchaseeach return only their respective transaction types. This is a valuable check on the backend’s type-filtering logic.
457-520: customer_type filtering across user/team sources is exercised appropriatelyThe
"filters results by customer_type across sources"test covers team products, team items, and ensures allentries[].customer_typematch the requestedcustomer_type. That gives good confidence that the backendcustomer_typefilter is wired correctly for subscriptions and item quantity changes.
522-598: Server-granted subscription test matches the new Transaction shape
"returns server-granted subscriptions in transactions"ensures that server-created subscriptions appear astype: "purchase"with aproduct_grantentry and correct inline product metadata. This closes an important gap for non-checkout subscription flows.
https://www.loom.com/share/db645a1799454ec6b0234c55ee28cee9
Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Tests
Chores
Note
Replaces AdminTransaction with a unified, entry-based Transaction model, updates the backend route/builders, admin SDK, dashboard table, and tests (incl. filtering and serializer tweaks).
transaction-builder.tsto buildTransactionobjects (product grants, money transfers, item quantity changes) with multi-currency amounts andeffective_at_millis/adjusted_by./internal/payments/transactionsnow returnstransactionSchema, supports filtering byTRANSACTION_TYPES, merges sources, and preserves concatenated-cursor pagination.transactionEntrySchema,transactionSchema,TRANSACTION_TYPES, andTransactionType; removeAdminTransactionsurface.listTransactionssignatures, caches, and types to useTransaction/TransactionTypeacross interfaces and app implementations.Transactionshape; add tests for type/customer filters and server-granted subscriptions; serializer now stripseffective_at_millis.Written by Cursor Bugbot for commit c914d11. This will update automatically on new commits. Configure here.