From 1a68dbb3045a69053d97ba524f4e819740558643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 20 Mar 2026 14:23:31 +0100 Subject: [PATCH 1/4] chore: cleanup project affiliations after the last member organizations row is deleted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../deleteMemberWorkExperience.ts | 2 ++ .../src/members/projectAffiliations.ts | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts index 67ca87e14e..dca286c684 100644 --- a/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts @@ -6,6 +6,7 @@ import { NotFoundError } from '@crowd/common' import { CommonMemberService } from '@crowd/common_services' import { MemberField, + cleanupMemberSegmentAffiliationsForOrg, deleteMemberOrganizations, fetchManyMemberOrgsWithOrgData, findMemberById, @@ -46,6 +47,7 @@ export async function deleteMemberWorkExperience(req: Request, res: Response): P await qx.tx(async (tx) => { await deleteMemberOrganizations(tx, memberId, [workExperienceId]) + await cleanupMemberSegmentAffiliationsForOrg(tx, memberId, memberOrg.organizationId) const commonMemberService = new CommonMemberService(tx, req.temporal, req.log) await commonMemberService.startAffiliationRecalculation(memberId, [ diff --git a/services/libs/data-access-layer/src/members/projectAffiliations.ts b/services/libs/data-access-layer/src/members/projectAffiliations.ts index ac3288260a..3b657eb8db 100644 --- a/services/libs/data-access-layer/src/members/projectAffiliations.ts +++ b/services/libs/data-access-layer/src/members/projectAffiliations.ts @@ -144,6 +144,31 @@ export async function deleteAllMemberSegmentAffiliationsForProject( ) } +/** + * Delete all segment affiliations for a member + organization combination, + * but only if no non-deleted work experiences remain for that org. + */ +export async function cleanupMemberSegmentAffiliationsForOrg( + qx: QueryExecutor, + memberId: string, + organizationId: string, +): Promise { + await qx.result( + ` + DELETE FROM "memberSegmentAffiliations" + WHERE "memberId" = $(memberId) + AND "organizationId" = $(organizationId) + AND NOT EXISTS ( + SELECT 1 FROM "memberOrganizations" + WHERE "memberId" = $(memberId) + AND "organizationId" = $(organizationId) + AND "deletedAt" IS NULL + ) + `, + { memberId, organizationId }, + ) +} + /** * Insert multiple segment affiliations for a member + project (segment) combination. * All inserted affiliations are marked as verified. From 535a7c2a03483b208e6420ee8ca2462b3ec85db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 20 Mar 2026 14:39:55 +0100 Subject: [PATCH 2/4] chore: refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../deleteMemberWorkExperience.ts | 3 --- .../src/members/organizations.ts | 21 ++++++++++++++-- .../src/members/projectAffiliations.ts | 25 ------------------- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts index dca286c684..1b6305b3a8 100644 --- a/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts @@ -6,7 +6,6 @@ import { NotFoundError } from '@crowd/common' import { CommonMemberService } from '@crowd/common_services' import { MemberField, - cleanupMemberSegmentAffiliationsForOrg, deleteMemberOrganizations, fetchManyMemberOrgsWithOrgData, findMemberById, @@ -47,8 +46,6 @@ export async function deleteMemberWorkExperience(req: Request, res: Response): P await qx.tx(async (tx) => { await deleteMemberOrganizations(tx, memberId, [workExperienceId]) - await cleanupMemberSegmentAffiliationsForOrg(tx, memberId, memberOrg.organizationId) - const commonMemberService = new CommonMemberService(tx, req.temporal, req.log) await commonMemberService.startAffiliationRecalculation(memberId, [ memberOrg.organizationId, diff --git a/services/libs/data-access-layer/src/members/organizations.ts b/services/libs/data-access-layer/src/members/organizations.ts index 636bb87068..07eee9388b 100644 --- a/services/libs/data-access-layer/src/members/organizations.ts +++ b/services/libs/data-access-layer/src/members/organizations.ts @@ -337,9 +337,9 @@ export async function deleteMemberOrganizations( await qx.tx(async (tx) => { // First delete from memberOrganizationAffiliationOverrides using the same conditions await tx.result( - `DELETE FROM "memberOrganizationAffiliationOverrides" + `DELETE FROM "memberOrganizationAffiliationOverrides" WHERE "memberOrganizationId" IN ( - SELECT "id" FROM "memberOrganizations" + SELECT "id" FROM "memberOrganizations" WHERE ${whereClause} )`, params, @@ -347,6 +347,23 @@ export async function deleteMemberOrganizations( // Then perform the soft/hard delete on memberOrganizations await tx.result(query, params) + + // Clean up segment affiliation overrides for orgs that no longer have any active work experiences + await tx.result( + `DELETE FROM "memberSegmentAffiliations" msa + WHERE msa."memberId" = $(memberId) + AND msa."organizationId" IN ( + SELECT DISTINCT "organizationId" FROM "memberOrganizations" + WHERE ${whereClause} + ) + AND NOT EXISTS ( + SELECT 1 FROM "memberOrganizations" mo + WHERE mo."memberId" = $(memberId) + AND mo."organizationId" = msa."organizationId" + AND mo."deletedAt" IS NULL + )`, + params, + ) }) } diff --git a/services/libs/data-access-layer/src/members/projectAffiliations.ts b/services/libs/data-access-layer/src/members/projectAffiliations.ts index 3b657eb8db..ac3288260a 100644 --- a/services/libs/data-access-layer/src/members/projectAffiliations.ts +++ b/services/libs/data-access-layer/src/members/projectAffiliations.ts @@ -144,31 +144,6 @@ export async function deleteAllMemberSegmentAffiliationsForProject( ) } -/** - * Delete all segment affiliations for a member + organization combination, - * but only if no non-deleted work experiences remain for that org. - */ -export async function cleanupMemberSegmentAffiliationsForOrg( - qx: QueryExecutor, - memberId: string, - organizationId: string, -): Promise { - await qx.result( - ` - DELETE FROM "memberSegmentAffiliations" - WHERE "memberId" = $(memberId) - AND "organizationId" = $(organizationId) - AND NOT EXISTS ( - SELECT 1 FROM "memberOrganizations" - WHERE "memberId" = $(memberId) - AND "organizationId" = $(organizationId) - AND "deletedAt" IS NULL - ) - `, - { memberId, organizationId }, - ) -} - /** * Insert multiple segment affiliations for a member + project (segment) combination. * All inserted affiliations are marked as verified. From 82c1518f9c1163cfdf40aa06a8eee2bf5a1eb7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 20 Mar 2026 15:21:10 +0100 Subject: [PATCH 3/4] chore: fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/members/organizations.ts | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/services/libs/data-access-layer/src/members/organizations.ts b/services/libs/data-access-layer/src/members/organizations.ts index 07eee9388b..83c150ca89 100644 --- a/services/libs/data-access-layer/src/members/organizations.ts +++ b/services/libs/data-access-layer/src/members/organizations.ts @@ -335,6 +335,14 @@ export async function deleteMemberOrganizations( const query = `${baseQuery} WHERE ${whereClause};` await qx.tx(async (tx) => { + // Capture affected org IDs before the delete — needed for the cleanup step below, + // since a hard delete removes rows before we can look them up. + const affectedOrgs: { organizationId: string }[] = await tx.select( + `SELECT DISTINCT "organizationId" FROM "memberOrganizations" WHERE ${whereClause}`, + params, + ) + const affectedOrgIds = affectedOrgs.map((r) => r.organizationId) + // First delete from memberOrganizationAffiliationOverrides using the same conditions await tx.result( `DELETE FROM "memberOrganizationAffiliationOverrides" @@ -348,22 +356,21 @@ export async function deleteMemberOrganizations( // Then perform the soft/hard delete on memberOrganizations await tx.result(query, params) - // Clean up segment affiliation overrides for orgs that no longer have any active work experiences - await tx.result( - `DELETE FROM "memberSegmentAffiliations" msa - WHERE msa."memberId" = $(memberId) - AND msa."organizationId" IN ( - SELECT DISTINCT "organizationId" FROM "memberOrganizations" - WHERE ${whereClause} - ) - AND NOT EXISTS ( - SELECT 1 FROM "memberOrganizations" mo - WHERE mo."memberId" = $(memberId) - AND mo."organizationId" = msa."organizationId" - AND mo."deletedAt" IS NULL - )`, - params, - ) + // Clean up segment affiliations for orgs that no longer have any active work experiences + if (affectedOrgIds.length > 0) { + await tx.result( + `DELETE FROM "memberSegmentAffiliations" msa + WHERE msa."memberId" = $(memberId) + AND msa."organizationId" IN ($(orgIds:csv)) + AND NOT EXISTS ( + SELECT 1 FROM "memberOrganizations" mo + WHERE mo."memberId" = $(memberId) + AND mo."organizationId" = msa."organizationId" + AND mo."deletedAt" IS NULL + )`, + { memberId, orgIds: affectedOrgIds }, + ) + } }) } From 3c4a7f75ca91c71205e45abccc0e249bea1ccce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Fri, 20 Mar 2026 15:24:45 +0100 Subject: [PATCH 4/4] chore: soft delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../apps/profiles_worker/src/activities/member/botSuggestion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/profiles_worker/src/activities/member/botSuggestion.ts b/services/apps/profiles_worker/src/activities/member/botSuggestion.ts index 7e75044e3e..bde90b3475 100644 --- a/services/apps/profiles_worker/src/activities/member/botSuggestion.ts +++ b/services/apps/profiles_worker/src/activities/member/botSuggestion.ts @@ -84,7 +84,7 @@ export async function updateMemberAttributes( export async function removeMemberOrganizations(memberId: string): Promise { try { const qx = pgpQx(svc.postgres.writer.connection()) - await deleteMemberOrganizations(qx, memberId, undefined, false) + await deleteMemberOrganizations(qx, memberId, undefined, true) } catch (error) { svc.log.error({ error, memberId }, `Failed to remove member organizations!`) throw error