All Merges Complete
-
Successfully merged all organizations. ${totalMoved} records moved in this merge.
+
Successfully merged: ${summaryText}.
diff --git a/server/src/addie/mcp/admin-tools.ts b/server/src/addie/mcp/admin-tools.ts
index 2cadacea3e..c8395928f4 100644
--- a/server/src/addie/mcp/admin-tools.ts
+++ b/server/src/addie/mcp/admin-tools.ts
@@ -3882,6 +3882,7 @@ Use add_committee_leader to assign a leader.`;
primaryOrgId,
secondaryOrgId,
mergedBy,
+ workos,
stripeCustomerResolution ? { stripeCustomerResolution } : undefined
);
diff --git a/server/src/db/org-merge-db.ts b/server/src/db/org-merge-db.ts
index 20ead8c497..46fcaa5e05 100644
--- a/server/src/db/org-merge-db.ts
+++ b/server/src/db/org-merge-db.ts
@@ -12,6 +12,7 @@
import { getPool } from './client.js';
import { createLogger } from '../logger.js';
+import { WorkOS } from '@workos-inc/node';
const logger = createLogger('org-merge-db');
@@ -28,6 +29,7 @@ export interface MergeSummary {
prospect_notes_merged: boolean;
enrichment_data_preserved: boolean;
stripe_customer_action: 'kept_primary' | 'moved_from_secondary' | 'none' | 'conflict_unresolved' | null;
+ workos_org_deleted: boolean;
warnings: string[];
}
@@ -39,6 +41,7 @@ export type StripeCustomerResolution = 'keep_primary' | 'use_secondary' | 'keep_
* @param primaryOrgId - The organization to keep (merge into)
* @param secondaryOrgId - The organization to remove (merge from)
* @param mergedBy - WorkOS user ID of person initiating the merge
+ * @param workos - WorkOS client instance for deleting the secondary org
* @param options.stripeCustomerResolution - How to handle stripe_customer_id conflict
* @returns Summary of the merge operation
*/
@@ -46,6 +49,7 @@ export async function mergeOrganizations(
primaryOrgId: string,
secondaryOrgId: string,
mergedBy: string,
+ workos: WorkOS,
options?: {
stripeCustomerResolution?: StripeCustomerResolution;
}
@@ -62,6 +66,7 @@ export async function mergeOrganizations(
prospect_notes_merged: false,
enrichment_data_preserved: false,
stripe_customer_action: null,
+ workos_org_deleted: false,
warnings: [],
};
@@ -97,9 +102,10 @@ export async function mergeOrganizations(
throw new Error('Could not load organization details');
}
- // Block merging personal workspaces
- if (primaryOrg.is_personal || secondaryOrg.is_personal) {
- throw new Error('Cannot merge personal workspaces. Personal workspaces represent individual users and should not be merged with company organizations.');
+ // Block merging a company INTO a personal workspace (would lose company identity)
+ // But allow: personal INTO company, personal INTO personal
+ if (primaryOrg.is_personal && !secondaryOrg.is_personal) {
+ throw new Error('Cannot merge a company organization into a personal workspace. The company would lose its identity. Instead, merge the personal workspace into the company.');
}
logger.info(
@@ -688,7 +694,7 @@ export async function mergeOrganizations(
);
// =====================================================
- // 24. Delete the secondary organization
+ // 24. Delete the secondary organization from local DB
// =====================================================
await client.query(
`DELETE FROM organizations WHERE workos_organization_id = $1`,
@@ -698,11 +704,31 @@ export async function mergeOrganizations(
// Commit transaction
await client.query('COMMIT');
+ // =====================================================
+ // 25. Delete the secondary organization from WorkOS
+ // =====================================================
+ // This is done after commit so local changes persist even if WorkOS fails
+ try {
+ await workos.organizations.deleteOrganization(secondaryOrgId);
+ summary.workos_org_deleted = true;
+ logger.info({ secondaryOrgId }, 'Deleted secondary organization from WorkOS');
+ } catch (workosError) {
+ // Log but don't fail - local merge succeeded
+ logger.error(
+ { error: workosError, secondaryOrgId },
+ 'Failed to delete secondary organization from WorkOS - manual cleanup may be required'
+ );
+ summary.warnings.push(
+ 'Failed to delete secondary organization from WorkOS. The organization may need to be manually deleted in WorkOS Dashboard.'
+ );
+ }
+
logger.info(
{
primaryOrgId,
secondaryOrgId,
totalMoved: summary.tables_merged.reduce((sum, t) => sum + t.rows_moved, 0),
+ workosDeleted: summary.workos_org_deleted,
},
'Organization merge completed successfully'
);
diff --git a/server/src/routes/admin/cleanup.ts b/server/src/routes/admin/cleanup.ts
index ac410735de..ba0890b79c 100644
--- a/server/src/routes/admin/cleanup.ts
+++ b/server/src/routes/admin/cleanup.ts
@@ -13,6 +13,7 @@ import {
import { isLushaConfigured } from "../../services/lusha.js";
import { mergeOrganizations, previewMerge, StripeCustomerResolution } from "../../db/org-merge-db.js";
import { getPool } from "../../db/client.js";
+import { workos } from "../../auth/workos-client.js";
const logger = createLogger("admin-cleanup");
@@ -303,6 +304,7 @@ export function setupCleanupRoutes(apiRouter: Router): void {
primary_org_id,
secondary_org_id,
userId,
+ workos,
stripe_customer_resolution ? { stripeCustomerResolution: stripe_customer_resolution } : undefined
);
diff --git a/server/src/routes/organizations.ts b/server/src/routes/organizations.ts
index c812873159..706b6e34c4 100644
--- a/server/src/routes/organizations.ts
+++ b/server/src/routes/organizations.ts
@@ -620,21 +620,27 @@ export function createOrganizationsRouter(): Router {
const domains = domainsResult.rows.map(r => r.domain.toLowerCase());
- // Get current org members' emails
- const allMemberships = await workos!.userManagement.listOrganizationMemberships({
- organizationId: orgId,
- });
+ // Get current org members' emails (paginate to get all)
const memberEmails = new Set
();
- for (const membership of allMemberships.data) {
- try {
- const memberUser = await workos!.userManagement.getUser(membership.userId);
- if (memberUser.email) {
- memberEmails.add(memberUser.email.toLowerCase());
+ let after: string | undefined;
+ do {
+ const membershipsPage = await workos!.userManagement.listOrganizationMemberships({
+ organizationId: orgId,
+ after,
+ limit: 100,
+ });
+ for (const membership of membershipsPage.data) {
+ try {
+ const memberUser = await workos!.userManagement.getUser(membership.userId);
+ if (memberUser.email) {
+ memberEmails.add(memberUser.email.toLowerCase());
+ }
+ } catch {
+ // Skip if can't fetch user
}
- } catch {
- // Skip if can't fetch user
}
- }
+ after = membershipsPage.listMetadata?.after ?? undefined;
+ } while (after)
// Get pending join request emails for this org
const joinRequestsResult = await pool.query(