diff --git a/.env.example b/.env.example index 016b02fd22e143..dd1fa49e19e944 100644 --- a/.env.example +++ b/.env.example @@ -203,6 +203,7 @@ NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0 NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE= STRIPE_TEAM_MONTHLY_PRICE_ID= NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID= +ORG_MONTHLY_CREDITS= STRIPE_TEAM_PRODUCT_ID= # It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID STRIPE_ORG_MONTHLY_PRICE_ID= diff --git a/packages/features/ee/billing/credit-service.test.ts b/packages/features/ee/billing/credit-service.test.ts index de679ab30ad04f..702f93b9ca0529 100644 --- a/packages/features/ee/billing/credit-service.test.ts +++ b/packages/features/ee/billing/credit-service.test.ts @@ -435,8 +435,8 @@ describe("CreditService", () => { expect(result).toBe(1500); // (3 members * 1000 price) / 2 }); - it("should calculate credits with 20% multiplier for organizations", async () => { - vi.stubEnv("STRIPE_ORG_MONTHLY_PRICE_ID", "price_org_monthly"); + it("should calculate credits for organizations using ORG_MONTHLY_CREDITS", async () => { + vi.stubEnv("ORG_MONTHLY_CREDITS", "1500"); const mockTeamRepo = { findTeamWithMembers: vi.fn().mockResolvedValue({ id: 1, @@ -453,15 +453,31 @@ describe("CreditService", () => { mockTeamBillingService.getSubscriptionStatus ); - const mockStripeBillingService = { - getPrice: vi.fn().mockResolvedValue({ unit_amount: 3700 }), + const result = await creditService.getMonthlyCredits(1); + expect(result).toBe(3000); // 2 members * 1500 credits per seat + }); + + it("should calculate credits for organizations with default 1000 credits per seat", async () => { + // Clear ORG_MONTHLY_CREDITS to test default behavior + vi.stubEnv("ORG_MONTHLY_CREDITS", undefined); + const mockTeamRepo = { + findTeamWithMembers: vi.fn().mockResolvedValue({ + id: 1, + isOrganization: true, + members: [{ accepted: true }, { accepted: true }, { accepted: true }], + }), }; - vi.spyOn(StripeBillingService.prototype, "getPrice").mockImplementation( - mockStripeBillingService.getPrice + vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any); + + const mockTeamBillingService = { + getSubscriptionStatus: vi.fn().mockResolvedValue("active"), + }; + vi.spyOn(InternalTeamBilling.prototype, "getSubscriptionStatus").mockImplementation( + mockTeamBillingService.getSubscriptionStatus ); const result = await creditService.getMonthlyCredits(1); - expect(result).toBe(1480); // (2 members * 3700 price) * 0.2 + expect(result).toBe(3000); // 3 members * 1000 credits per seat (default) }); }); diff --git a/packages/features/ee/billing/credit-service.ts b/packages/features/ee/billing/credit-service.ts index 35c10d045cb8cf..067b3f3dde5146 100644 --- a/packages/features/ee/billing/credit-service.ts +++ b/packages/features/ee/billing/credit-service.ts @@ -588,8 +588,6 @@ export class CreditService { if (!team) return 0; - let totalMonthlyCredits = 0; - const teamBillingService = new InternalTeamBilling(team); const subscriptionStatus = await teamBillingService.getSubscriptionStatus(); @@ -599,25 +597,25 @@ export class CreditService { const activeMembers = team.members.filter((member) => member.accepted).length; - const billingService = new StripeBillingService(); + if (team.isOrganization) { + const orgMonthlyCredits = process.env.ORG_MONTHLY_CREDITS; + const creditsPerSeat = orgMonthlyCredits ? parseInt(orgMonthlyCredits) : 1000; + return activeMembers * creditsPerSeat; + } - const priceId = team.isOrganization - ? process.env.STRIPE_ORG_MONTHLY_PRICE_ID - : process.env.STRIPE_TEAM_MONTHLY_PRICE_ID; + const billingService = new StripeBillingService(); + const priceId = process.env.STRIPE_TEAM_MONTHLY_PRICE_ID; if (!priceId) { - log.warn("Monthly price ID not configured", { teamId, isOrganization: team.isOrganization }); + log.warn("Monthly price ID not configured", { teamId }); return 0; } - const monthlyPrice = await billingService.getPrice(priceId || ""); + const monthlyPrice = await billingService.getPrice(priceId); const pricePerSeat = monthlyPrice.unit_amount ?? 0; + const creditsPerSeat = pricePerSeat * 0.5; - // Teams get 50% of the price as credits, organizations get 20% - const creditMultiplier = team.isOrganization ? 0.2 : 0.5; - totalMonthlyCredits = activeMembers * pricePerSeat * creditMultiplier; - - return totalMonthlyCredits; + return activeMembers * creditsPerSeat; } calculateCreditsFromPrice(price: number) { diff --git a/turbo.json b/turbo.json index 412782c5bb7eaf..1042760af8085f 100644 --- a/turbo.json +++ b/turbo.json @@ -120,6 +120,7 @@ "NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE_MONTHLY", "NEXT_PUBLIC_STRIPE_PRICING_TABLE_PUBLISHABLE_KEY", "NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID", + "ORG_MONTHLY_CREDITS", "NEXT_PUBLIC_PLAIN_CHAT_ID", "NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS", "NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD", @@ -187,6 +188,7 @@ "STRIPE_TEAM_MONTHLY_PRICE_ID", "STRIPE_TEAM_PRODUCT_ID", "STRIPE_ORG_MONTHLY_PRICE_ID", + "ORG_MONTHLY_CREDITS", "TANDEM_BASE_URL", "TANDEM_CLIENT_ID", "TANDEM_CLIENT_SECRET", @@ -331,6 +333,7 @@ "STRIPE_TEAM_MONTHLY_PRICE_ID", "NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID", "STRIPE_TEAM_PRODUCT_ID", + "ORG_MONTHLY_CREDITS", "STRIPE_ORG_MONTHLY_PRICE_ID", "STRIPE_ORG_PRODUCT_ID", "NEXT_PUBLIC_API_V2_URL",