Skip to content

Comments

feat: Add team billing tables#24148

Merged
joeauyeung merged 17 commits intomainfrom
add-team-billing-table
Oct 7, 2025
Merged

feat: Add team billing tables#24148
joeauyeung merged 17 commits intomainfrom
add-team-billing-table

Conversation

@joeauyeung
Copy link
Contributor

@joeauyeung joeauyeung commented Sep 29, 2025

What does this PR do?

  • Adds TeamBilling and OrganizationBilling tables
  • For new teams/orgs write the billing information to the corresponding tables

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  • Create a new team with billing enabled
    • The TeamBilling table should have the corresponding billing info
  • Create a new org with billing enabled
    • The OrganizationBilling table should have the corresponding billing info

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 29, 2025

Walkthrough

The changes introduce a billing persistence layer and wire it into team and organization flows. New enums and interfaces (Plan, SubscriptionStatus, IBillingRepository) are added, with Prisma-backed repositories for team and organization billing and a factory to select between them. InternalTeamBilling is updated to use this repository and exposes saveTeamBilling. Team creation routes now conditionally persist billing details when a checkout session/subscription exists. The invoice.paid webhook logs invoices and persists organization billing. A Prisma migration and schema add TeamBilling and OrganizationBilling tables and relations. Tests are added/updated for the repository factory, InternalTeamBilling, and billing-related mocks.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
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.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title mentions adding team billing tables which is a core part of these changes, though it does not explicitly mention organization billing tables or related integration code.
Description Check ✅ Passed The description clearly outlines the addition of TeamBilling and OrganizationBilling tables along with writing billing information upon creation of teams and organizations, which directly relates to the changes in the pull request.
✨ 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 add-team-billing-table

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f8d968e and 1daddfd.

📒 Files selected for processing (1)
  • apps/web/app/api/teams/api/create/route.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/api/teams/api/create/route.ts
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Production builds / Build Docs
  • GitHub Check: Linters / lint
  • GitHub Check: Type check / check-types
  • GitHub Check: Production builds / Build API v1
  • GitHub Check: Tests / Unit

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.

@github-actions github-actions bot added the ❗️ migrations contains migration files label Sep 29, 2025
@keithwillcode keithwillcode added core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO labels Sep 29, 2025
@vercel
Copy link

vercel bot commented Sep 29, 2025

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

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Oct 7, 2025 5:56pm
cal-eu Ignored Ignored Oct 7, 2025 5:56pm

include: { members: true },
});

if (checkoutSessionSubscription) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If billing information is present then write to the billing table for new teams

subscriptionItemId: checkoutSessionSubscription.items.data[0].id,
customerId: checkoutSessionSubscription.customer as string,
// TODO: Implement true subscription status when webhook events are implemented
status: SubscriptionStatus.ACTIVE,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Until we implement handling webhook events from Stripe, assume all new subscriptions coming from the checkout session are active

},
});

if (checkoutSession && subscription) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same here. If creating a new team then write to the TeamBilling table and set the status to ACTIVE

paymentSubscriptionItemId,
});

const internalTeamBillingService = new InternalTeamBilling(organization);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the webhook event when a new organization is created and the payment succeeds. Write to the OrganizationBilling table

