diff --git a/packages/features/ee/billing/credit-service.test.ts b/packages/features/ee/billing/credit-service.test.ts index b790ae39ab..957e531b6b 100644 --- a/packages/features/ee/billing/credit-service.test.ts +++ b/packages/features/ee/billing/credit-service.test.ts @@ -842,6 +842,60 @@ describe("CreditService", () => { creditType: CreditType.ADDITIONAL, }); }); + + it("should fall back to user credits when team has limitReached", async () => { + vi.spyOn(CreditService.prototype, "_getTeamWithAvailableCredits").mockResolvedValue({ + teamId: 1, + availableCredits: 0, + creditType: CreditType.ADDITIONAL, + limitReached: true, + }); + + vi.spyOn(CreditService.prototype, "_getAllCredits").mockResolvedValue({ + totalMonthlyCredits: 0, + totalRemainingMonthlyCredits: 0, + additionalCredits: 50, + }); + + const result = await creditService.getUserOrTeamToCharge({ + credits: 10, + userId: 1, + }); + + expect(result).toEqual({ + userId: 1, + remainingCredits: 40, + creditType: CreditType.ADDITIONAL, + }); + }); + + it("should return team when both team and user have no credits", async () => { + vi.spyOn(CreditService.prototype, "_getTeamWithAvailableCredits").mockResolvedValue({ + teamId: 1, + availableCredits: 0, + creditType: CreditType.ADDITIONAL, + limitReached: true, + }); + + vi.spyOn(CreditService.prototype, "_getAllCredits").mockResolvedValue({ + totalMonthlyCredits: 0, + totalRemainingMonthlyCredits: 0, + additionalCredits: 0, + }); + + const result = await creditService.getUserOrTeamToCharge({ + credits: 10, + userId: 1, + }); + + expect(result).toEqual({ + teamId: 1, + availableCredits: 0, + creditType: CreditType.ADDITIONAL, + limitReached: true, + remainingCredits: -10, + }); + }); }); it("should skip unpublished platform organizations and return regular team with credits", async () => { diff --git a/packages/features/ee/billing/credit-service.ts b/packages/features/ee/billing/credit-service.ts index a01d0e94bc..75979f362a 100644 --- a/packages/features/ee/billing/credit-service.ts +++ b/packages/features/ee/billing/credit-service.ts @@ -1,5 +1,3 @@ -import type { TFunction } from "i18next"; - import dayjs from "@calcom/dayjs"; import { CreditsRepository } from "@calcom/features/credits/repositories/CreditsRepository"; import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository"; @@ -7,9 +5,9 @@ import { MembershipRepository } from "@calcom/features/membership/repositories/M import { IS_SMS_CREDITS_ENABLED } from "@calcom/lib/constants"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import logger from "@calcom/lib/logger"; -import { prisma, type PrismaTransaction } from "@calcom/prisma"; -import { CreditUsageType, CreditType } from "@calcom/prisma/enums"; - +import { type PrismaTransaction, prisma } from "@calcom/prisma"; +import { CreditType, CreditUsageType } from "@calcom/prisma/enums"; +import type { TFunction } from "i18next"; import { getBillingProviderService, getTeamBillingServiceFactory } from "./di/containers/Billing"; import { SubscriptionStatus } from "./repository/billing/IBillingRepository"; @@ -317,7 +315,7 @@ export class CreditService { } /* - always returns a team, even if all teams are out of credits + always returns a team or user, even if out of credits */ async getUserOrTeamToCharge({ credits, @@ -357,11 +355,27 @@ export class CreditService { if (userId) { const team = await this._getTeamWithAvailableCredits({ userId, tx }); - if (team) { + + const teamHasCredits = team && !team.limitReached; + + if (teamHasCredits) { return { ...team, remainingCredits: team.availableCredits - credits }; } const userCredits = await this._getAllCredits({ userId, tx }); + const userHasCredits = userCredits.additionalCredits > 0; + + if (userHasCredits) { + return { + userId, + remainingCredits: userCredits.additionalCredits - credits, + creditType: CreditType.ADDITIONAL, + }; + } + + if (team) { + return { ...team, remainingCredits: team.availableCredits - credits }; + } return { userId,