Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c50ab0d
add some logging
epipav Nov 28, 2024
e66da1e
log more
epipav Nov 28, 2024
eace62d
test new version
epipav Nov 28, 2024
8a4bb54
only update manual changes when dateEnd is null
epipav Nov 28, 2024
5473938
profile finder and work expfixes
epipav Nov 28, 2024
1175d84
simplify a bit
epipav Nov 28, 2024
ca1f8d4
clean incoming attributes from llm
epipav Nov 28, 2024
8faed6c
fix
epipav Nov 28, 2024
23e6cdb
update log levels
epipav Nov 28, 2024
00c7dc2
fix prog normalization
epipav Nov 28, 2024
21c76ba
fix serp linkedin handle parse
epipav Nov 28, 2024
c0abfa3
process all members
epipav Nov 29, 2024
763480f
when primary domain exists don't send the additional identities as ve…
epipav Nov 29, 2024
a36056c
improve progai work exp normalization
epipav Nov 29, 2024
6c19087
better replaceDoubleQuotes
epipav Nov 29, 2024
6c4750d
Merge branch 'main' into bugfix/check-org-existence-in-enrichment-fix
epipav Nov 29, 2024
e8f7618
some logging
epipav Nov 29, 2024
d59ac6b
remove logging
epipav Nov 29, 2024
31ee17b
org identity squash fix
epipav Nov 29, 2024
98105dd
fix
epipav Nov 29, 2024
d4e7bb0
fix for identities inside workExperiencesSquashedByLLM
epipav Nov 29, 2024
73add51
bit more fixing
epipav Nov 29, 2024
ab189a6
use replaceDoubleQuotes on work experience free text fields
epipav Nov 30, 2024
cf6f43a
switch to sonnet 3.5v2 because of longer output token support
epipav Nov 30, 2024
d8b9762
final fixes, some cleaning
epipav Dec 2, 2024
4c24a9b
remove unused code
epipav Dec 2, 2024
3b5c48a
Merge branch 'main' into bugfix/check-org-existence-in-enrichment-fix
epipav Dec 2, 2024
d8a63b4
formatting
epipav Dec 2, 2024
9fac18e
Merge branch 'bugfix/check-org-existence-in-enrichment-fix' of github…
epipav Dec 2, 2024
64aee6c
fix linting
epipav Dec 2, 2024
a859861
fix progai linkedin normalization
epipav Dec 3, 2024
5e2fcb0
checking existent work experiences when mapping incoming new work exp…
epipav Dec 5, 2024
22121ea
sync member and created orgs on enrchment to opensearch
epipav Dec 6, 2024
52549a0
also return unverified identities on getting existing member data whi…
epipav Dec 6, 2024
3a52818
check org existence in all platforms if the type is primary domain
epipav Dec 6, 2024
82aed4f
cross-checking verified domains in existing & incoming org for existe…
epipav Dec 6, 2024
71a297d
fix returning identities within existing work exps
epipav Dec 6, 2024
b8d5d2b
fix consistenct issue when syncing bcs of Promise.all
epipav Dec 6, 2024
7318e16
fix
epipav Dec 6, 2024
47f9263
check sent orgid exists
epipav Dec 6, 2024
1606a84
sync fix
epipav Dec 7, 2024
c87f732
fix
epipav Dec 7, 2024
6ae3a91
update linkedin discarding logic to discard all unverified when there…
epipav Dec 9, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function getEnrichmentData(
input: IEnrichmentSourceInput,
): Promise<IMemberEnrichmentData | null> {
const service = EnrichmentSourceServiceFactory.getEnrichmentSourceService(source, svc.log)
if ((await service.isEnrichableBySource(input)) && (await hasRemainingCredits(source))) {
if (await service.isEnrichableBySource(input)) {
return service.getData(input)
}
return null
Expand Down Expand Up @@ -228,7 +228,7 @@ export async function updateMemberUsingSquashedPayload(

// process identities
if (squashedPayload.identities.length > 0) {
svc.log.info({ memberId }, 'Adding to member identities!')
svc.log.debug({ memberId }, 'Adding to member identities!')
for (const i of squashedPayload.identities) {
updated = true
promises.push(
Expand All @@ -247,7 +247,7 @@ export async function updateMemberUsingSquashedPayload(
// process contributions
// if squashed payload has data from progai, we should fetch contributions here
// it's ommited from the payload because it takes a lot of space
svc.log.info('Processing contributions! ', { memberId, hasContributions })
svc.log.debug('Processing contributions! ', { memberId, hasContributions })
if (hasContributions) {
promises.push(
findMemberEnrichmentCache([MemberEnrichmentSource.PROGAI], memberId)
Expand All @@ -265,7 +265,6 @@ export async function updateMemberUsingSquashedPayload(
.then((normalized) => {
if (normalized) {
const typed = normalized as IMemberEnrichmentDataNormalized
svc.log.info('Normalized contributions: ', { contributions: typed.contributions })

if (typed.contributions) {
updated = true
Expand All @@ -280,7 +279,7 @@ export async function updateMemberUsingSquashedPayload(
let attributes = existingMemberData.attributes as Record<string, unknown>

if (squashedPayload.attributes) {
svc.log.info({ memberId }, 'Updating member attributes!')
svc.log.debug({ memberId }, 'Updating member attributes!')

attributes = _.merge({}, attributes, squashedPayload.attributes)

Expand All @@ -298,7 +297,7 @@ export async function updateMemberUsingSquashedPayload(

// process reach
if (squashedPayload.reach && Object.keys(squashedPayload.reach).length > 0) {
svc.log.info({ memberId }, 'Updating member reach!')
svc.log.debug({ memberId }, 'Updating member reach!')
let reach: IMemberReach

if (existingMemberData.reach && existingMemberData.reach.total) {
Expand Down Expand Up @@ -608,7 +607,7 @@ export async function findWhichLinkedinProfileToUseAmongScraperResult(
}
}

if (!categorized.selected && profilesFromUnverfiedIdentities.length > 0) {
if (profilesFromUnverfiedIdentities.length > 0) {
const result = await findRelatedLinkedinProfilesWithLLM(
memberId,
memberData,
Expand All @@ -617,7 +616,9 @@ export async function findWhichLinkedinProfileToUseAmongScraperResult(

// check if empty object
if (result.profileIndex !== null) {
categorized.selected = profilesFromUnverfiedIdentities[result.profileIndex]
if (!categorized.selected) {
categorized.selected = profilesFromUnverfiedIdentities[result.profileIndex]
}
// add profiles not selected to discarded
for (let i = 0; i < profilesFromUnverfiedIdentities.length; i++) {
if (i !== result.profileIndex) {
Expand Down Expand Up @@ -796,46 +797,35 @@ function prepareWorkExperiences(
// we iterate through the existing version experiences to see if update is needed
for (const current of orderedCurrentVersion) {
// try and find a matching experience in the new versions by title
let match = orderedNewVersion.find(
const match = orderedNewVersion.find(
(e) =>
e.title === current.jobTitle &&
e.identities &&
e.identities.some((e) => e.organizationId === current.orgId),
)
if (!match) {
// if we didn't find a match by title we should check dates
match = orderedNewVersion.find(
(e) =>
dateIntersects(current.dateStart, current.dateEnd, e.startDate, e.endDate) &&
e.identities &&
e.identities.some((e) => e.organizationId === current.orgId),
)
}

// if we found a match we can check if we need something to update
if (match) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (
match &&
current.dateStart === match.startDate &&
current.dateEnd === null &&
match.endDate !== null
) {
const toUpdateInner: Record<string, any> = {}

// lets check if the dates and title are the same otherwise we need to update them
if (current.dateStart !== match.startDate) {
toUpdateInner.dateStart = match.startDate
}

if (current.dateEnd !== match.endDate) {
toUpdateInner.dateEnd = match.endDate
}

if (current.jobTitle !== match.title) {
toUpdateInner.title = match.title
}

if (Object.keys(toUpdateInner).length > 0) {
toUpdate.set(current, toUpdateInner)
}
toUpdateInner.dateEnd = match.endDate
toUpdate.set(current, toUpdateInner)

// remove the match from the new version array so we later don't process it again
orderedNewVersion = orderedNewVersion.filter((e) => e.id !== match.id)
} else if (
match &&
(current.dateStart !== match.startDate || current.dateEnd !== null || match.endDate === null)
) {
// there's an incoming work experiences, but it's conflicting with the existing manually updated data
// we shouldn't add or update anything when this happens
// we can only update dateEnd of existing manually changed data, when it has a null dateEnd
orderedNewVersion = orderedNewVersion.filter((e) => e.id !== match.id)
Comment on lines +910 to +938

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling for work experience matching.

The work experience matching logic needs better error handling:

  1. Validation of input data structures
  2. Handling of edge cases where identities array is undefined
  3. Logging of matching decisions for debugging

Add error handling:

     const match = orderedNewVersion.find(
       (e) =>
         e.title === current.jobTitle &&
-        e.identities &&
-        e.identities.some((e) => e.organizationId === current.orgId),
+        Array.isArray(e.identities) &&
+        e.identities.some((identity) => {
+          if (!identity || !identity.organizationId) {
+            svc.log.warn({ experience: e }, 'Invalid identity data in work experience')
+            return false
+          }
+          return identity.organizationId === current.orgId
+        }),
     )

+    svc.log.debug(
+      { current, match },
+      match ? 'Found matching work experience' : 'No matching work experience found'
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const match = orderedNewVersion.find(
(e) =>
e.title === current.jobTitle &&
e.identities &&
e.identities.some((e) => e.organizationId === current.orgId),
)
if (!match) {
// if we didn't find a match by title we should check dates
match = orderedNewVersion.find(
(e) =>
dateIntersects(current.dateStart, current.dateEnd, e.startDate, e.endDate) &&
e.identities &&
e.identities.some((e) => e.organizationId === current.orgId),
)
}
// if we found a match we can check if we need something to update
if (match) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (
match &&
current.dateStart === match.startDate &&
current.dateEnd === null &&
match.endDate !== null
) {
const toUpdateInner: Record<string, any> = {}
// lets check if the dates and title are the same otherwise we need to update them
if (current.dateStart !== match.startDate) {
toUpdateInner.dateStart = match.startDate
}
if (current.dateEnd !== match.endDate) {
toUpdateInner.dateEnd = match.endDate
}
if (current.jobTitle !== match.title) {
toUpdateInner.title = match.title
}
if (Object.keys(toUpdateInner).length > 0) {
toUpdate.set(current, toUpdateInner)
}
toUpdateInner.dateEnd = match.endDate
toUpdate.set(current, toUpdateInner)
// remove the match from the new version array so we later don't process it again
orderedNewVersion = orderedNewVersion.filter((e) => e.id !== match.id)
} else if (
match &&
(current.dateStart !== match.startDate || current.dateEnd !== null || match.endDate === null)
) {
// there's an incoming work experiences, but it's conflicting with the existing manually updated data
// we shouldn't add or update anything when this happens
// we can only update dateEnd of existing manually changed data, when it has a null dateEnd
orderedNewVersion = orderedNewVersion.filter((e) => e.id !== match.id)
const match = orderedNewVersion.find(
(e) =>
e.title === current.jobTitle &&
Array.isArray(e.identities) &&
e.identities.some((identity) => {
if (!identity || !identity.organizationId) {
svc.log.warn({ experience: e }, 'Invalid identity data in work experience')
return false
}
return identity.organizationId === current.orgId
}),
)
svc.log.debug(
{ current, match },
match ? 'Found matching work experience' : 'No matching work experience found'
)
// if we found a match we can check if we need something to update
if (
match &&
current.dateStart === match.startDate &&
current.dateEnd === null &&
match.endDate !== null
) {
const toUpdateInner: Record<string, any> = {}
toUpdateInner.dateEnd = match.endDate
toUpdate.set(current, toUpdateInner)
// remove the match from the new version array so we later don't process it again
orderedNewVersion = orderedNewVersion.filter((e) => e.id !== match.id)
} else if (
match &&
(current.dateStart !== match.startDate || current.dateEnd !== null || match.endDate === null)
) {
// there's an incoming work experiences, but it's conflicting with the existing manually updated data
// we shouldn't add or update anything when this happens
// we can only update dateEnd of existing manually changed data, when it has a null dateEnd
orderedNewVersion = orderedNewVersion.filter((e) => e.id !== match.id)

}
// if we didn't find a match we should just leave it as it is in the database since it was manual input
}
Expand All @@ -850,28 +840,6 @@ function prepareWorkExperiences(
}
}

function dateIntersects(
d1Start?: string | null,
d1End?: string | null,
d2Start?: string | null,
d2End?: string | null,
): boolean {
// If both periods have no dates at all, we can't determine intersection
if ((!d1Start && !d1End) || (!d2Start && !d2End)) {
return false
}

// Convert strings to timestamps, using fallbacks for missing dates
const start1 = d1Start ? new Date(d1Start).getTime() : -Infinity
const end1 = d1End ? new Date(d1End).getTime() : Infinity
const start2 = d2Start ? new Date(d2Start).getTime() : -Infinity
const end2 = d2End ? new Date(d2End).getTime() : Infinity

// Periods intersect if one period's start is before other period's end
// and that same period's end is after the other period's start
return start1 <= end2 && end1 >= start2
}

export async function cleanAttributeValue(
attributeValue: string | string[] | Record<string, any>,
): Promise<string | string[] | Record<string, any>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const tenantId = processArguments[0]

const minMemberActivities = 100
const maxConcurrentProcessing = 5
const maxMembersToProcess = 1000
const maxMembersToProcess = Infinity

async function getEnrichableMembers(limit: number): Promise<string[]> {
const query = `
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios'

import { replaceDoubleQuotes } from '@crowd/common'
import { Logger, LoggerBase } from '@crowd/logging'
import {
MemberAttributeName,
Expand Down Expand Up @@ -216,10 +217,10 @@ export default class EnrichmentServiceClearbit extends LoggerBase implements IEn
}

normalized.memberOrganizations.push({
name: data.employment.name,
name: replaceDoubleQuotes(data.employment.name),
source: OrganizationSource.ENRICHMENT_CLEARBIT,
identities: orgIdentities,
title: data.employment.title,
title: replaceDoubleQuotes(data.employment.title),
startDate: null,
endDate: null,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from 'axios'

import { isEmail } from '@crowd/common'
import { isEmail, replaceDoubleQuotes } from '@crowd/common'
import { Logger, LoggerBase } from '@crowd/logging'
import {
IMemberEnrichmentCache,
Expand Down Expand Up @@ -352,13 +352,15 @@ export default class EnrichmentServiceCrustdata extends LoggerBase implements IE
}

normalized.memberOrganizations.push({
name: workExperience.employer_name,
name: replaceDoubleQuotes(workExperience.employer_name),
source: OrganizationSource.ENRICHMENT_CRUSTDATA,
identities,
title: workExperience.employee_title,
title: replaceDoubleQuotes(workExperience.employee_title),
startDate: workExperience?.start_date ?? null,
endDate: workExperience?.end_date ?? null,
organizationDescription: workExperience.employer_linkedin_description,
organizationDescription: replaceDoubleQuotes(
workExperience.employer_linkedin_description,
),
})
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from 'axios'
import lodash from 'lodash'

import { websiteNormalizer } from '@crowd/common'
import { replaceDoubleQuotes, websiteNormalizer } from '@crowd/common'
import { Logger, LoggerBase } from '@crowd/logging'
import {
MemberAttributeName,
Expand Down Expand Up @@ -261,51 +261,80 @@ export default class EnrichmentServiceProgAI extends LoggerBase implements IEnri
): IMemberEnrichmentDataNormalized {
if (data.work_experiences) {
for (const workExperience of data.work_experiences) {
const identities = []

if (workExperience.companyUrl) {
const normalizedDomain = websiteNormalizer(workExperience.companyUrl, false)
if (
workExperience.company !== null ||
workExperience.companyUrl !== null ||
workExperience.companyLinkedInUrl !== null
) {
const identities = []
let hasPrimaryDomainIdentity = false

if (workExperience.companyUrl) {
const normalizedDomain = websiteNormalizer(workExperience.companyUrl, false)

// sometimes companyUrl is a github link, we don't want to add it as a primary domain
if (
normalizedDomain &&
!workExperience.companyUrl.toLowerCase().includes('github') &&
!(workExperience.company || '').toLowerCase().includes('github')
) {
identities.push({
platform: PlatformType.LINKEDIN,
value: normalizedDomain,
type: OrganizationIdentityType.PRIMARY_DOMAIN,
verified: true,
})
hasPrimaryDomainIdentity = true
}
}

// sometimes companyUrl is a github link, we don't want to add it as a primary domain
if (
normalizedDomain &&
!workExperience.companyUrl.toLowerCase().includes('github') &&
!workExperience.company.toLowerCase().includes('github')
workExperience.companyLinkedInUrl &&
this.getLinkedInProfileHandle(workExperience.companyLinkedInUrl)
) {
identities.push({
platform: PlatformType.LINKEDIN,
value: normalizedDomain,
type: OrganizationIdentityType.PRIMARY_DOMAIN,
verified: true,
value: this.getLinkedInProfileHandle(workExperience.companyLinkedInUrl),
type: OrganizationIdentityType.USERNAME,
verified: !hasPrimaryDomainIdentity,
})
}
}

if (workExperience.companyLinkedInUrl) {
identities.push({
platform: PlatformType.LINKEDIN,
value: `company:${workExperience.companyLinkedInUrl.split('/').pop()}`,
type: OrganizationIdentityType.USERNAME,
verified: true,
normalized.memberOrganizations.push({
name: replaceDoubleQuotes(workExperience.company),
source: OrganizationSource.ENRICHMENT_PROGAI,
identities,
title: replaceDoubleQuotes(workExperience.title),
startDate: workExperience.startDate
? workExperience.startDate.replace('Z', '+00:00')
: null,
endDate: workExperience.endDate ? workExperience.endDate.replace('Z', '+00:00') : null,
Comment on lines +308 to +311

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Based on the code analysis, I can now provide the final response:

Add date format validation for work experience dates

The code assumes dates from the ProgAI API are in a format ending with 'Z' (UTC timezone marker), but there's no validation to ensure this. The type definition shows that startDate and endDate are just string types without format constraints. Consider:

  • Adding input validation to ensure dates match the expected format before manipulation
  • Using a date parsing library like date-fns or similar to handle the timezone conversion safely
  • Adding error handling for malformed dates
🔗 Analysis chain

Validate date string manipulation

The date string manipulation assumes a specific format. Consider adding validation or using a date manipulation library to handle different date formats safely.


Let me check the input date format and validation in the codebase.


Let me check if there's any date format validation or parsing before this transformation.


Let me check the entire file content to understand the context better.


Let me check the type definitions for the work experience data structure.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for potential date format issues in the codebase
rg -A 2 "replace\('Z', '\+00:00'\)" 

Length of output: 608


Script:

#!/bin/bash
# Check the workExperience type definition and any date validation
ast-grep --pattern 'interface $_WorkExperience {
  $$$
  startDate$_
  $$$
  endDate$_
  $$$
}'

# Check for any date validation or parsing logic
rg -B 2 -A 2 "workExperience\.(startDate|endDate)" 

Length of output: 1053


Script:

#!/bin/bash
# Check for date validation or parsing in the file
rg -B 5 -A 5 "function.*workExperience" services/apps/members_enrichment_worker/src/sources/progai/service.ts

# Check if there's any date validation utility being used
rg -l "import.*date|moment|luxon|dayjs" services/apps/members_enrichment_worker/src/sources/progai/service.ts

# Look for any date format validation
ast-grep --pattern 'function $_(workExperience$_) {
  $$$
}'

Length of output: 286


Script:

#!/bin/bash
# Get the content of the service file
cat services/apps/members_enrichment_worker/src/sources/progai/service.ts

# Check for any date-related types or interfaces
ast-grep --pattern 'type $_ = {
  $$$
  date$_
  $$$
}'

# Check for any date validation functions
rg -l "validateDate|isValidDate|parseDate" services/apps/members_enrichment_worker/src/sources/progai/service.ts

Length of output: 11440


Script:

#!/bin/bash
# Check the types file for work experience interface
cat services/apps/members_enrichment_worker/src/sources/progai/types.ts

# Check if there's any date format validation in the utils
rg -l "date" services/apps/members_enrichment_worker/src/utils/

# Look for any date-related imports in the types file
rg "import.*date" services/apps/members_enrichment_worker/src/sources/progai/types.ts

Length of output: 2202

})
}

normalized.memberOrganizations.push({
name: workExperience.company,
source: OrganizationSource.ENRICHMENT_PROGAI,
identities,
title: workExperience.title,
startDate: workExperience.startDate
? workExperience.startDate.replace('Z', '+00:00')
: null,
endDate: workExperience.endDate ? workExperience.endDate.replace('Z', '+00:00') : null,
})
}
}

return normalized
}

private getLinkedInProfileHandle(url: string): string | null {
let regex = /company\/([^/]+)/
let match = url.match(regex)

if (match) {
return `company:${match[1]}`
}

regex = /school\/([^/]+)/
match = url.match(regex)

if (match) {
return `school:${match[1]}`
}

return null
}

async getDataUsingGitHubHandle(githubUsername: string): Promise<IMemberEnrichmentDataProgAI> {
const url = `${process.env['CROWD_ENRICHMENT_PROGAI_URL']}/get_profile`
const config = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,19 @@ export default class EnrichmentServiceSerpApi extends LoggerBase implements IEnr
platform: PlatformType.LINKEDIN,
type: MemberIdentityType.USERNAME,
verified: false,
value: this.normalizeLinkedUrl(data.linkedinUrl).split('/').pop(),
value: this.getLinkedInProfileHandle(this.normalizeLinkedUrl(data.linkedinUrl)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for handle extraction

The normalize method should handle cases where getLinkedInProfileHandle returns null.

-  value: this.getLinkedInProfileHandle(this.normalizeLinkedUrl(data.linkedinUrl)),
+  value: this.getLinkedInProfileHandle(this.normalizeLinkedUrl(data.linkedinUrl)) ?? 
+    this.log.warn('Failed to extract LinkedIn handle', { url: data.linkedinUrl }) || null,

Committable suggestion skipped: line range outside the PR's diff.

},
],
}
return normalized
}

private getLinkedInProfileHandle(url: string): string | null {
const regex = /in\/([^/]+)/
const match = url.match(regex)
return match ? match[1] : null
}

private normalizeLinkedUrl(url: string): string {
try {
const parsedUrl = new URL(url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { IEnrichableMember, MemberEnrichmentSource } from '@crowd/types'

import * as activities from '../activities'
import { hasRemainingCredits } from '../activities/enrichment'
import { IEnrichmentSourceInput } from '../types'
import { sourceHasDifferentDataComparedToCache } from '../utils/common'

Expand Down Expand Up @@ -46,6 +47,12 @@ export async function enrichMember(
if (await isCacheObsolete(source, cache)) {
const enrichmentInput: IEnrichmentSourceInput = await getEnrichmentInput(input)

if (!(await hasRemainingCredits(source))) {
// no credits remaining, only update cache.updatedAt and keep the old data
await touchMemberEnrichmentCacheUpdatedAt(source, input.id)
continue
}

const data = await getEnrichmentData(source, enrichmentInput)

if (!cache) {
Expand Down
Loading