diff --git a/apps/web/app/api/teams/api/create/route.ts b/apps/web/app/api/teams/api/create/route.ts index 08fd304fbb..670fd044b5 100644 --- a/apps/web/app/api/teams/api/create/route.ts +++ b/apps/web/app/api/teams/api/create/route.ts @@ -4,9 +4,11 @@ import { NextResponse } from "next/server"; import type Stripe from "stripe"; import { z } from "zod"; +import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository"; +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"; +import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; import { MembershipSchema } from "@calcom/prisma/zod/modelSchema/MembershipSchema"; import { TeamSchema } from "@calcom/prisma/zod/modelSchema/TeamSchema"; @@ -54,6 +56,19 @@ async function handler(request: NextRequest) { include: { members: true }, }); + if (checkoutSessionSubscription) { + const internalBillingService = new InternalTeamBilling(finalizedTeam); + await internalBillingService.saveTeamBilling({ + teamId: finalizedTeam.id, + subscriptionId: checkoutSessionSubscription.id, + subscriptionItemId: checkoutSessionSubscription.items.data[0].id, + customerId: checkoutSessionSubscription.customer as string, + // TODO: Implement true subscription status when webhook events are implemented + status: SubscriptionStatus.ACTIVE, + planName: Plan.TEAM, + }); + } + const response = { message: `Team created successfully. We also made user with ID=${checkoutSessionMetadata.ownerId} the owner of this team.`, team: schemaTeamReadPublic.parse(finalizedTeam), diff --git a/apps/web/app/api/teams/create/route.ts b/apps/web/app/api/teams/create/route.ts index b4e68fb47e..0099914ab5 100644 --- a/apps/web/app/api/teams/create/route.ts +++ b/apps/web/app/api/teams/create/route.ts @@ -4,6 +4,8 @@ import { NextResponse } from "next/server"; import type Stripe from "stripe"; import { z } from "zod"; +import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository"; +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"; @@ -84,6 +86,19 @@ async function getHandler(req: NextRequest) { }, }); + if (checkoutSession && subscription) { + const internalBillingService = new InternalTeamBilling(team); + await internalBillingService.saveTeamBilling({ + teamId: team.id, + subscriptionId: subscription.id, + subscriptionItemId: subscription.items.data[0].id, + customerId: subscription.customer as string, + // TODO: Implement true subscription status when webhook events are implemented + status: SubscriptionStatus.ACTIVE, + planName: Plan.TEAM, + }); + } + // redirect to team screen return NextResponse.redirect( new URL(`/settings/teams/${team.id}/onboard-members?event=team_created`, req.nextUrl.origin), diff --git a/packages/features/ee/billing/api/webhook/_invoice.paid.org.ts b/packages/features/ee/billing/api/webhook/_invoice.paid.org.ts index c40d2dbdb2..739b2a15dc 100644 --- a/packages/features/ee/billing/api/webhook/_invoice.paid.org.ts +++ b/packages/features/ee/billing/api/webhook/_invoice.paid.org.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository"; +import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing"; import { createOrganizationFromOnboarding } from "@calcom/features/ee/organizations/lib/server/createOrganizationFromOnboarding"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -94,6 +96,17 @@ const handler = async (data: SWHMap["invoice.paid"]["data"]) => { paymentSubscriptionItemId, }); + const internalTeamBillingService = new InternalTeamBilling(organization); + await internalTeamBillingService.saveTeamBilling({ + teamId: organization.id, + subscriptionId: paymentSubscriptionId, + subscriptionItemId: paymentSubscriptionItemId, + customerId: invoice.customer, + // TODO: Write actual status when webhook events are added + status: SubscriptionStatus.ACTIVE, + planName: Plan.ORGANIZATION, + }); + logger.debug(`Marking onboarding as complete for organization ${organization.id}`); await OrganizationOnboardingRepository.markAsComplete(organizationOnboarding.id); return { success: true }; diff --git a/packages/features/ee/billing/credit-service.test.ts b/packages/features/ee/billing/credit-service.test.ts index e19106ab6a..1e61fae92e 100644 --- a/packages/features/ee/billing/credit-service.test.ts +++ b/packages/features/ee/billing/credit-service.test.ts @@ -14,11 +14,16 @@ import { InternalTeamBilling } from "./teams/internal-team-billing"; const MOCK_TX = {}; -vi.mock("@calcom/prisma", () => { +vi.mock("@calcom/prisma", async (importOriginal) => { + const actual = await importOriginal(); return { + ...actual, default: { $transaction: vi.fn((fn) => fn(MOCK_TX)), }, + prisma: { + $transaction: vi.fn((fn) => fn(MOCK_TX)), + }, }; }); @@ -85,7 +90,7 @@ CreditsRepository.findCreditBalance.mockResolvedValueOnce({ describe("CreditService", () => { let creditService: CreditService; - let stripeMock: any; + let stripeMock: Partial; beforeEach(() => { vi.restoreAllMocks(); @@ -96,7 +101,7 @@ describe("CreditService", () => { retrieve: vi.fn().mockResolvedValue({ id: "price_123", unit_amount: 1000 }), }, }; - (Stripe as any).mockImplementation(() => stripeMock); + vi.mocked(Stripe).mockImplementation(() => stripeMock as Stripe); creditService = new CreditService(); vi.mocked(CreditsRepository.findCreditExpenseLogByExternalRef).mockResolvedValue(null); @@ -400,7 +405,7 @@ describe("CreditService", () => { members: [{ accepted: true }], }), }; - vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any); + vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as unknown as TeamRepository); const mockTeamBillingService = { getSubscriptionStatus: vi.fn().mockResolvedValue("trialing"), @@ -421,7 +426,7 @@ describe("CreditService", () => { members: [{ accepted: true }, { accepted: true }, { accepted: true }], }), }; - vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any); + vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as unknown as TeamRepository); const mockTeamBillingService = { getSubscriptionStatus: vi.fn().mockResolvedValue("active"), @@ -450,7 +455,7 @@ describe("CreditService", () => { members: [{ accepted: true }, { accepted: true }], }), }; - vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any); + vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as unknown as TeamRepository); const mockTeamBillingService = { getSubscriptionStatus: vi.fn().mockResolvedValue("active"), @@ -473,7 +478,7 @@ describe("CreditService", () => { members: [{ accepted: true }, { accepted: true }, { accepted: true }], }), }; - vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any); + vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as unknown as TeamRepository); const mockTeamBillingService = { getSubscriptionStatus: vi.fn().mockResolvedValue("active"), diff --git a/packages/features/ee/billing/repository/IBillingRepository.ts b/packages/features/ee/billing/repository/IBillingRepository.ts new file mode 100644 index 0000000000..0d5e619178 --- /dev/null +++ b/packages/features/ee/billing/repository/IBillingRepository.ts @@ -0,0 +1,40 @@ +export enum Plan { + TEAM = "TEAM", + ORGANIZATION = "ORGANIZATION", + ENTERPRISE = "ENTERPRISE", +} + +export enum SubscriptionStatus { + ACTIVE = "ACTIVE", + CANCELLED = "CANCELLED", + PAST_DUE = "PAST_DUE", + TRIALING = "TRIALING", +} + +export interface BillingRecord { + id: string; + teamId: number; + subscriptionId: string; + subscriptionItemId: string; + customerId: string; + planName: Plan; + status: SubscriptionStatus; +} + +export interface IBillingRepository { + create(args: IBillingRepositoryCreateArgs): Promise; +} + +export interface IBillingRepositoryConstructorArgs { + teamId: number; + isOrganization: boolean; +} + +export interface IBillingRepositoryCreateArgs { + teamId: number; + subscriptionId: string; + subscriptionItemId: string; + customerId: string; + planName: Plan; + status: SubscriptionStatus; +} diff --git a/packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts b/packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts new file mode 100644 index 0000000000..6783a47a02 --- /dev/null +++ b/packages/features/ee/billing/repository/PrismaOrganizationBillingRepository.ts @@ -0,0 +1,26 @@ +import type { PrismaClient } from "@calcom/prisma"; + +import { + IBillingRepository, + IBillingRepositoryCreateArgs, + BillingRecord, + Plan, + SubscriptionStatus, +} from "./IBillingRepository"; + +export class PrismaOrganizationBillingRepository implements IBillingRepository { + constructor(private readonly prismaClient: PrismaClient) {} + async create(args: IBillingRepositoryCreateArgs): Promise { + const billingRecord = await this.prismaClient.organizationBilling.create({ + data: { + ...args, + }, + }); + + return { + ...billingRecord, + planName: billingRecord.planName as Plan, + status: billingRecord.status as SubscriptionStatus, + }; + } +} diff --git a/packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts b/packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts new file mode 100644 index 0000000000..685b39bdb6 --- /dev/null +++ b/packages/features/ee/billing/repository/PrismaTeamBillingRepository.ts @@ -0,0 +1,26 @@ +import type { PrismaClient } from "@calcom/prisma"; + +import { + IBillingRepository, + IBillingRepositoryCreateArgs, + BillingRecord, + Plan, + SubscriptionStatus, +} from "./IBillingRepository"; + +export class PrismaTeamBillingRepository implements IBillingRepository { + constructor(private readonly prismaClient: PrismaClient) {} + async create(args: IBillingRepositoryCreateArgs): Promise { + const billingRecord = await this.prismaClient.teamBilling.create({ + data: { + ...args, + }, + }); + + return { + ...billingRecord, + planName: billingRecord.planName as Plan, + status: billingRecord.status as SubscriptionStatus, + }; + } +} diff --git a/packages/features/ee/billing/repository/billingRepositoryFactory.test.ts b/packages/features/ee/billing/repository/billingRepositoryFactory.test.ts new file mode 100644 index 0000000000..b16251d049 --- /dev/null +++ b/packages/features/ee/billing/repository/billingRepositoryFactory.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; + +import { PrismaOrganizationBillingRepository } from "./PrismaOrganizationBillingRepository"; +import { PrismaTeamBillingRepository } from "./PrismaTeamBillingRepository"; +import { BillingRepositoryFactory } from "./billingRepositoryFactory"; + +describe("BillingRepositoryFactory", () => { + describe("getRepository", () => { + it("should return PrismaOrganizationBillingRepository when isOrganization is true", () => { + const repository = BillingRepositoryFactory.getRepository(true); + + expect(repository).toBeInstanceOf(PrismaOrganizationBillingRepository); + }); + + it("should return PrismaTeamBillingRepository when isOrganization is false", () => { + const repository = BillingRepositoryFactory.getRepository(false); + + expect(repository).toBeInstanceOf(PrismaTeamBillingRepository); + }); + + it("should return same repository type for multiple calls with same parameter", () => { + const repository1 = BillingRepositoryFactory.getRepository(true); + const repository2 = BillingRepositoryFactory.getRepository(true); + + expect(repository1).toBeInstanceOf(PrismaOrganizationBillingRepository); + expect(repository2).toBeInstanceOf(PrismaOrganizationBillingRepository); + }); + + it("should return different repository types for different parameters", () => { + const orgRepository = BillingRepositoryFactory.getRepository(true); + const teamRepository = BillingRepositoryFactory.getRepository(false); + + expect(orgRepository).toBeInstanceOf(PrismaOrganizationBillingRepository); + expect(teamRepository).toBeInstanceOf(PrismaTeamBillingRepository); + expect(orgRepository).not.toBeInstanceOf(PrismaTeamBillingRepository); + expect(teamRepository).not.toBeInstanceOf(PrismaOrganizationBillingRepository); + }); + }); +}); diff --git a/packages/features/ee/billing/repository/billingRepositoryFactory.ts b/packages/features/ee/billing/repository/billingRepositoryFactory.ts new file mode 100644 index 0000000000..94cdccbdf3 --- /dev/null +++ b/packages/features/ee/billing/repository/billingRepositoryFactory.ts @@ -0,0 +1,13 @@ +import { prisma } from "@calcom/prisma"; + +import { PrismaOrganizationBillingRepository } from "./PrismaOrganizationBillingRepository"; +import { PrismaTeamBillingRepository } from "./PrismaTeamBillingRepository"; + +export class BillingRepositoryFactory { + static getRepository(isOrganization: boolean) { + if (isOrganization) { + return new PrismaOrganizationBillingRepository(prisma); + } + return new PrismaTeamBillingRepository(prisma); + } +} diff --git a/packages/features/ee/billing/teams/internal-team-billing.test.ts b/packages/features/ee/billing/teams/internal-team-billing.test.ts index 4fbe172413..548ae8ba7e 100644 --- a/packages/features/ee/billing/teams/internal-team-billing.test.ts +++ b/packages/features/ee/billing/teams/internal-team-billing.test.ts @@ -6,6 +6,7 @@ import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/pay import { WEBAPP_URL } from "@calcom/lib/constants"; import * as billingModule from ".."; +import { BillingRepositoryFactory } from "../repository/billingRepositoryFactory"; import { InternalTeamBilling } from "./internal-team-billing"; import { TeamBillingPublishResponseStatus } from "./team-billing"; @@ -28,6 +29,8 @@ vi.mock("..", () => ({ vi.mock("@calcom/features/ee/teams/lib/payments", () => ({ purchaseTeamOrOrgSubscription: vi.fn(), })); + +vi.mock("../repository/billingRepositoryFactory"); const mockTeam = { id: 1, metadata: { @@ -173,4 +176,160 @@ describe("InternalTeamBilling", () => { }); }); }); + + describe("saveTeamBilling", () => { + const mockOrgRepository = { + create: vi.fn(), + }; + + const mockTeamRepository = { + create: vi.fn(), + }; + + it("should delegate to organization billing repository when team is an organization", async () => { + const mockOrgTeam = { + id: 1, + metadata: {}, + isOrganization: true, + parentId: null, + }; + + const mockBillingArgs = { + teamId: 1, + subscriptionId: "sub_org_123", + subscriptionItemId: "si_org_123", + customerId: "cus_org_123", + planName: "ORGANIZATION" as const, + status: "ACTIVE" as const, + }; + + const mockCreatedRecord = { + id: "billing_org_123", + ...mockBillingArgs, + createdAt: new Date(), + updatedAt: new Date(), + }; + + mockOrgRepository.create.mockResolvedValue(mockCreatedRecord); + vi.mocked(BillingRepositoryFactory.getRepository).mockReturnValue( + mockOrgRepository as unknown as ReturnType + ); + + const internalTeamBilling = new InternalTeamBilling(mockOrgTeam); + await internalTeamBilling.saveTeamBilling(mockBillingArgs); + + expect(BillingRepositoryFactory.getRepository).toHaveBeenCalledWith(true); + expect(mockOrgRepository.create).toHaveBeenCalledWith(mockBillingArgs); + }); + + it("should delegate to team billing repository when team is not an organization", async () => { + const mockRegularTeam = { + id: 2, + metadata: {}, + isOrganization: false, + parentId: null, + }; + + const mockBillingArgs = { + teamId: 2, + subscriptionId: "sub_team_456", + subscriptionItemId: "si_team_456", + customerId: "cus_team_456", + planName: "TEAM" as const, + status: "ACTIVE" as const, + }; + + const mockCreatedRecord = { + id: "billing_team_456", + ...mockBillingArgs, + createdAt: new Date(), + updatedAt: new Date(), + }; + + mockTeamRepository.create.mockResolvedValue(mockCreatedRecord); + vi.mocked(BillingRepositoryFactory.getRepository).mockReturnValue( + mockTeamRepository as unknown as ReturnType + ); + + const internalTeamBilling = new InternalTeamBilling(mockRegularTeam); + await internalTeamBilling.saveTeamBilling(mockBillingArgs); + + expect(BillingRepositoryFactory.getRepository).toHaveBeenCalledWith(false); + expect(mockTeamRepository.create).toHaveBeenCalledWith(mockBillingArgs); + }); + + it("should pass all billing arguments correctly to repository", async () => { + const mockTeam = { + id: 3, + metadata: {}, + isOrganization: false, + parentId: null, + }; + + const mockBillingArgs = { + teamId: 3, + subscriptionId: "sub_detailed_789", + subscriptionItemId: "si_detailed_789", + customerId: "cus_detailed_789", + planName: "ENTERPRISE" as const, + status: "TRIALING" as const, + }; + + const mockCreatedRecord = { + id: "billing_detailed_789", + ...mockBillingArgs, + createdAt: new Date(), + updatedAt: new Date(), + }; + + mockTeamRepository.create.mockResolvedValue(mockCreatedRecord); + vi.mocked(BillingRepositoryFactory.getRepository).mockReturnValue( + mockTeamRepository as unknown as ReturnType + ); + + const internalTeamBilling = new InternalTeamBilling(mockTeam); + await internalTeamBilling.saveTeamBilling(mockBillingArgs); + + expect(mockTeamRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + teamId: 3, + subscriptionId: "sub_detailed_789", + subscriptionItemId: "si_detailed_789", + customerId: "cus_detailed_789", + planName: "ENTERPRISE", + status: "TRIALING", + }) + ); + }); + + it("should propagate repository errors", async () => { + const mockTeam = { + id: 4, + metadata: {}, + isOrganization: false, + parentId: null, + }; + + const mockBillingArgs = { + teamId: 4, + subscriptionId: "sub_error_999", + subscriptionItemId: "si_error_999", + customerId: "cus_error_999", + planName: "TEAM" as const, + status: "ACTIVE" as const, + }; + + const repositoryError = new Error("Database constraint violation"); + mockTeamRepository.create.mockRejectedValue(repositoryError); + vi.mocked(BillingRepositoryFactory.getRepository).mockReturnValue( + mockTeamRepository as unknown as ReturnType + ); + + const internalTeamBilling = new InternalTeamBilling(mockTeam); + + await expect(internalTeamBilling.saveTeamBilling(mockBillingArgs)).rejects.toThrow( + "Database constraint violation" + ); + }); + }); }); diff --git a/packages/features/ee/billing/teams/internal-team-billing.ts b/packages/features/ee/billing/teams/internal-team-billing.ts index ea62553baa..2b76ca11a0 100644 --- a/packages/features/ee/billing/teams/internal-team-billing.ts +++ b/packages/features/ee/billing/teams/internal-team-billing.ts @@ -7,12 +7,13 @@ import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers"; import logger from "@calcom/lib/logger"; import { Redirect } from "@calcom/lib/redirect"; import { safeStringify } from "@calcom/lib/safeStringify"; -import { OrganizationOnboardingRepository } from "@calcom/lib/server/repository/organizationOnboarding"; import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils"; import billing from ".."; +import { IBillingRepository, IBillingRepositoryCreateArgs } from "../repository/IBillingRepository"; +import { BillingRepositoryFactory } from "../repository/billingRepositoryFactory"; import { TeamBillingPublishResponseStatus, type TeamBilling, type TeamBillingInput } from "./team-billing"; const log = logger.getSubLogger({ prefix: ["TeamBilling"] }); @@ -23,8 +24,10 @@ export class InternalTeamBilling implements TeamBilling { private _team!: Omit & { metadata: NonNullable>; }; + private billingRepository: IBillingRepository; constructor(team: TeamBillingInput) { this.team = team; + this.billingRepository = BillingRepositoryFactory.getRepository(team.isOrganization); } set team(team: TeamBillingInput) { const metadata = teamPaymentMetadataSchema.parse(team.metadata || {}); @@ -125,9 +128,6 @@ export class InternalTeamBilling implements TeamBilling { const { id: teamId, metadata, isOrganization } = this.team; const { url } = await this.checkIfTeamPaymentRequired(); - const organizationOnboarding = await OrganizationOnboardingRepository.findByOrganizationId( - this.team.id - ); log.debug("updateQuantity", safeStringify({ url, team: this.team })); /** @@ -205,4 +205,7 @@ export class InternalTeamBilling implements TeamBilling { return false; } } + async saveTeamBilling(args: IBillingRepositoryCreateArgs) { + await this.billingRepository.create(args); + } } diff --git a/packages/prisma/migrations/20250929190134_init_team_and_org_billing_tables/migration.sql b/packages/prisma/migrations/20250929190134_init_team_and_org_billing_tables/migration.sql new file mode 100644 index 0000000000..6e166c12e5 --- /dev/null +++ b/packages/prisma/migrations/20250929190134_init_team_and_org_billing_tables/migration.sql @@ -0,0 +1,47 @@ +-- CreateTable +CREATE TABLE "TeamBilling" ( + "id" TEXT NOT NULL, + "teamId" INTEGER NOT NULL, + "subscriptionId" TEXT NOT NULL, + "subscriptionItemId" TEXT NOT NULL, + "customerId" TEXT NOT NULL, + "status" TEXT NOT NULL, + "planName" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TeamBilling_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OrganizationBilling" ( + "id" TEXT NOT NULL, + "teamId" INTEGER NOT NULL, + "subscriptionId" TEXT NOT NULL, + "subscriptionItemId" TEXT NOT NULL, + "customerId" TEXT NOT NULL, + "status" TEXT NOT NULL, + "planName" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OrganizationBilling_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "TeamBilling_teamId_key" ON "TeamBilling"("teamId"); + +-- CreateIndex +CREATE UNIQUE INDEX "TeamBilling_subscriptionId_key" ON "TeamBilling"("subscriptionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "OrganizationBilling_teamId_key" ON "OrganizationBilling"("teamId"); + +-- CreateIndex +CREATE UNIQUE INDEX "OrganizationBilling_subscriptionId_key" ON "OrganizationBilling"("subscriptionId"); + +-- AddForeignKey +ALTER TABLE "TeamBilling" ADD CONSTRAINT "TeamBilling_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OrganizationBilling" ADD CONSTRAINT "OrganizationBilling_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index c3c254de42..68783b98f0 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -588,6 +588,9 @@ model Team { managedOrganizations ManagedOrganization[] @relation("ManagerOrganization") filterSegments FilterSegment[] + teamBilling TeamBilling? @relation("TeamBilling") + organizationBilling OrganizationBilling? @relation("OrganizationBilling") + @@unique([slug, parentId]) @@index([parentId]) } @@ -2595,6 +2598,36 @@ model CalAiPhoneNumber { @@index([outboundAgentId]) } +model TeamBilling { + id String @id @default(uuid()) + teamId Int @unique + team Team? @relation("TeamBilling", fields: [teamId], references: [id], onDelete: Cascade) + + subscriptionId String @unique + subscriptionItemId String + customerId String + status String + planName String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model OrganizationBilling { + id String @id @default(uuid()) + teamId Int @unique + team Team @relation("OrganizationBilling", fields: [teamId], references: [id], onDelete: Cascade) + + subscriptionId String @unique + subscriptionItemId String + customerId String + status String + planName String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + enum CalendarCacheEventStatus { confirmed @map("confirmed") tentative @map("tentative")