private billingRepository: IBillingRepository;
constructor(team: TeamBillingInput) {
this.team = team;
this.billingRepository = BillingRepositoryFactory.getRepository(team.isOrganization);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When initializing the InternalTeamBilling we also initialize the right billing repository


model TeamBilling {
id String @id @default(uuid())
teamId Int @unique
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There should be a 1:1 relationship between teams and subscriptions so we teamId and subscriptionId should be unique. Also adds an index to these fields.

Comment on lines +2620 to +2621
teamId Int @unique
team Team @relation("OrganizationBilling", fields: [teamId], references: [id], onDelete: Cascade)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about naming these to be organization specific but this would cause some confusion and added boiler plate around if (team) or if (org). Ideally the service should handle all of this logic and the caller of the service only cares about the subscription output.

- Fix credit-service.test.ts Prisma mock to export prisma object
- Replace any types with proper TypeScript types in credit-service.test.ts
- Add unit tests for PrismaTeamBillingRepository covering record creation, enum casting, and error handling
- Add unit tests for PrismaOrganizationBillingRepository with same coverage
- Add unit tests for BillingRepositoryFactory to verify correct repository selection
- Add unit tests for InternalTeamBilling.saveTeamBilling() method testing delegation to correct repositories
- All 53 tests pass with TZ=UTC yarn test
- Type checking passes with yarn type-check:ci --force

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
@joeauyeung joeauyeung marked this pull request as ready for review September 29, 2025 20:30
@joeauyeung joeauyeung requested a review from a team as a code owner September 29, 2025 20:30
@graphite-app graphite-app bot requested a review from a team September 29, 2025 20:30
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (7)
packages/prisma/schema.prisma (1)

2603-2631: TeamBilling and OrganizationBilling models look solid.

The 1:1 relationships (via unique constraints on teamId and subscriptionId) and CASCADE deletes are correctly configured.

Minor suggestions:

  1. Consider adding indexes on customerId if you'll frequently query billing records by customer
  2. Consider adding an index on status if you'll filter by subscription status
  3. Consider defining status and planName as Prisma enums (matching the TypeScript enums in IBillingRepository) for additional type safety at the schema level
packages/prisma/migrations/20250929190134_init_team_and_org_billing_tables/migration.sql (2)

2-29: Consider consolidating identical table structures.

Both TeamBilling and OrganizationBilling have identical schemas. Since organizations are teams (distinguished by the isOrganization flag), a single Billing table with teamId could reduce duplication and simplify schema maintenance. The current separate-table approach may be intentional for data isolation or query optimization, but the maintenance cost of keeping two identical structures in sync should be weighed against those benefits.


31-47: Consider additional indexes for common query patterns.

While the unique indexes on teamId and subscriptionId are appropriate, consider whether queries by customerId (e.g., listing all billing records for a Stripe customer) or composite queries are common enough to warrant additional indexes. This is a performance optimization that can be deferred if these query patterns don't exist yet.

packages/features/ee/billing/teams/internal-team-billing.ts (1)

208-210: Consider returning the created record and adding error handling.

While the delegation is clean, consider these improvements:

  1. Return value: Other persistence operations might need the created record's ID or timestamp. Consider returning the BillingRecord.
  2. Error handling: Unlike other methods in this class (e.g., cancel, updateQuantity), saveTeamBilling doesn't catch errors. Should it call this.logErrorFromUnknown(error) for consistency?

Example refactor:

-  async saveTeamBilling(args: IBillingRepositoryCreateArgs) {
-    await this.billingRepository.create(args);
+  async saveTeamBilling(args: IBillingRepositoryCreateArgs): Promise<BillingRecord> {
+    try {
+      const billingRecord = await this.billingRepository.create(args);
+      log.info(`Saved billing record for team ${args.teamId}`);
+      return billingRecord;
+    } catch (error) {
+      this.logErrorFromUnknown(error);
+      throw error;
+    }
   }
packages/features/ee/billing/repository/IBillingRepository.ts (3)

14-22: Consider including timestamps in BillingRecord interface.

The BillingRecord interface doesn't include createdAt and updatedAt fields, but the test files show these are returned by the repository implementations. Consider whether these timestamps should be part of the public interface for audit trail purposes.

If timestamps should be public, apply this diff:

 export interface BillingRecord {
   id: string;
   teamId: number;
   subscriptionId: string;
   subscriptionItemId: string;
   customerId: string;
   planName: Plan;
   status: SubscriptionStatus;
+  createdAt: Date;
+  updatedAt: Date;
 }

24-26: Consider extending IBillingRepository with additional methods.

The interface currently only includes create, but billing repositories typically need additional operations. Consider whether methods like update, findByTeamId, or findBySubscriptionId should be added to the interface now or documented as future enhancements.


28-31: Remove unused interface IBillingRepositoryConstructorArgs.

This interface is not used anywhere in the codebase. Both PrismaOrganizationBillingRepository and PrismaTeamBillingRepository constructors take PrismaClient directly instead:

constructor(private readonly prismaClient: PrismaClient) {}

Consider removing the unused interface to keep the code clean.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 04cefeb and 423e810.

📒 Files selected for processing (15)
  • apps/web/app/api/teams/api/create/route.ts (2 hunks)
  • apps/web/app/api/teams/create/route.ts (2 hunks)
  • packages/features/ee/billing/api/webhook/_invoice.paid.org.ts (3 hunks)
  • packages/features/ee/billing/credit-service.test.ts (7 hunks)
  • packages/features/ee/billing/repository/IBillingRepository.ts (1 hunks)
  • packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.test.ts (1 hunks)
  • packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts (1 hunks)
  • packages/features/ee/billing/repository/PrismaTeamBillingRepository.test.ts (1 hunks)
  • packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts (1 hunks)
  • packages/features/ee/billing/repository/billingRepositoryFactory.test.ts (1 hunks)
  • packages/features/ee/billing/repository/billingRepositoryFactory.ts (1 hunks)
  • packages/features/ee/billing/teams/internal-team-billing.test.ts (1 hunks)
  • packages/features/ee/billing/teams/internal-team-billing.ts (3 hunks)
  • packages/prisma/migrations/20250929190134_init_team_and_org_billing_tables/migration.sql (1 hunks)
  • packages/prisma/schema.prisma (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/ee/billing/repository/billingRepositoryFactory.test.ts
  • packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.test.ts
  • apps/web/app/api/teams/create/route.ts
  • packages/features/ee/billing/repository/billingRepositoryFactory.ts
  • packages/features/ee/billing/teams/internal-team-billing.ts
  • apps/web/app/api/teams/api/create/route.ts
  • packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts
  • packages/features/ee/billing/repository/IBillingRepository.ts
  • packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts
  • packages/features/ee/billing/repository/PrismaTeamBillingRepository.test.ts
  • packages/features/ee/billing/teams/internal-team-billing.test.ts
  • packages/features/ee/billing/api/webhook/_invoice.paid.org.ts
  • packages/features/ee/billing/credit-service.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/ee/billing/repository/billingRepositoryFactory.test.ts
  • packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.test.ts
  • apps/web/app/api/teams/create/route.ts
  • packages/features/ee/billing/repository/billingRepositoryFactory.ts
  • packages/features/ee/billing/teams/internal-team-billing.ts
  • apps/web/app/api/teams/api/create/route.ts
  • packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts
  • packages/features/ee/billing/repository/IBillingRepository.ts
  • packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts
  • packages/features/ee/billing/repository/PrismaTeamBillingRepository.test.ts
  • packages/features/ee/billing/teams/internal-team-billing.test.ts
  • packages/features/ee/billing/api/webhook/_invoice.paid.org.ts
  • packages/features/ee/billing/credit-service.test.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/ee/billing/repository/billingRepositoryFactory.test.ts
  • packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.test.ts
  • apps/web/app/api/teams/create/route.ts
  • packages/features/ee/billing/repository/billingRepositoryFactory.ts
  • packages/features/ee/billing/teams/internal-team-billing.ts
  • apps/web/app/api/teams/api/create/route.ts
  • packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts
  • packages/features/ee/billing/repository/IBillingRepository.ts
  • packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts
  • packages/features/ee/billing/repository/PrismaTeamBillingRepository.test.ts
  • packages/features/ee/billing/teams/internal-team-billing.test.ts
  • packages/features/ee/billing/api/webhook/_invoice.paid.org.ts
  • packages/features/ee/billing/credit-service.test.ts
**/*Repository.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Repository files must include Repository suffix, prefix with technology if applicable (e.g., PrismaAppRepository.ts), and use PascalCase matching the exported class

Files:

  • packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts
  • packages/features/ee/billing/repository/IBillingRepository.ts
  • packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts
🧬 Code graph analysis (12)
packages/features/ee/billing/repository/billingRepositoryFactory.test.ts (3)
packages/features/ee/billing/repository/billingRepositoryFactory.ts (1)
  • BillingRepositoryFactory (6-13)
packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts (1)
  • PrismaOrganizationBillingRepository (11-26)
packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts (1)
  • PrismaTeamBillingRepository (11-26)
packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.test.ts (1)
packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts (1)
  • PrismaOrganizationBillingRepository (11-26)
apps/web/app/api/teams/create/route.ts (1)
packages/features/ee/billing/teams/internal-team-billing.ts (1)
  • InternalTeamBilling (23-211)
packages/features/ee/billing/repository/billingRepositoryFactory.ts (2)
packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts (1)
  • PrismaOrganizationBillingRepository (11-26)
packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts (1)
  • PrismaTeamBillingRepository (11-26)
packages/features/ee/billing/teams/internal-team-billing.ts (2)
packages/features/ee/billing/repository/IBillingRepository.ts (2)
  • IBillingRepository (24-26)
  • IBillingRepositoryCreateArgs (33-40)
packages/features/ee/billing/repository/billingRepositoryFactory.ts (1)
  • BillingRepositoryFactory (6-13)
apps/web/app/api/teams/api/create/route.ts (1)
packages/features/ee/billing/teams/internal-team-billing.ts (1)
  • InternalTeamBilling (23-211)
packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts (1)
packages/features/ee/billing/repository/IBillingRepository.ts (3)
  • IBillingRepository (24-26)
  • IBillingRepositoryCreateArgs (33-40)
  • BillingRecord (14-22)
packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts (1)
packages/features/ee/billing/repository/IBillingRepository.ts (3)
  • IBillingRepository (24-26)
  • IBillingRepositoryCreateArgs (33-40)
  • BillingRecord (14-22)
packages/features/ee/billing/repository/PrismaTeamBillingRepository.test.ts (1)
packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts (1)
  • PrismaTeamBillingRepository (11-26)
packages/features/ee/billing/teams/internal-team-billing.test.ts (1)
packages/features/ee/billing/teams/internal-team-billing.ts (1)
  • InternalTeamBilling (23-211)
packages/features/ee/billing/api/webhook/_invoice.paid.org.ts (1)
packages/features/ee/billing/teams/internal-team-billing.ts (1)
  • InternalTeamBilling (23-211)
packages/features/ee/billing/credit-service.test.ts (1)
packages/lib/server/repository/team.ts (1)
  • TeamRepository (170-414)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (32)
packages/features/ee/billing/credit-service.test.ts (3)

17-28: LGTM: Prisma mock setup is thorough.

The async importOriginal() and spread of the actual module ensure full coverage of both default and named exports. Adding prisma.$transaction alongside default.$transaction handles multiple import patterns consistently.


93-104: LGTM: Improved Stripe mock typing.

Typing stripeMock as Partial<Stripe> and using vi.mocked(Stripe).mockImplementation() improves type safety while maintaining test flexibility.


408-408: LGTM: Stricter typing for TeamRepository mocks.

Replacing as any with as unknown as TeamRepository improves type safety by requiring an explicit double cast, without changing runtime behavior. All four instances are updated consistently.

Also applies to: 429-429, 458-458, 481-481

apps/web/app/api/teams/api/create/route.ts (2)

7-8: LGTM: Billing imports are correct.

The imports for Plan, SubscriptionStatus, and InternalTeamBilling are appropriate for the new billing persistence logic.


67-67: TODO acknowledged: Subscription status hardcoded.

The TODO correctly identifies that SubscriptionStatus.ACTIVE is assumed for all new subscriptions. True status tracking via webhooks should be implemented in a follow-up.

packages/features/ee/billing/repository/billingRepositoryFactory.test.ts (1)

1-40: LGTM: Comprehensive test coverage for the factory.

The test suite thoroughly validates BillingRepositoryFactory.getRepository:

  • Correct repository selection based on isOrganization flag
  • Type consistency across multiple calls
  • Type exclusivity with negative assertions

The coverage is complete for the factory's selection logic.

apps/web/app/api/teams/create/route.ts (2)

7-8: LGTM: Billing imports are correct.

The imports for Plan, SubscriptionStatus, and InternalTeamBilling are appropriate for the billing persistence logic.


97-97: TODO acknowledged: Subscription status hardcoded.

The TODO correctly identifies that SubscriptionStatus.ACTIVE is assumed for all new subscriptions. Webhook-based status tracking should be implemented in a follow-up.

packages/prisma/schema.prisma (2)

593-594: LGTM: Team billing relations are correctly defined.

The optional relations teamBilling and organizationBilling are appropriately added to the Team model with explicit relation names.


2636-2636: LGTM: CalendarCacheEventStatus enum extended.

Adding cancelled to the CalendarCacheEventStatus enum is appropriate. Note: this change is unrelated to the billing tables added in this PR.

packages/features/ee/billing/repository/billingRepositoryFactory.ts (1)

1-13: LGTM! Factory pattern correctly implemented.

The factory correctly selects between organization and team repositories based on the isOrganization flag. The use of a static method is appropriate for this stateless factory, and the shared Prisma client follows the standard pattern in this codebase.

packages/features/ee/billing/api/webhook/_invoice.paid.org.ts (2)

3-4: LGTM! Imports are appropriate.

The new imports for Plan, SubscriptionStatus, and InternalTeamBilling support the billing persistence functionality added below.


100-109: Missing webhook handlers for subscription lifecycle - ACTIVE status is correct but incomplete.

The hardcoded ACTIVE status is appropriate for invoice.paid events. However, verification reveals significant gaps in subscription status management:

Missing handlers for organization billing:

  • No customer.subscription.updated handler exists for team/org billing (only for CalAI phone numbers). This means PAST_DUE, TRIALING, and other status transitions won't be captured.
  • customer.subscription.deleted calls downgrade() which clears metadata but does NOT update the status field to CANCELLED in the new billing tables.

Impact on OrganizationBilling table:
The new OrganizationBilling table introduced in this PR will have stale status data after initial activation, as only invoice.paid sets the status field.

Recommendations:

  1. Implement customer.subscription.updated handler for organizations to map Stripe subscription statuses (active, past_due, trialing, etc.) to SubscriptionStatus enum values
  2. Update the downgrade() method or subscription.deleted handler to set status to CANCELLED in billing tables
  3. Address the TODO comment as a high priority

The current implementation is functionally correct for the happy path (successful payments) but lacks status synchronization for the full subscription lifecycle.

packages/prisma/migrations/20250929190134_init_team_and_org_billing_tables/migration.sql (1)

10-11: Prisma manages updatedAt — verify only raw‑SQL/edge cases

packages/prisma/schema.prisma defines TeamBilling and OrganizationBilling with updatedAt DateTime @updatedAt, so Prisma Client will set/update that column automatically for normal Prisma writes. (prismagraphql.com)

Notes / caveats:

  • Migration created updatedAt as NOT NULL without a DB default (packages/prisma/migrations/20250929190134_init_team_and_org_billing_tables/migration.sql lines 10–11) — fine for Prisma-managed writes but raw DB writes must supply a value.
  • Raw SQL / prisma.$executeRaw or external DB scripts do not trigger Prisma’s @updatedAt behavior; those paths must set updatedAt explicitly or you must add a DB trigger/DEFAULT. (prismagraphql.com)
  • If an update call provides an empty data: {} the @updatedAt value will remain unchanged. (prismagraphql.com)
  • Certain nested updateMany / nested-update patterns have known cases where related records’ @updatedAt isn’t updated — check for nested updateMany against billing records. (github.com)

Action: confirm there are no raw SQL/direct DB updates to TeamBilling or OrganizationBilling (or update those paths to set updatedAt / add a DB trigger or DEFAULT).

packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts (1)

11-26: LGTM! Repository implementation follows established patterns.

The implementation correctly mirrors PrismaTeamBillingRepository with proper dependency injection and delegation to Prisma. The type casts on lines 22-23 align with the contract defined in IBillingRepository.

packages/features/ee/billing/teams/internal-team-billing.ts (2)

15-16: LGTM! Clean repository integration.

The new imports properly introduce the billing repository abstraction layer.


27-27: LGTM! Proper dependency injection.

The private billing repository member follows the existing class structure and enables testability.

packages/features/ee/billing/repository/PrismaTeamBillingRepository.test.ts (1)

1-154: LGTM! Comprehensive test coverage.

The test suite thoroughly validates the repository implementation:

  • ✅ Basic create operation with complete data structure verification
  • ✅ Enum casting for both planName and status
  • ✅ Error propagation from Prisma
  • ✅ Proper args spreading with objectContaining
  • ✅ Multiple enum variations (TEAM, ENTERPRISE, ORGANIZATION plans; ACTIVE, TRIALING, PAST_DUE statuses)
  • ✅ Proper test isolation with beforeEach and vi.clearAllMocks

The tests provide good coverage for the critical repository behaviors and align well with the implementation in PrismaTeamBillingRepository.ts.

packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.test.ts (6)

1-14: LGTM! Clean test setup.

The imports and test setup follow best practices with proper mock clearing in beforeEach.


17-53: LGTM! Comprehensive data structure validation.

This test thoroughly validates the create operation, checking both the arguments passed to Prisma and the returned BillingRecord structure.


55-78: LGTM! Important table routing validation.

This test correctly verifies that the organization repository writes to the organizationBilling table and not to teamBilling, which is critical for data integrity.


80-128: LGTM! Enum casting validation is thorough.

Both tests correctly verify that the repository casts Prisma's string values to the appropriate enum types while maintaining runtime string type, which aligns with TypeScript enum behavior.


130-146: LGTM! Error propagation verified.

This test ensures that Prisma errors are properly propagated to callers, which is essential for proper error handling upstream.


148-177: LGTM! Interface contract validation.

This test validates that the repository correctly implements the IBillingRepository interface by checking all required properties are present in the returned object.

packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts (2)

1-9: LGTM! Clean imports following guidelines.

All imports use named exports as per coding guidelines, and only necessary types are imported.


11-26: LGTM! Solid repository implementation.

The implementation correctly:

  • Follows the Repository suffix naming convention as per coding guidelines
  • Delegates to the appropriate Prisma table (teamBilling)
  • Casts string values to enum types for type safety
  • Uses readonly for the PrismaClient dependency
packages/features/ee/billing/teams/internal-team-billing.test.ts (4)

177-209: LGTM! Correct repository delegation for organizations.

This test validates that organization teams correctly route to the organizationBilling repository and don't accidentally write to the teamBilling table.


211-242: LGTM! Correct repository delegation for regular teams.

This test validates that non-organization teams correctly route to the teamBilling repository, providing complete coverage of the routing logic.


244-281: LGTM! Comprehensive argument validation.

This test ensures all billing fields (teamId, subscriptionId, subscriptionItemId, customerId, planName, status) are correctly passed through to the repository, including different enum values (ENTERPRISE, TRIALING).


283-307: LGTM! Error propagation validated.

This test ensures that repository errors (like database constraint violations) are properly propagated to callers rather than being swallowed, which is critical for proper error handling.

packages/features/ee/billing/repository/IBillingRepository.ts (2)

1-5: LGTM! Well-defined Plan enum.

The Plan enum clearly defines the three billing tiers with string values matching their keys, which aids debugging and logging.


7-12: Consider additional subscription statuses for future webhook implementation.

The current enum defines 4 statuses (ACTIVE, CANCELLED, PAST_DUE, TRIALING), which match the most commonly checked states in the codebase. However, note that:

  • Runtime status checks currently use raw Stripe status strings (e.g., subscriptionStatus === "active"), not this enum
  • This enum is primarily used for database storage in billing records
  • No webhook handlers exist yet for team/organization subscription updates (unlike phone number subscriptions which have comprehensive status mapping)
  • Multiple TODOs indicate webhook implementation is pending

When implementing webhook handlers for team/organization subscriptions, consider adding:

  • INCOMPLETE - subscription requiring payment action
  • INCOMPLETE_EXPIRED - expired incomplete subscription
  • UNPAID - subscription with unpaid invoice
  • PAUSED - paused subscription (if using Stripe's pause feature)

Reference the existing phone number subscription webhook handler at packages/features/ee/billing/api/webhook/_customer.subscription.updated.ts which demonstrates comprehensive Stripe status mapping.

- Replace prismaMock usage with BillingRepositoryFactory mock
- Mock IBillingRepository interface instead of Prisma directly
- Follow repository mocking pattern from handleResponse.test.ts
- All tests passing (53 total)

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
@github-actions
Copy link
Contributor

github-actions bot commented Oct 3, 2025

E2E results are ready!

Copy link
Contributor

@volnei volnei left a comment

Choose a reason for hiding this comment

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

We need to get rid of prismaMock.

@@ -0,0 +1,178 @@
import prismaMock from "../../../../../tests/libs/__mocks__/prismaMock";
Copy link
Contributor

Choose a reason for hiding this comment

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

anyway to avoid using prismaMock here?

@@ -0,0 +1,154 @@
import prismaMock from "../../../../../tests/libs/__mocks__/prismaMock";
Copy link
Contributor

Choose a reason for hiding this comment

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

here too

@github-actions github-actions bot marked this pull request as draft October 3, 2025 18:42
@pull-request-size pull-request-size bot added size/L and removed size/XL labels Oct 3, 2025
@joeauyeung joeauyeung requested a review from volnei October 3, 2025 19:00
@joeauyeung joeauyeung marked this pull request as ready for review October 3, 2025 19:00
@joeauyeung
Copy link
Contributor Author

@volnei removed the repository tests as these don't have any logic in them.

volnei
volnei previously approved these changes Oct 7, 2025
Copy link
Contributor

@volnei volnei left a comment

Choose a reason for hiding this comment

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

LGTM

import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we are here can you replace this by { prisma } instead of the default exports?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

billing area: billing, stripe, payments, paypal, get paid core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO ❗️ migrations contains migration files ready-for-e2e size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants