Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions services/libs/common/src/member.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import merge from 'lodash.merge'
import ldSum from 'lodash.sum'

import { OrganizationSource } from '@crowd/types'

/* eslint-disable @typescript-eslint/no-explicit-any */

export async function setAttributesDefaultValues(
Expand Down Expand Up @@ -79,3 +81,10 @@ export const calculateReach = (oldReach: any, newReach: any): { total: number }
out.total = ldSum(Object.values(out))
return out
}

export function getMemberOrganizationSourceRank(source: string | null | undefined): number {
if (source === OrganizationSource.UI) return 0
if (source === OrganizationSource.EMAIL_DOMAIN) return 1
if (source?.startsWith('enrichment-')) return 2
return 3
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,92 @@ describe('selectPrimaryWorkExperience', () => {
const result = selectPrimaryWorkExperience([shortRange, longRange])
expect(result.organizationName).toBe('LongRange')
})

it('email-domain beats enrichment when both are dated', () => {
const enrichment = makeRow({
organizationId: 'enrichment',
organizationName: 'Enrichment Org',
dateStart: '2020-01-01',
source: 'enrichment-progai',
})
const emailDomain = makeRow({
organizationId: 'email',
organizationName: 'Email Org',
dateStart: '2020-01-01',
source: 'email-domain',
})
expect(selectPrimaryWorkExperience([enrichment, emailDomain]).organizationName).toBe(
'Email Org',
)
})

it('ui beats email-domain when both are dated', () => {
const emailDomain = makeRow({
organizationId: 'email',
organizationName: 'Email Org',
dateStart: '2020-01-01',
source: 'email-domain',
})
const ui = makeRow({
organizationId: 'ui',
organizationName: 'UI Org',
dateStart: '2020-01-01',
source: 'ui',
})
expect(selectPrimaryWorkExperience([emailDomain, ui]).organizationName).toBe('UI Org')
})

it('ui beats enrichment when both are dated', () => {
const enrichment = makeRow({
organizationId: 'enrichment',
organizationName: 'Enrichment Org',
dateStart: '2020-01-01',
source: 'enrichment-clearbit',
})
const ui = makeRow({
organizationId: 'ui',
organizationName: 'UI Org',
dateStart: '2020-01-01',
source: 'ui',
})
expect(selectPrimaryWorkExperience([enrichment, ui]).organizationName).toBe('UI Org')
})

it('falls through to member count when source tiers are equal', () => {
const small = makeRow({
organizationId: 'small',
organizationName: 'Small Enrichment',
dateStart: '2020-01-01',
memberCount: 10,
source: 'enrichment-progai',
})
const large = makeRow({
organizationId: 'large',
organizationName: 'Large Enrichment',
dateStart: '2020-01-01',
memberCount: 100,
source: 'enrichment-progai',
})
expect(selectPrimaryWorkExperience([small, large]).organizationName).toBe('Large Enrichment')
})

it('undated rows are not affected by source priority — dated enrichment beats undated email-domain', () => {
const undatedEmailDomain = makeRow({
organizationId: 'email',
organizationName: 'Email Org',
dateStart: null,
source: 'email-domain',
})
const datedEnrichment = makeRow({
organizationId: 'enrichment',
organizationName: 'Enrichment Org',
dateStart: '2020-01-01',
source: 'enrichment-progai',
})
expect(
selectPrimaryWorkExperience([undatedEmailDomain, datedEnrichment]).organizationName,
).toBe('Enrichment Org')
})
})

// ---------------------------------------------------------------------------
Expand Down
23 changes: 18 additions & 5 deletions services/libs/data-access-layer/src/affiliations/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getLongestDateRange } from '@crowd/common'
import { getLongestDateRange, getMemberOrganizationSourceRank } from '@crowd/common'
import { IMemberOrganization } from '@crowd/types'

import { BLACKLISTED_MEMBER_TITLES } from '../members/base'
Expand All @@ -22,6 +22,7 @@ export interface IWorkExperienceResolution {
isPrimaryWorkExperience: boolean
memberCount: number
segmentId: string | null
source?: string | null
}

