Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@BilalG1
Copy link
Contributor

@BilalG1 BilalG1 commented Oct 30, 2025

https://www.loom.com/share/db645a1799454ec6b0234c55ee28cee9

Summary by CodeRabbit

  • New Features

    • Multi-currency, entry-based transaction model; unified transaction builders.
    • Refund API for subscriptions and one-time purchases; Stripe invoice handling and webhook invoice-paid processing.
  • Refactor

    • Admin UI and SDK migrated to the new transaction shape; table rendering simplified with summary-driven columns and avatar cells.
  • Bug Fixes

    • Improved aggregation, sorting and cursor pagination across mixed transaction sources; refund state handling.
  • Tests

    • Expanded end-to-end tests, webhook simulation helpers, and snapshot updates.
  • Chores

    • Database migrations for subscription invoices and refundedAt fields.

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

  • Backend:
    • Entry-based transactions: Add transaction-builder.ts to build Transaction objects (product grants, money transfers, item quantity changes) with multi-currency amounts and effective_at_millis/adjusted_by.
    • API: /internal/payments/transactions now returns transactionSchema, supports filtering by TRANSACTION_TYPES, merges sources, and preserves concatenated-cursor pagination.
  • Shared (stack-shared):
    • Add transactionEntrySchema, transactionSchema, TRANSACTION_TYPES, and TransactionType; remove AdminTransaction surface.
  • Admin SDK/Template:
    • Update listTransactions signatures, caches, and types to use Transaction/TransactionType across interfaces and app implementations.
  • Dashboard UI:
    • Refactor transaction table to new model: iconified type, avatar customer cells, amount/details columns, and filters for type and customer.
  • Tests:
    • Update e2e snapshots to new Transaction shape; add tests for type/customer filters and server-granted subscriptions; serializer now strips effective_at_millis.

Written by Cursor Bugbot for commit c914d11. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Oct 30, 2025

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

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Nov 18, 2025 8:01pm
stack-dashboard Ready Ready Preview Comment Nov 18, 2025 8:01pm
stack-demo Ready Ready Preview Comment Nov 18, 2025 8:01pm
stack-docs Ready Ready Preview Comment Nov 18, 2025 8:01pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 30, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Replace 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

Cohort / File(s) Summary
Core Transaction Schema & Shared Types
packages/stack-shared/src/interface/crud/transactions.ts
Remove AdminTransaction; add transactionSchema, Transaction, TransactionEntry union, TRANSACTION_TYPES/TransactionType, multi-currency charged_amount/net_amount, and entry schemas (money_transfer, product_grant, item_quantity_change, revocation, reversal).
Transaction Builders
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts
New module exporting buildSubscriptionTransaction, buildOneTimePurchaseTransaction, buildItemQuantityChangeTransaction, buildSubscriptionRenewalTransaction, ProductWithPrices, plus helpers for price resolution, currency-aware money math, and entry assembly.
Backend Transactions Route
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx
Replace AdminTransaction-based retrieval with merged TransactionRow[]; aggregate multiple sources (subscriptions, itemQuantityChanges, oneTimePurchases, subscriptionInvoices), build typed Transaction objects via builders, validate with transactionSchema, implement type-based filtering and cursor pagination using last-seen ids across sources.
Refund API Route & Logic
apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx
New POST refund endpoint: validates target (subscription or one-time purchase), interacts with Stripe to create refunds, updates Prisma models (refundedAt, subscription cancellation), and returns structured KnownErrors on failure.
Stripe Integration & Invoice Handling
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx, apps/backend/src/lib/stripe.tsx
Add handleStripeInvoicePaid to persist SubscriptionInvoice, new helper to resolve tenancy from stripe account, call invoice handler from webhook on invoice.payment_succeeded, and refactor tenancy resolution in subscription sync.
Prisma Schema & Migrations
apps/backend/prisma/schema.prisma, apps/backend/prisma/migrations/...subscription_invoice.../migration.sql, ...one_time_payment_refunds.../migration.sql, ...subscription_refunds.../migration.sql
Add SubscriptionInvoice model and relations; add refundedAt fields to OneTimePurchase and Subscription; create corresponding SQL migrations and indexes.
SDK / Admin Interface & Template App
packages/stack-shared/src/interface/admin-interface.ts, packages/template/src/lib/stack-app/.../admin-app-impl.ts, packages/template/src/lib/stack-app/.../admin-app.ts
Replace AdminTransaction with Transaction/TransactionType in public APIs, update listTransactions/useTransactions signatures and caches; add refundTransaction client method.
Dashboard Transaction Table
apps/dashboard/src/components/data-table/transaction-table.tsx
Refactor rendering to derive TransactionSummary from entry-level entries; add type guards, source/type formatting, avatar cell components, new filter keys mapped to TRANSACTION_TYPES, and summary-driven columns.
End-to-End Tests & Helpers
apps/e2e/tests/backend/endpoints/api/v1/internal/transactions.test.ts, apps/e2e/tests/backend/endpoints/api/v1/transactions-refund.test.ts, apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts, apps/e2e/tests/backend/backend-helpers.ts, apps/e2e/tests/snapshot-serializer.ts
Update tests to expect entries, effective_at_millis, and adjusted_by behavior; add Stripe webhook simulation helper Payments.sendStripeWebhook, new payment test utilities and refunds E2E tests; strip effective_at_millis in snapshots.
Known Errors & Public Errors Surface
packages/stack-shared/src/known-errors.tsx
Add known error constructors: SUBSCRIPTION_INVOICE_NOT_FOUND, ONE_TIME_PURCHASE_NOT_FOUND, SUBSCRIPTION_ALREADY_REFUNDED, ONE_TIME_PURCHASE_ALREADY_REFUNDED, TEST_MODE_PURCHASE_NON_REFUNDABLE and wire them into exports.
Misc, Config & Dev Tools
apps/backend/src/lib/payments.tsx, docker/dependencies/docker.compose.yaml
Exclude refunded one-time purchases from owned products query; change supabase-meta PG_META_DB_PORT from 8128 to 5432.

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

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Files warranting extra attention:
    • packages/stack-shared/src/interface/crud/transactions.ts — union schemas, currency invariants, and exported type changes.
    • apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts — money math, multi-currency aggregation, test-mode logic, and edge-case handling.
    • apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx — merging heterogeneous sources, sort/tie resolution, and cursor construction.
    • apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx — Stripe interactions, idempotency, error mapping to KnownErrors, and DB updates.
    • Prisma migrations and apps/backend/prisma/schema.prisma — ensure migrations preserve data integrity and FK constraints.

