Merge branch 'main' into fix/cc-recipient-order-last

This commit is contained in:
Catalin Pit
2026-06-09 17:02:54 +03:00
committed by GitHub
52 changed files with 1628 additions and 954 deletions
@@ -0,0 +1,55 @@
import { prisma } from '@documenso/prisma';
import { ORGANISATION_USER_ACCOUNT_TYPE } from '../../constants/organisations';
import { jobs } from '../../jobs/client';
import { orphanEnvelopes } from '../envelope/orphan-envelopes';
export type DeleteOrganisationOptions = {
organisation: {
id: string;
teams: { id: number }[];
subscription: { planId: string } | null;
};
};
/**
* Fully tears down an organisation:
*
* 1. Orphans every team's envelopes (so foreign key constraints don't block the delete).
* 2. Removes the organisation's account rows and the organisation itself in a transaction.
* 3. Schedules the Stripe subscription to be cancelled at the end of the billing period
* (when one exists). The job runs asynchronously so a Stripe outage doesn't block the
* delete, and is retried by the job runner if Stripe is temporarily unavailable.
*
* Authorization must be handled by the caller. This is the shared implementation used by
* the organisation delete route, the admin delete-organisation job, and account deletion.
*/
export const deleteOrganisation = async ({ organisation }: DeleteOrganisationOptions) => {
// Orphan all envelopes to get rid of foreign key constraints.
await Promise.all(organisation.teams.map(async (team) => orphanEnvelopes({ teamId: team.id })));
await prisma.$transaction(async (tx) => {
await tx.account.deleteMany({
where: {
type: ORGANISATION_USER_ACCOUNT_TYPE,
provider: organisation.id,
},
});
await tx.organisation.delete({
where: {
id: organisation.id,
},
});
});
if (organisation.subscription) {
await jobs.triggerJob({
name: 'internal.cancel-organisation-subscription',
payload: {
stripeSubscriptionId: organisation.subscription.planId,
organisationId: organisation.id,
},
});
}
};
+13 -6
View File
@@ -1,7 +1,7 @@
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { orphanEnvelopes } from '../envelope/orphan-envelopes';
import { deleteOrganisation } from '../organisation/delete-organisation';
export type DeleteUserOptions = {
id: number;
@@ -20,6 +20,11 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
id: true,
},
},
subscription: {
select: {
planId: true,
},
},
},
},
organisationMember: {
@@ -44,9 +49,6 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
});
}
// Get team IDs from organisations the user owns.
const ownedTeamIds = user.ownedOrganisations.flatMap((org) => org.teams.map((team) => team.id));
// Get team IDs from organisations the user is a member of (but not owner).
const memberTeams = user.organisationMember
.filter((member) => member.organisation.ownerUserId !== user.id)
@@ -57,8 +59,13 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
})),
);
// For teams where user is the org owner - orphan their envelopes.
await Promise.all(ownedTeamIds.map(async (teamId) => orphanEnvelopes({ teamId })));
// For organisations the user owns - fully tear them down (orphan envelopes,
// delete the organisation, and cancel any Stripe subscription). Without this
// the organisations would only cascade away when the user row is deleted,
// leaving their subscriptions billing and account rows behind.
for (const organisation of user.ownedOrganisations) {
await deleteOrganisation({ organisation });
}
// For teams where user is a member (not owner) - transfer envelopes to team owner.
await Promise.all(