feat: Add team billing tables (#24148)
* Init billing tables * Create `IBillingRepository` types * Create billing repositories * Create billingRepositoryFactory * Eslint fix - remove unused organizationOnboarding * internal-team-billing create saveTeamBilling method using repositories * On new teams write to team billing table * On new org write to org billing table * Change fields to organizationId * Add todo comment * Revert "Change fields to organizationId" This reverts commit bbb2e5dfa6b4c20a8a395f5730848a492cd70d68. * test: add comprehensive tests for team billing tables - 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> * refactor: update saveTeamBilling tests to mock repository interface - 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> * Remove log statement * Remove repository tests * Address feedback --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
92169eb0d2
commit
c702da0334
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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<Stripe>;
|
||||
|
||||
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"),
|
||||
|
||||
@@ -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<BillingRecord>;
|
||||
}
|
||||
|
||||
export interface IBillingRepositoryConstructorArgs {
|
||||
teamId: number;
|
||||
isOrganization: boolean;
|
||||
}
|
||||
|
||||
export interface IBillingRepositoryCreateArgs {
|
||||
teamId: number;
|
||||
subscriptionId: string;
|
||||
subscriptionItemId: string;
|
||||
customerId: string;
|
||||
planName: Plan;
|
||||
status: SubscriptionStatus;
|
||||
}
|
||||
@@ -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<BillingRecord> {
|
||||
const billingRecord = await this.prismaClient.organizationBilling.create({
|
||||
data: {
|
||||
...args,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...billingRecord,
|
||||
planName: billingRecord.planName as Plan,
|
||||
status: billingRecord.status as SubscriptionStatus,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<BillingRecord> {
|
||||
const billingRecord = await this.prismaClient.teamBilling.create({
|
||||
data: {
|
||||
...args,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...billingRecord,
|
||||
planName: billingRecord.planName as Plan,
|
||||
status: billingRecord.status as SubscriptionStatus,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<typeof BillingRepositoryFactory.getRepository>
|
||||
);
|
||||
|
||||
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<typeof BillingRepositoryFactory.getRepository>
|
||||
);
|
||||
|
||||
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<typeof BillingRepositoryFactory.getRepository>
|
||||
);
|
||||
|
||||
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<typeof BillingRepositoryFactory.getRepository>
|
||||
);
|
||||
|
||||
const internalTeamBilling = new InternalTeamBilling(mockTeam);
|
||||
|
||||
await expect(internalTeamBilling.saveTeamBilling(mockBillingArgs)).rejects.toThrow(
|
||||
"Database constraint violation"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<TeamBillingInput, "metadata"> & {
|
||||
metadata: NonNullable<z.infer<typeof teamPaymentMetadataSchema>>;
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -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;
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user