fix: move team credits to org on upgarde (#24386)

* move credits from team to org

* add test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2025-10-13 09:30:22 +00:00
1 parent b881e025f6
commit 3bcaf0ec22
3 files changed
+116 -1

No files matched your search

@@ -689,4 +689,65 @@ export class CreditService {
totalCreditsUsedThisMonth,
};
}
async moveCreditsFromTeamToOrg({ teamId, orgId }: { teamId: number; orgId: number }) {
return await prisma.$transaction(async (tx) => {
// Get team's credit balance
const teamCreditBalance = await CreditsRepository.findCreditBalance({ teamId }, tx);
if (!teamCreditBalance || teamCreditBalance.additionalCredits <= 0) {
log.info("No credits to transfer from team to org", { teamId, orgId });
return;
}
// Get or create org's credit balance
let orgCreditBalance = await CreditsRepository.findCreditBalance({ teamId: orgId }, tx);
if (!orgCreditBalance) {
orgCreditBalance = await CreditsRepository.createCreditBalance(
{
teamId: orgId,
},
tx
);
}
const creditsToTransfer = teamCreditBalance.additionalCredits;
// Transfer credits from team to org
await CreditsRepository.updateCreditBalance(
{
teamId,
data: {
additionalCredits: 0,
},
},
tx
);
await CreditsRepository.updateCreditBalance(
{
teamId: orgId,
data: {
additionalCredits: {
increment: creditsToTransfer,
},
},
},
tx
);
log.info("Successfully transferred credits from team to org", {
teamId,
orgId,
creditsTransferred: creditsToTransfer,
});
return {
creditsTransferred: creditsToTransfer,
teamId,
orgId,
};
});
}
}
@@ -311,7 +311,6 @@ describe("createTeams handler", () => {
// Verify platform team was not moved
const platformTeam = await prismock.team.findFirst({
where: { name: "Platform Team" },
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
include: { parent: true },
});
@@ -423,4 +422,55 @@ describe("createTeams handler", () => {
const redirectsWithNullFrom = redirects.filter((redirect) => redirect.from === null);
expect(redirectsWithNullFrom).toHaveLength(0);
});
it("should transfer credits from team to organization when moving team", async () => {
const { owner, organization, teams } = await createScenario({
teams: [{ name: "Team With Credits", slug: "team-with-credits", addToParentId: null }],
});
const teamToMove = teams[0];
// Add credits to the team before moving
await prismock.creditBalance.create({
data: {
teamId: teamToMove.id,
additionalCredits: 500,
limitReachedAt: null,
warningSentAt: null,
},
});
const result = await createTeamsHandler({
ctx: {
user: {
id: owner.id,
organizationId: organization.id,
},
},
input: {
teamNames: [],
orgId: organization.id,
moveTeams: [
{
id: teamToMove.id,
shouldMove: true,
newSlug: "moved-team-with-credits",
},
],
creationSource: CreationSource.WEBAPP,
},
});
expect(result).toEqual({ duplicatedSlugs: [] });
const teamCreditBalance = await prismock.creditBalance.findFirst({
where: { teamId: teamToMove.id },
});
expect(teamCreditBalance?.additionalCredits).toBe(0);
const orgCreditBalance = await prismock.creditBalance.findFirst({
where: { teamId: organization.id },
});
expect(orgCreditBalance?.additionalCredits).toBe(500);
});
});
@@ -1,4 +1,5 @@
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import logger from "@calcom/lib/logger";
@@ -230,6 +231,9 @@ async function moveTeam({
parentId: org.id,
},
});
const creditService = new CreditService();
await creditService.moveCreditsFromTeamToOrg({ teamId, orgId: org.id });
} catch (error) {
log.error(
"Error while moving team to organization",