From f0b8a594cf95dd58ac56f1ee94520182a4bc73df Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 15 Oct 2026 11:47:33 -0400 Subject: [PATCH] Fix deleting orgs --- server/lib/deleteOrg.ts | 22 ++++++++++++++++++++-- server/routers/auth/deleteMyAccount.ts | 9 ++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/server/lib/deleteOrg.ts b/server/lib/deleteOrg.ts index 131214aab..e396a56ee 100644 --- a/server/lib/deleteOrg.ts +++ b/server/lib/deleteOrg.ts @@ -70,6 +70,10 @@ export async function deleteOrgById( let siteCount: number | null = null; let userCount: number | null = null; let remoteExitNodeCount: number | null = null; + // billingOrgId has no FK, so the billing org may already be gone (e.g. + // a partially failed account deletion). Writing usage for it would + // violate the usage FK and abort the transaction. + let billingOrgExists = false; await db.transaction(async (trx) => { for (const site of orgSites) { @@ -161,7 +165,17 @@ export async function deleteOrgById( .where(inArray(domains.domainId, domainIdsToDelete)); } - await usageService.add(orgId, LimitId.ORGANIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here + const [billingOrg] = org.billingOrgId + ? await trx + .select({ orgId: orgs.orgId }) + .from(orgs) + .where(eq(orgs.orgId, org.billingOrgId)) + .limit(1) + : [org]; + billingOrgExists = !!billingOrg; + if (billingOrgExists) { + await usageService.add(orgId, LimitId.ORGANIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here + } await trx.delete(orgs).where(eq(orgs.orgId, orgId)); @@ -205,7 +219,11 @@ export async function deleteOrgById( } }); - if (org.billingOrgId) { + if ( + org.billingOrgId && + org.billingOrgId !== orgId && + billingOrgExists + ) { usageService.updateCount( org.billingOrgId, LimitId.DOMAINS, diff --git a/server/routers/auth/deleteMyAccount.ts b/server/routers/auth/deleteMyAccount.ts index 0a1f74f3e..5892e4ada 100644 --- a/server/routers/auth/deleteMyAccount.ts +++ b/server/routers/auth/deleteMyAccount.ts @@ -182,7 +182,14 @@ export async function deleteMyAccount( const allDeletedNewtIds: string[] = []; const allOlmsToTerminate: string[] = []; - for (const row of ownedOrgsRows) { + // Delete billing orgs last: deleting a child org decrements usage on + // its billing org, which fails (and aborts the transaction) if the + // billing org is already gone. + const orgsInDeleteOrder = [...ownedOrgsRows].sort( + (a, b) => Number(!!a.isBillingOrg) - Number(!!b.isBillingOrg) + ); + + for (const row of orgsInDeleteOrder) { try { const result = await deleteOrgById(row.orgId); allDeletedNewtIds.push(...result.deletedNewtIds);