import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { createOrganisationClaimUpsertData } from '@documenso/lib/server-only/organisation/create-organisation'; import { getSubscriptionClaim } from '@documenso/lib/server-only/subscription/get-subscription-claim'; import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription'; import { prisma } from '@documenso/prisma'; import { SubscriptionStatus } from '@prisma/client'; import { adminProcedure } from '../trpc'; import { ZSwapOrganisationSubscriptionRequestSchema, ZSwapOrganisationSubscriptionResponseSchema, } from './swap-organisation-subscription.types'; export const swapOrganisationSubscriptionRoute = adminProcedure .input(ZSwapOrganisationSubscriptionRequestSchema) .output(ZSwapOrganisationSubscriptionResponseSchema) .mutation(async ({ input, ctx }) => { const { sourceOrganisationId, targetOrganisationId } = input; ctx.logger.info({ input: { sourceOrganisationId, targetOrganisationId, }, }); if (sourceOrganisationId === targetOrganisationId) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Source and target organisations must be different', }); } const sourceOrg = await prisma.organisation.findUnique({ where: { id: sourceOrganisationId }, include: { subscription: true, organisationClaim: true, }, }); if (!sourceOrg) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Source organisation not found', }); } if ( !sourceOrg.subscription || (sourceOrg.subscription.status !== SubscriptionStatus.ACTIVE && sourceOrg.subscription.status !== SubscriptionStatus.PAST_DUE) ) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Source organisation does not have an active subscription', }); } const targetOrg = await prisma.organisation.findUnique({ where: { id: targetOrganisationId }, include: { subscription: true, organisationClaim: true, }, }); if (!targetOrg) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Target organisation not found', }); } if (sourceOrg.ownerUserId !== targetOrg.ownerUserId) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Both organisations must be owned by the same user', }); } if ( targetOrg.subscription && (targetOrg.subscription.status === SubscriptionStatus.ACTIVE || targetOrg.subscription.status === SubscriptionStatus.PAST_DUE) ) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Target organisation already has an active subscription', }); } const customerId = sourceOrg.customerId ?? sourceOrg.subscription.customerId; const freeSubscriptionClaim = await getSubscriptionClaim(INTERNAL_CLAIM_ID.FREE); await prisma.$transaction(async (tx) => { // Delete stale INACTIVE subscription on target if present. if (targetOrg.subscription) { await tx.subscription.delete({ where: { id: targetOrg.subscription.id }, }); } // Clear customerId on source org to avoid unique constraint violation. await tx.organisation.update({ where: { id: sourceOrganisationId }, data: { customerId: null }, }); // Set customerId on target org. await tx.organisation.update({ where: { id: targetOrganisationId }, data: { customerId }, }); // Move the subscription record to the target org. await tx.subscription.update({ where: { id: sourceOrg.subscription!.id }, data: { organisationId: targetOrganisationId }, }); // Copy source org's claim entitlements to target org's claim. if (sourceOrg.organisationClaim && targetOrg.organisationClaim) { await tx.organisationClaim.update({ where: { id: targetOrg.organisationClaim.id }, data: { originalSubscriptionClaimId: sourceOrg.organisationClaim.originalSubscriptionClaimId, teamCount: sourceOrg.organisationClaim.teamCount, memberCount: sourceOrg.organisationClaim.memberCount, envelopeItemCount: sourceOrg.organisationClaim.envelopeItemCount, recipientCount: sourceOrg.organisationClaim.recipientCount, flags: sourceOrg.organisationClaim.flags, }, }); } // Reset source org's claim to FREE. if (sourceOrg.organisationClaim) { await tx.organisationClaim.update({ where: { id: sourceOrg.organisationClaim.id }, data: { originalSubscriptionClaimId: INTERNAL_CLAIM_ID.FREE, ...createOrganisationClaimUpsertData(freeSubscriptionClaim), }, }); } }); });