/**
Expand Down Expand Up @@ -58,7 +59,8 @@ export async function findWorkExperiencesBulk(
mo."createdAt",
COALESCE(ovr."isPrimaryWorkExperience", false) AS "isPrimaryWorkExperience",
COALESCE(a.total_count, 0) AS "memberCount",
NULL::text AS "segmentId"
NULL::text AS "segmentId",
mo."source"
FROM "memberOrganizations" mo
JOIN organizations o ON mo."organizationId" = o.id
LEFT JOIN "memberOrganizationAffiliationOverrides" ovr ON ovr."memberOrganizationId" = mo.id
Expand Down Expand Up @@ -92,7 +94,8 @@ export async function findManualAffiliationsBulk(
NULL::timestamptz AS "createdAt",
false AS "isPrimaryWorkExperience",
0 AS "memberCount",
msa."segmentId"
msa."segmentId",
NULL AS "source"
FROM "memberSegmentAffiliations" msa
JOIN organizations o ON msa."organizationId" = o.id
WHERE msa."memberId" IN ($(memberIds:csv))
Expand Down Expand Up @@ -121,13 +124,23 @@ export function selectPrimaryWorkExperience(orgs: IWorkExperienceResolution[]) {
const withDates = orgs.filter((r) => r.dateStart)
if (withDates.length === 1) return withDates[0]

// 4. Org with strictly more members wins; if tied, fall through
// 4. Among dated rows, pick the best source tier (ui > email-domain > enrichment-*)
if (withDates.length > 1) {
const bestRank = Math.min(...withDates.map((r) => getMemberOrganizationSourceRank(r.source)))
const topSourceGroup = withDates.filter(
(r) => getMemberOrganizationSourceRank(r.source) === bestRank,
)
Comment thread
skwowet marked this conversation as resolved.
Outdated
if (topSourceGroup.length === 1) return topSourceGroup[0]
Comment thread
skwowet marked this conversation as resolved.
orgs = topSourceGroup
}

// 5. Org with strictly more members wins; if tied, fall through
const sorted = [...orgs].sort((a, b) => b.memberCount - a.memberCount)
if (sorted.length >= 2 && sorted[0].memberCount > sorted[1].memberCount) {
return sorted[0]
}

// 5. Longest date range as final tiebreaker
// 6. Longest date range as final tiebreaker
return getLongestDateRange(
orgs as unknown as IMemberOrganization[],
) as unknown as IWorkExperienceResolution
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from 'lodash'
import { v4 as uuid } from 'uuid'

import { getLongestDateRange } from '@crowd/common'
import { getLongestDateRange, getMemberOrganizationSourceRank } from '@crowd/common'
import { getServiceChildLogger } from '@crowd/logging'
import {
IChangeAffiliationOverrideData,
Expand Down Expand Up @@ -81,7 +81,16 @@ async function prepareMemberOrganizationAffiliationTimeline(
return withDates[0]
}

// 2. get the two orgs with the most members, and return the one with the most members if there's no draw
// 2. among dated rows, pick the best source tier (ui > email-domain > enrichment-*)
if (withDates.length > 1) {
const sourceRank = (row: AffiliationItem) =>
getMemberOrganizationSourceRank((row as MemberOrganizationWithOverrides).source)
const bestRank = Math.min(...withDates.map(sourceRank))
orgs = withDates.filter((row) => sourceRank(row) === bestRank)
if (orgs.length === 1) return orgs[0]
Comment thread
skwowet marked this conversation as resolved.
Outdated
Comment thread
skwowet marked this conversation as resolved.
Outdated
}
Comment thread
skwowet marked this conversation as resolved.

// 3. get the two orgs with the most members, and return the one with the most members if there's no draw
// only compare member orgs (manual affiliations don't have memberCount)
const memberOrgsOnly = orgs.filter(
(row: AffiliationItem) => 'segmentId' in row && !!row.segmentId,
Expand All @@ -93,7 +102,7 @@ async function prepareMemberOrganizationAffiliationTimeline(
}
}

// 3. there's a draw, return the one with the longer date range
// 4. there's a draw, return the one with the longer date range
return getLongestDateRange(orgs)
}
}
Expand Down Expand Up @@ -243,6 +252,7 @@ async function prepareMemberOrganizationAffiliationTimeline(
mo."dateStart",
mo."dateEnd",
mo."createdAt",
mo."source",
coalesce(ovr."isPrimaryWorkExperience", false) as "isPrimaryWorkExperience",
coalesce(a.total_count, 0) as "memberCount"
FROM "memberOrganizations" mo
Expand Down
Loading