Skip to content

Commit a33b6b3

Browse files
feat(enrichments): add Wiza + Prospeo phone reveal to phone-number waterfall
1 parent 55a1849 commit a33b6b3

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { phoneNumberEnrichment } from '@/enrichments/phone-number/phone-number'
6+
import type { EnrichmentProvider } from '@/enrichments/types'
7+
8+
function provider(id: string): EnrichmentProvider {
9+
const p = phoneNumberEnrichment.providers.find((x) => x.id === id)
10+
if (!p) throw new Error(`Provider ${id} not found in phone-number cascade`)
11+
return p
12+
}
13+
14+
const inputs = { fullName: 'John Doe', companyDomain: 'https://www.acme.com/careers' }
15+
16+
describe('phone-number enrichment cascade', () => {
17+
it('chains PDL then the phone-capable hosted providers', () => {
18+
expect(phoneNumberEnrichment.providers.map((p) => p.id)).toEqual(['pdl', 'wiza', 'prospeo'])
19+
})
20+
21+
describe('wiza', () => {
22+
const p = provider('wiza')
23+
it('reveals phone (5 credits) and maps mobile_phone/phones', () => {
24+
expect(p.toolId).toBe('wiza_individual_reveal')
25+
expect(p.buildParams(inputs)).toEqual({
26+
full_name: 'John Doe',
27+
domain: 'acme.com',
28+
enrichment_level: 'phone',
29+
})
30+
expect(p.mapOutput({ mobile_phone: '+1555', phones: [] })).toEqual({ phone: '+1555' })
31+
expect(p.mapOutput({ phones: [{ number: '+1777' }] })).toEqual({ phone: '+1777' })
32+
expect(p.mapOutput({ mobile_phone: null, phone_number: null, phones: [] })).toBeNull()
33+
})
34+
it('skips without a company domain', () => {
35+
expect(p.buildParams({ fullName: 'John Doe', companyDomain: '' })).toBeNull()
36+
})
37+
})
38+
39+
describe('prospeo', () => {
40+
const p = provider('prospeo')
41+
it('requests mobile enrichment and maps person.mobile.mobile', () => {
42+
expect(p.toolId).toBe('prospeo_enrich_person')
43+
expect(p.buildParams(inputs)).toEqual({
44+
full_name: 'John Doe',
45+
company_website: 'acme.com',
46+
enrich_mobile: true,
47+
})
48+
expect(p.mapOutput({ person: { mobile: { mobile: '+1555', status: 'VERIFIED' } } })).toEqual({
49+
phone: '+1555',
50+
})
51+
expect(p.mapOutput({ person: { mobile: { mobile: '' } } })).toBeNull()
52+
})
53+
it('skips without a company domain', () => {
54+
expect(p.buildParams({ fullName: 'John Doe', companyDomain: '' })).toBeNull()
55+
})
56+
})
57+
})

apps/sim/enrichments/phone-number/phone-number.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import type { EnrichmentConfig } from '@/enrichments/types'
55

66
/**
77
* Phone Number enrichment. Finds a contact's phone number from their full name
8-
* and (optionally) company domain via a People Data Labs person match.
8+
* and (optionally) company domain via a waterfall: People Data Labs first
9+
* (cheapest, name-only capable), then Wiza and Prospeo mobile reveals as
10+
* fallbacks. Wiza/Prospeo need a company domain, so they self-skip without one.
11+
* The first provider to return a phone wins; all support hosted keys.
912
*/
1013
export const phoneNumberEnrichment: EnrichmentConfig = {
1114
id: 'phone-number',
@@ -37,5 +40,41 @@ export const phoneNumberEnrichment: EnrichmentConfig = {
3740
return phone ? { phone } : null
3841
},
3942
}),
43+
toolProvider({
44+
id: 'wiza',
45+
label: 'Wiza',
46+
toolId: 'wiza_individual_reveal',
47+
buildParams: (inputs) => {
48+
const fullName = str(inputs.fullName)
49+
const domain = normalizeDomain(inputs.companyDomain)
50+
if (!fullName || !domain) return null
51+
// 'phone' reveals the mobile number (5 credits).
52+
return { full_name: fullName, domain, enrichment_level: 'phone' }
53+
},
54+
mapOutput: (output) => {
55+
const phones = Array.isArray(output.phones)
56+
? (output.phones as Record<string, unknown>[])
57+
: []
58+
const phone = str(output.mobile_phone) || str(output.phone_number) || str(phones[0]?.number)
59+
return phone ? { phone } : null
60+
},
61+
}),
62+
toolProvider({
63+
id: 'prospeo',
64+
label: 'Prospeo',
65+
toolId: 'prospeo_enrich_person',
66+
buildParams: (inputs) => {
67+
const fullName = str(inputs.fullName)
68+
const companyWebsite = normalizeDomain(inputs.companyDomain)
69+
if (!fullName || !companyWebsite) return null
70+
return { full_name: fullName, company_website: companyWebsite, enrich_mobile: true }
71+
},
72+
mapOutput: (output) => {
73+
const person = output.person as Record<string, unknown> | undefined
74+
const mobile = person?.mobile as Record<string, unknown> | undefined
75+
const phone = str(mobile?.mobile)
76+
return phone ? { phone } : null
77+
},
78+
}),
4079
],
4180
}

0 commit comments

Comments
 (0)