Skip to content

Commit 71efa95

Browse files
committed
fix: improve handling of overlapping email-domain member organizations
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
1 parent 93c712d commit 71efa95

7 files changed

Lines changed: 85 additions & 56 deletions

File tree

backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export async function deleteMemberWorkExperience(req: Request, res: Response): P
4646

4747
const memberOrgIdsToDelete = [
4848
workExperienceId,
49-
...overlappingEmailDomainRows.map((row) => row.id as string),
49+
...overlappingEmailDomainRows.flatMap((row) => (row.id ? [row.id] : [])),
5050
]
5151

5252
// Delete hidden grouped rows with the visible row so read responses stay consistent

backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ export async function updateMemberWorkExperience(req: Request, res: Response): P
8686
await cleanSoftDeletedMemberOrganization(tx, memberId, data.organizationId, update)
8787
await updateMemberOrganization(tx, memberId, workExperienceId, update)
8888

89+
const overlapBasis = { ...existing, ...update }
90+
8991
const overlappingEmailDomainRows = getOverlappingEmailDomainMemberOrganizations(
9092
memberOrgs,
91-
existing,
93+
overlapBasis,
9294
)
9395

9496
const groupedUpdate: MemberOrganizationUpdate = {}
@@ -105,8 +107,10 @@ export async function updateMemberWorkExperience(req: Request, res: Response): P
105107
}
106108

107109
if (overlappingEmailDomainRows.length > 0 && Object.keys(groupedUpdate).length > 0) {
108-
for (const overlappingRow of overlappingEmailDomainRows) {
109-
await updateMemberOrganization(tx, memberId, overlappingRow.id as string, groupedUpdate)
110+
for (const overlappingRow of overlappingEmailDomainRows.filter(
111+
(row): row is typeof row & { id: string } => !!row.id,
112+
)) {
113+
await updateMemberOrganization(tx, memberId, overlappingRow.id, groupedUpdate)
110114
}
111115
}
112116
})

backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export async function verifyMemberWorkExperience(req: Request, res: Response): P
5959

6060
const memberOrgIdsToDelete = [
6161
workExperienceId,
62-
...overlappingEmailDomainRows.map((row) => row.id as string),
62+
...overlappingEmailDomainRows.flatMap((row) => (row.id ? [row.id] : [])),
6363
]
6464

6565
const verifiedUpdate = { verified, verifiedBy }
@@ -81,13 +81,10 @@ export async function verifyMemberWorkExperience(req: Request, res: Response): P
8181
verifiedUpdate,
8282
)
8383