Possibly related PRs

Poem

🐇 I hop through ledgers, nibble entries neat,

Builders stitch carrots, amounts tiny and sweet.
Cursors scurry onward, invoices tucked in rows,
Refunds find their burrow, where tidy data grows.
I thump a happy test — the transactions all compose.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive Title 'Payment transactions' is vague and generic; it does not convey the specific nature of the change (replacing AdminTransaction model with entry-based Transaction model). Provide a more descriptive title that captures the main change, e.g., 'Replace AdminTransaction with entry-based Transaction model' or 'Refactor payment transactions to unified schema'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive with a detailed Cursor-generated summary explaining the changes across backend, shared, SDK, UI, and tests. However, the primary description content is just a Loom video link without direct explanation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch payment-transactions

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19dfcff and 31a924f.

📒 Files selected for processing (1)
  • apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
⏰ 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: restart-dev-and-test
  • GitHub Check: setup-tests
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: all-good
  • GitHub Check: Vercel Agent Review
  • 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: build (22.x)
  • GitHub Check: docker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile 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 TransactionSource types and external Transaction types
  • 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:

  1. Route handler manages pagination and API concerns
  2. Builder functions handle transaction object construction
  3. Money arithmetic is isolated and well-tested
  4. 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
Loading

9 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aea7ad0 and db16ac2.

📒 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.ts
  • apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts
  • apps/backend/src/lib/email-rendering.tsx
  • apps/dashboard/src/components/data-table/transaction-table.tsx
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/stack-shared/src/interface/admin-interface.ts
  • apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx
  • packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
  • packages/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.ts
  • packages/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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 a productGrant exists 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 any cast suppresses type checking. Since transactionType is typed as TransactionType | null, the default case should only handle null. 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

📥 Commits

Reviewing files that changed from the base of the PR and between db16ac2 and 5b1a8c5.

📒 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)').

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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. pickChargedAmountDisplay only 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 SourceType values but the toolbar populates the filter dropdown with TransactionType values from TRANSACTION_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 in net_amount.

🧹 Nitpick comments (1)
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts (1)

263-291: Unused parameter in buildItemQuantityChangeTransaction.

The tenancy parameter 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5b1a8c5 and 47141ce.

📒 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ 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. The net_amount should 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 ProductSnapshot cast on line 41 bypasses type checking. While the structure appears complete, if ProductSnapshot evolves, 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.

buildSubscriptionTransaction and buildOneTimePurchaseTransaction share nearly identical logic (~45 lines of duplication), differing only in which ID field they pass to createProductGrantEntry. 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

📥 Commits

Reviewing files that changed from the base of the PR and between b30fa8c and 42e3d83.

📒 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 any cast 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_change entry.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 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 Transaction and TransactionType. The implementation properly handles optional parameters and builds the query string as needed.

Consider extracting customerType to a type definition similar to TransactionType for 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

📥 Commits

