Skip to content
105 changes: 52 additions & 53 deletions services/libs/data-access-layer/src/members/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,8 @@ export interface IMergeStrat {
targetOrganizationId(role: IMemberOrganization): string
}

type RoleToAdd = IMemberOrganization & { originalRoleIds?: string[] }

const MemberMergeStrat = (primaryMemberId: string): IMergeStrat => ({
entityIdField: EntityField.memberId,
intersectBasedOnField: EntityField.organizationId,
Expand All @@ -588,7 +590,7 @@ const MemberMergeStrat = (primaryMemberId: string): IMergeStrat => ({
return role.organizationId
},
worthMerging(a: IMemberOrganization, b: IMemberOrganization): boolean {
return a.memberId === b.memberId
return a.organizationId === b.organizationId
},
targetMemberId(): string {
return primaryMemberId
Expand All @@ -608,7 +610,7 @@ const OrgMergeStrat = (primaryOrganizationId: string): IMergeStrat => ({
return role.memberId
},
worthMerging(a: IMemberOrganization, b: IMemberOrganization): boolean {
return a.organizationId === b.organizationId
return a.memberId === b.memberId
},
targetMemberId(role: IMemberOrganization): string {
return role.memberId
Expand Down Expand Up @@ -928,15 +930,15 @@ export async function moveOrgsBetweenMembers(
function transformRoleToTargetEntity(
role: IMemberOrganization,
mergeStrat: IMergeStrat,
): IMemberOrganization & { originalRoleId?: string } {
): RoleToAdd {
return {
title: role.title,
dateStart: role.dateStart,
dateEnd: role.dateEnd,
memberId: mergeStrat.targetMemberId(role),
organizationId: mergeStrat.targetOrganizationId(role),
source: role.source,
originalRoleId: role.id,
originalRoleIds: role.id ? [role.id] : undefined,
}
}

Expand All @@ -947,13 +949,12 @@ function areDatesEqual(dateA: Date | string | null, dateB: Date | string | null)
}

function isSamePrimaryRole(a: IMemberOrganization, b: IMemberOrganization): boolean {
const isSameMember = a.memberId === b.memberId
const isSameOrganization = a.organizationId === b.organizationId
const isSameTitle = a.title === b.title
const hasSameStartDate = areDatesEqual(a.dateStart, b.dateStart)
const hasSameEndDate = areDatesEqual(a.dateEnd, b.dateEnd)

return isSameMember && isSameOrganization && isSameTitle && hasSameStartDate && hasSameEndDate
return (
a.memberId === b.memberId &&
a.organizationId === b.organizationId &&
areDatesEqual(a.dateStart, b.dateStart) &&
areDatesEqual(a.dateEnd, b.dateEnd)
)
}

export async function mergeRoles(
Expand All @@ -968,16 +969,23 @@ export async function mergeRoles(
let shouldRecalculateAffiliations = false
const allExistingOverrides = [...primaryAffiliationOverrides, ...secondaryAffiliationOverrides]
const removeRoles: IMemberOrganization[] = []
const addRoles: (IMemberOrganization & { originalRoleId?: string })[] = []
const addRoles: RoleToAdd[] = []
const affiliationOverridesToRecreate: {
role: IMemberOrganization
override: IMemberOrganizationAffiliationOverride
}[] = []
const queueRoleRemoval = (role: IMemberOrganization) => {
if (role.id && removeRoles.some((r) => r.id === role.id)) {
return
}

removeRoles.push(role)
}

// Phase 1: Analyze all secondary roles and build the complete plan
for (const memberOrganization of secondaryRoles) {
if (memberOrganization.dateStart === null && memberOrganization.dateEnd === null) {
removeRoles.push(memberOrganization)
queueRoleRemoval(memberOrganization)
} else if (memberOrganization.dateStart !== null && memberOrganization.dateEnd === null) {
const currentRoles = primaryRoles.filter(
(mo) =>
Expand All @@ -988,7 +996,7 @@ export async function mergeRoles(

if (currentRoles.length === 0) {
addRoles.push(transformRoleToTargetEntity(memberOrganization, mergeStrat))
removeRoles.push(memberOrganization)
queueRoleRemoval(memberOrganization)
} else if (currentRoles.length === 1) {
const currentRole = currentRoles[0]
if (new Date(memberOrganization.dateStart) <= new Date(currentRoles[0].dateStart)) {
Expand All @@ -1000,14 +1008,27 @@ export async function mergeRoles(
organizationId: currentRole.organizationId,
title: currentRole.title,
source: currentRole.source,
originalRoleIds: [currentRole.id, memberOrganization.id].filter(Boolean),
})
Comment thread
skwowet marked this conversation as resolved.
Outdated
Comment thread
skwowet marked this conversation as resolved.
Outdated

removeRoles.push(currentRole)
queueRoleRemoval(currentRole)
} else {
// Primary started earlier — keep it, but track secondary for override transfer.
// Insert is a no-op (ON CONFLICT), Phase 3 resolves via isSamePrimaryRole.
addRoles.push({
dateStart: toIsoString(currentRole.dateStart as Date | string),
dateEnd: null,
memberId: currentRole.memberId,
organizationId: currentRole.organizationId,
title: currentRole.title,
source: currentRole.source,
originalRoleIds: [currentRole.id, memberOrganization.id].filter(Boolean),
})
Comment thread
skwowet marked this conversation as resolved.
Outdated
Comment thread
skwowet marked this conversation as resolved.
}

removeRoles.push(memberOrganization)
queueRoleRemoval(memberOrganization)
Comment thread
skwowet marked this conversation as resolved.
Outdated
} else {
throw new Error(`Member ${memberOrganization.memberId} has more than one current roles.`)
throw new Error(`Member ${memberOrganization.memberId} has more than one current role.`)
}
} else if (memberOrganization.dateStart === null && memberOrganization.dateEnd !== null) {
throw new Error(`Member organization with dateEnd and without dateStart!`)
Expand All @@ -1019,13 +1040,15 @@ export async function mergeRoles(
const secondaryEnd = new Date(memberOrganization.dateEnd)

return (
mo.memberId === memberOrganization.memberId &&
mergeStrat.intersectBasedOn(mo) === mergeStrat.intersectBasedOn(memberOrganization) &&
Comment thread
skwowet marked this conversation as resolved.
Outdated
mo.dateStart !== null &&
mo.dateEnd !== null &&
((secondaryStart < primaryStart && secondaryEnd > primaryStart) ||
(primaryStart < secondaryStart && secondaryEnd < primaryEnd) ||
(secondaryStart < primaryStart && secondaryEnd > primaryEnd) ||
(primaryStart < secondaryStart && secondaryEnd > primaryEnd))
((secondaryStart <= primaryStart && secondaryEnd > primaryStart) ||
(primaryStart <= secondaryStart && secondaryEnd <= primaryEnd) ||
(secondaryStart <= primaryStart && secondaryEnd >= primaryEnd) ||
(primaryStart <= secondaryStart &&
secondaryStart < primaryEnd &&
secondaryEnd >= primaryEnd))
Comment thread
skwowet marked this conversation as resolved.
Outdated
Comment thread
skwowet marked this conversation as resolved.
Outdated
)
})

Expand All @@ -1049,11 +1072,15 @@ export async function mergeRoles(
foundIntersectingRoles.length > 0
? foundIntersectingRoles[0].source
: memberOrganization.source,
originalRoleIds: [...foundIntersectingRoles.map((r) => r.id), memberOrganization.id].filter(
Boolean,
Comment thread
skwowet marked this conversation as resolved.
Outdated
Comment thread
skwowet marked this conversation as resolved.
Outdated
),
})

for (const r of foundIntersectingRoles) {
removeRoles.push(r)
queueRoleRemoval(r)
}
queueRoleRemoval(memberOrganization)
}
}

Expand Down Expand Up @@ -1089,8 +1116,8 @@ export async function mergeRoles(

// 1. Gather all overrides relevant to this specific role merge
const relevantOverrides = affiliationOverridesToRecreate.filter((item) => {
return addRole.originalRoleId
? item.role.id === addRole.originalRoleId
return addRole.originalRoleIds?.length
? !!item.role.id && addRole.originalRoleIds.includes(item.role.id)
Comment thread
skwowet marked this conversation as resolved.
Outdated
: item.role.memberId === addRole.memberId && item.role.title === addRole.title
})

Expand All @@ -1104,34 +1131,6 @@ export async function mergeRoles(
const incomingSecondaries = relevantOverrides.filter((r) =>
secondaryAffiliationOverrides.some((s) => s.memberOrganizationId === r.role.id),
)
if (!newRoleId) {
// Use targetOrganizationId so the comparison works in both member-merge
// (org unchanged) and org-merge (secondary org -> primary org) contexts.
const directSecondaryRole = secondaryRoles.find((role) =>
secondaryAffiliationOverrides.some(
(override) =>
override.memberOrganizationId === role.id &&
mergeStrat.targetOrganizationId(role) === addRole.organizationId &&
role.title === addRole.title,
),
)
const directSecondaryOverride = directSecondaryRole
? secondaryAffiliationOverrides.find(
(override) => override.memberOrganizationId === directSecondaryRole.id,
)
: undefined

if (
directSecondaryRole &&
directSecondaryOverride &&
!incomingSecondaries.some((item) => item.role.id === directSecondaryRole.id)
) {
incomingSecondaries.push({
role: directSecondaryRole,
override: directSecondaryOverride,
})
}
}

// 2. Resolve "Primary Work Experience"
// Keep it if the primary had it, or if a secondary had it and no other primary role claims it.
Expand Down
Loading