84-
for (const overlappingRow of overlappingEmailDomainRows) {
85-
await updateMemberOrganization(
86-
tx,
87-
memberId,
88-
overlappingRow.id as string,
89-
verifiedUpdate,
90-
)
84+
for (const overlappingRow of overlappingEmailDomainRows.filter(
85+
(row): row is typeof row & { id: string } => !!row.id,
86+
)) {
87+
await updateMemberOrganization(tx, memberId, overlappingRow.id, verifiedUpdate)
9188
}
9289
} else {
9390
// Unverifying removes the grouped work experience from both visible and hidden rows

backend/src/services/member/memberAffiliationsService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export default class MemberAffiliationsService extends LoggerBase {
119119

120120
const memberOrgIds = [
121121
data.memberOrganizationId,
122-
...overlappingEmailDomainRows.map((row) => row.id as string),
122+
...overlappingEmailDomainRows.flatMap((row) => (row.id ? [row.id] : [])),
123123
]
124124

125125
// Apply the override to hidden grouped rows so the merged work experience has one decision

backend/src/services/member/memberOrganizationsService.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ export default class MemberOrganizationsService extends LoggerBase {
8787
memberOrganizations.map((mo) => mo.id),
8888
)
8989

90+
const overridesByMemberOrganizationId = new Map(
91+
affiliationOverrides.map((override) => [override.memberOrganizationId, override]),
92+
)
93+
9094
// Create mapping by id to speed up the processing
9195
const orgById: Record<string, IOrganizationSummary> = organizations.reduce(
9296
(obj: Record<string, IOrganizationSummary>, org) => ({
@@ -100,7 +104,7 @@ export default class MemberOrganizationsService extends LoggerBase {
100104
const groupedMemberOrganizations = groupMemberOrganizations(memberOrganizations)
101105

102106
const allOrganizations = groupedMemberOrganizations
103-
.filter((mo) => orgById[mo.organizationId]) // Only include non-deleted organizations
107+
.filter((mo): mo is typeof mo & { id: string } => !!mo.id && !!orgById[mo.organizationId])
104108
.map((mo) => {
105109
const overlappingEmailDomainRows = getOverlappingEmailDomainMemberOrganizations(
106110
memberOrganizations,
@@ -110,10 +114,9 @@ export default class MemberOrganizationsService extends LoggerBase {
110114
const relatedIds = [mo.id, ...overlappingEmailDomainRows.map((row) => row.id)]
111115

112116
const relatedOverrides = relatedIds.map((memberOrganizationId) =>
113-
affiliationOverrides.find(
114-
(override) => override.memberOrganizationId === memberOrganizationId,
115-
),
117+
overridesByMemberOrganizationId.get(memberOrganizationId),
116118
)
119+
117120
const resolvedOverrides = relatedOverrides.filter((override) => !!override)
118121

119122
// Merge override flags from rows that are displayed as one work experience
@@ -132,7 +135,7 @@ export default class MemberOrganizationsService extends LoggerBase {
132135
...mo,
133136
affiliationOverride: {
134137
memberId,
135-
memberOrganizationId: mo.id as string,
138+
memberOrganizationId: mo.id,
136139
allowAffiliation,
137140
isPrimaryWorkExperience,
138141
},
@@ -289,9 +292,11 @@ export default class MemberOrganizationsService extends LoggerBase {
289292

290293
const memberOrganizations = await fetchMemberOrganizations(qx, memberId)
291294

295+
const overlapBasis = { ...existing, ...update }
296+
292297
const overlappingEmailDomainRows = getOverlappingEmailDomainMemberOrganizations(
293298
memberOrganizations,
294-
existing,
299+
overlapBasis,
295300
)
296301

297302
const groupedUpdate = lodash.pickBy(
@@ -306,7 +311,11 @@ export default class MemberOrganizationsService extends LoggerBase {
306311

307312
if (overlappingEmailDomainRows.length > 0 && Object.keys(groupedUpdate).length > 0) {
308313
for (const overlappingRow of overlappingEmailDomainRows) {
309-
await updateMemberOrganization(qx, memberId, overlappingRow.id as string, groupedUpdate)
314+
if (!overlappingRow.id) {
315+
continue
316+
}
317+
318+
await updateMemberOrganization(qx, memberId, overlappingRow.id, groupedUpdate)
310319
}
311320
}
312321

@@ -353,7 +362,7 @@ export default class MemberOrganizationsService extends LoggerBase {
353362

354363
const memberOrganizationIdsToDelete = [
355364
id,
356-
...overlappingEmailDomainRows.map((row) => row.id as string),
365+
...overlappingEmailDomainRows.flatMap((row) => (row.id ? [row.id] : [])),
357366
]
358367

359368
// Delete hidden grouped rows with the visible row so list responses stay consistent

backend/src/utils/mapper.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,35 +39,44 @@ export function groupMemberOrganizations<T extends IMemberOrganization>(rows: T[
3939
const hiddenEmailDomainIds = new Set<string>()
4040
const displayGroups = new Map<string, { displayRow: T; groupedEmailDomainRows: T[] }>()
4141

42-
for (const emailDomainRow of emailDomainRows) {
42+
for (const emailDomainRow of emailDomainRows.filter(
43+
(row): row is T & { id: string } => !!row.id,
44+
)) {
4345
const overlappingNonEmailRows = nonEmailRows.filter((row) =>
4446
memberOrganizationsOverlap(emailDomainRow, row),
4547
)
4648

4749
if (overlappingNonEmailRows.length > 0) {
48-
const displayRow = [...overlappingNonEmailRows].sort(
49-
(a, b) =>
50-
getMemberOrganizationSourceRank(a.source) - getMemberOrganizationSourceRank(b.source),
51-
)[0]
52-
53-
hiddenEmailDomainIds.add(emailDomainRow.id as string)
54-
55-
const existingGroup = displayGroups.get(displayRow.id as string)
56-
if (existingGroup) {
57-
existingGroup.groupedEmailDomainRows.push(emailDomainRow)
58-
} else {
59-
displayGroups.set(displayRow.id as string, {
60-
displayRow,
61-
groupedEmailDomainRows: [emailDomainRow],
62-
})
50+
const displayRow = [...overlappingNonEmailRows].sort((a, b) => {
51+
const rankDiff =
52+
getMemberOrganizationSourceRank(a.source) - getMemberOrganizationSourceRank(b.source)
53+
if (rankDiff !== 0) {
54+
return rankDiff
55+
}
56+
57+
return (a.id ?? '').localeCompare(b.id ?? '')
58+
})[0]
59+
60+
if (displayRow.id) {
61+
hiddenEmailDomainIds.add(displayRow.id)
62+
63+
const existingGroup = displayGroups.get(displayRow.id)
64+
if (existingGroup) {
65+
existingGroup.groupedEmailDomainRows.push(emailDomainRow)
66+
} else {
67+
displayGroups.set(displayRow.id, {
68+
displayRow,
69+
groupedEmailDomainRows: [emailDomainRow],
70+
})
71+
}
6372
}
6473
}
6574
}
6675

6776
return rows
68-
.filter((row) => !hiddenEmailDomainIds.has(row.id as string))
77+
.filter((row): row is T & { id: string } => !!row.id && !hiddenEmailDomainIds.has(row.id))
6978
.map((row) => {
70-
const group = displayGroups.get(row.id as string)
79+
const group = displayGroups.get(row.id)
7180
if (!group) {
7281
return row
7382
}
@@ -96,9 +105,8 @@ export function groupMemberOrganizations<T extends IMemberOrganization>(rows: T[
96105
if (normalizedEnds.some((date) => date === null)) {
97106
dateEnd = null
98107
} else if (normalizedEnds.length > 0) {
99-
dateEnd = normalizedEnds.reduce((max, date) =>
100-
(date as string) > max ? (date as string) : max,
101-
)
108+
const datedEnds = normalizedEnds.filter((date): date is string => date !== null)
109+
dateEnd = datedEnds.reduce((max, date) => (date > max ? date : max))
102110
}
103111

104112
return {

services/libs/common_services/src/services/member-organization.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,9 @@ export function inferMemberOrganizationStintChanges(
176176
const activeRows = normalizedRows.filter((row) => !row.deletedAt)
177177

178178
// Deleted dated rows suppress recreation for dates the user removed
179-
const deletedRows = normalizedRows.filter((row) => row.deletedAt && row.dateStart)
179+
const deletedRows = normalizedRows.filter(
180+
(row): row is typeof row & { dateStart: string } => !!row.deletedAt && !!row.dateStart,
181+
)
180182

181183
const sortedDates = orgDates
182184
.map((entry) => ({
@@ -201,7 +203,7 @@ export function inferMemberOrganizationStintChanges(
201203
deletedRows.some(
202204
(row) =>
203205
row.organizationId === organizationId &&
204-
dateIntersects(row.dateStart as string, row.dateEnd, targetDate, targetDate),
206+
dateIntersects(row.dateStart, row.dateEnd, targetDate, targetDate),
205207
)
206208
) {
207209
continue
@@ -299,17 +301,26 @@ export function inferMemberOrganizationStintChanges(
299301
}
300302

301303
// 7. Map only modified or new stints back to change objects
302-
return stints
303-
.filter((s) => s.isDirty && s.dateStart && s.dateEnd)
304-
.map((s): MemberOrgStintChange => {
305-
const payload = {
306-
memberId,
307-
organizationId: s.organizationId,
308-
dateStart: s.dateStart as string,
309-
dateEnd: s.dateEnd as string,
310-
}
304+
return stints.flatMap((s): MemberOrgStintChange[] => {
305+
if (!s.isDirty || !s.dateStart || !s.dateEnd) {
306+
return []
307+
}
311308

312-
if (s.isNew) return { type: 'insert', ...payload }
313-
return { type: 'update', id: s.id as string, ...payload }
314-
})
309+
const payload = {
310+
memberId,
311+
organizationId: s.organizationId,
312+
dateStart: s.dateStart,
313+
dateEnd: s.dateEnd,
314+
}
315+
316+
if (s.isNew) {
317+
return [{ type: 'insert', ...payload }]
318+
}
319+
320+
if (!s.id) {
321+
return []
322+
}
323+
324+
return [{ type: 'update', id: s.id, ...payload }]
325+
})
315326
}

0 commit comments

Comments
 (0)