Reviewing files that changed from the base of the PR and between bd9d850 and 6fce497.

📒 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 AdminTransaction to Transaction and TransactionType, 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 Transaction and TransactionType from 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 | undefined and maintains parameter order alignment with the listTransactions method signature, ensuring correct cache behavior for different query combinations.


594-604: LGTM! Transaction methods consistently updated.

Both listTransactions and useTransactions are correctly updated to use the new Transaction and TransactionType types. Cache access patterns properly align with the cache definition, and return types are consistent across both the async method and React hook.

@BilalG1 BilalG1 requested a review from N2D4 November 7, 2025 17:54
@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Nov 7, 2025
Copy link
Contributor

@N2D4 N2D4 left a 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?

@github-actions github-actions bot assigned BilalG1 and unassigned N2D4 Nov 11, 2025
Copy link

@cursor cursor bot left a 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
apps/dashboard/src/components/data-table/transaction-table.tsx (2)

192-356: Restore functional type filtering
The source_type column still returns SourceType values while the toolbar hands it TransactionType values and the API filter expects TransactionType. 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 on transaction.type so 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
pickChargedAmountDisplay still 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> for extraProducts and extraItems loses 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 baseProducts and baseItems are 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

📥 Commits

Reviewing files that changed from the base of the PR and between 420a508 and 740b941.

📒 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_id instead of subscription_id while maintaining the same entry-based structure.


229-251: LGTM!

The item quantity change snapshot correctly uses the item_quantity_change entry type with appropriate fields and the manual-item-quantity-change transaction 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: null since 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_by field 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).

@BilalG1 BilalG1 requested a review from N2D4 November 11, 2025 22:26
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 740b941 and 4693a82.

📒 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)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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_amount to USD while charged_amount supports 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 use transaction.type instead of derived sourceType.

The “Type” column currently has id: 'source_type' and its accessor returns the derived SourceType ('subscription' | 'one_time' | 'item_quantity_change' | 'other'), while:

  • The toolbar dropdown is populated from TRANSACTION_TYPES ('purchase', 'subscription-renewal', etc.), and
  • onUpdate forwards options.columnFilters.find(f => f.id === 'source_type')?.value as the backend type filter, which expects a TransactionType.

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_type column 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.type for filtering/sorting.
  • Keep using summary.displayType only 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 call table.getColumn('type').

Also applies to: 252-353, 355-375, 378-441


142-150: Make pickChargedAmountDisplay multi‑currency aware and robust to undefined USD amounts.

pickChargedAmountDisplay still 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_transfer entries already carry a multi‑currency charged_amount map, 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 in multiplyMoneyAmount (currently overstates amounts for extra fractional digits).

The multiplyMoneyAmount helper currently pads the fractional part twice but never truncates it to currency.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.decimals digits, 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: Make net_amount truthful for non‑USD charges instead of forcing { USD: "0" }.

Both createMoneyTransferEntry and buildSubscriptionRenewalTransaction currently construct net_amount as USD only, defaulting to "0" or potentially undefined when 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 having net_amount: { USD: "0" }, effectively zeroing out revenue for those transactions.
  • Potentially USD: undefined being serialized for renewals when only non‑USD currencies are present.

Since charged_amount is already a per‑currency map, net_amount should mirror real financial impact rather than fabricating a zero USD value. A simple fix is:

  • Filter charged_amount down 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_amount consistent 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 on merged (not just page) to keep pagination correct under filtering and avoid repeated scans

Right now lastSubId/lastIqcId/lastOtpId/lastSiId are derived only from page (the filtered slice). If a source’s rows are filtered out by query.type, its cursor never advances, so that table is repeatedly scanned from the beginning on subsequent pages. Today each table maps to a single transaction type, 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 paginated type views.

You can fix both correctness (future‑proof) and efficiency by advancing cursors based on all merged records 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 for params in listTransactions / useTransactions

Both methods now require a params object, so adminApp.listTransactions() or adminApp.useTransactions() with no args will throw at runtime when accessing params.*. 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 asserting transactionsRes.status === 200 (and similarly for transactionsAfterRefund) would make failures easier to diagnose if the endpoint ever starts returning a KnownError instead of a 2xx.
  • createTestModeTransaction currently picks the first transaction from /transactions; if you ever add more background transactions in this project, you might want to filter by type === "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 refundedAt fields and the new SubscriptionInvoice model align with how the refund route and handleStripeInvoicePaid use 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 = true can’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:

  1. Invoice line subscription extraction: Stripe SDK v18.3.0 does expose parent.subscription_item_details.subscription on invoice line items, so the parsing at lines 125–127 is correct. No issue here.

  2. 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 same accountId to both syncStripeSubscriptions and handleStripeInvoicePaid. Inside getTenancyFromStripeAccountIdOrThrow, calling stripe.accounts.retrieve(stripeAccountId) succeeds because the account-scoped client's stripeAccount config matches the stripeAccountId parameter.

However, this pattern is fragile: it silently depends on callers always passing a client and stripeAccountId that match. If a refactoring mixes a platform client with a specific stripeAccountId (or vice versa), the calls would fail subtly. To harden this:

  • Document the expected invariant: all three functions assume the stripe client's account scope (if any) matches the provided stripeAccountId.
  • Consider adding a runtime check in getTenancyFromStripeAccountIdOrThrow or an assertion that the client and accountId are compatible.
  • Add integration tests using the mock that verify a real webhook event produces the expected invoiceSubscriptionIds array (currently no tests found).
apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx (1)

101-103: Verify Prisma relation filter for subscriptionInvoice + customer_type

customerTypeFilter is { customerType: ... }, which is passed directly as subscription: customerTypeFilter in the subscriptionInvoice.findMany where clause. For typical Prisma schemas, a relation field uses a RelationFilter (e.g. { is: { customerType: ... } } for 1‑1), so subscription: customerTypeFilter may either be a type error or throw a runtime validation error.

Please double‑check the generated SubscriptionInvoiceWhereInput:

  • If subscription is 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 subscription as 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 duplicating sendStripeWebhook logic already available in backend helpers

This local sendStripeWebhook mirrors the implementation in backend-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

📥 Commits

Reviewing files that changed from the base of the PR and between 4693a82 and 19dfcff.

📒 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_PORT change 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 the db service, and no other services or external configuration files reference it.

The port 8128 references found in docker/server/.env.example and apps/backend/.env are 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_id and one_time_purchase_id are 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 InferType ensures 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 listTransactions method correctly supports filtering by type and customerType, and returns the new Transaction[] type.


613-626: LGTM! Clean refund method implementation.

The new refundTransaction method 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: null ensures 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 refundedAt column 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 sendStripeWebhook function 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 SubscriptionInvoice table 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 via Payments.sendStripeWebhook looks good.

Routing all Stripe webhook test flows through the shared Payments.sendStripeWebhook helper (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:

  1. Error semantics (lines 39, 55): The code throws SubscriptionInvoiceNotFound when the subscription itself is missing, which appears semantically confusing. However, the e2e test at apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts:112 explicitly validates this as the intended API contract—it expects SUBSCRIPTION_INVOICE_NOT_FOUND when a non-existent subscription ID is passed. This is locked in by the existing API specification, not a bug.

  2. Stripe invoice API (line 62–71): The code correctly uses invoice.payments?.data[] and accesses payment.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 "prefer invoice.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 consistent

The transactions cache shape and dependency tuple ([cursor, limit, type, customerType]) line up with the backend route and shared Transaction/TransactionType types.


602-605: Keep refundTransaction cache invalidation; looks good

Forwarding type/id to the interface and invalidating _transactionsCache ensures 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 consistent

The transactions async store now correctly exposes { transactions: Transaction[], nextCursor } with a typed TransactionType filter, and the new refundTransaction method 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 exported

The added errors for missing subscription invoices / one-time purchases, already-refunded purchases, and non-refundable test-mode purchases have consistent status codes, details, and constructorArgsFromJson implementations, and are wired into the KnownErrors map so KnownError.fromJson can 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

setupProjectWithPaymentsConfig and createPurchaseCodeForCustomer encapsulate the boilerplate around payments configuration and purchase URL creation nicely, and the createPurchaseCode wrapper keeps existing user flows simple.


118-316: Core transaction snapshots and pagination test cover new Transaction shape well

The updated snapshots for test-mode subscriptions, one-time purchases, and item quantity changes correctly assert against the new entries/type structure, 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 covered

The "omits subscription-renewal entries for subscription creation invoices" test builds realistic mock Stripe events and verifies that only renewal invoices produce subscription-renewal money_transfer entries, while the initial purchase remains typed as purchase. This is a good regression guard for the new subscriptionInvoice source.


422-455: Type-based filtering test validates backend query.type behavior

The "filters results by transaction type" test asserts that type=manual-item-quantity-change and type=purchase each 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 appropriately

The "filters results by customer_type across sources" test covers team products, team items, and ensures all entries[].customer_type match the requested customer_type. That gives good confidence that the backend customer_type filter 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 as type: "purchase" with a product_grant entry and correct inline product metadata. This closes an important gap for non-checkout subscription flows.

@BilalG1 BilalG1 merged commit 243caf9 into dev Nov 18, 2025
22 checks passed
@BilalG1 BilalG1 deleted the payment-transactions branch November 18, 2025 20:11
@coderabbitai coderabbitai bot mentioned this pull request Jan 8, 2026
This was referenced Jan 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants