import { defineBatchStrategyMap, type BatchOperationStrategy, } from './batching-types'; const BETTERCONTACT_BATCH_ITEM_KEY = 'deepline_batch_item_key'; const BETTERCONTACT_BATCH_SIZE = 100; const BETTERCONTACT_SCOPED_REQUEST_PREFIX = 'deepline-bc-batch-v1:'; const BETTERCONTACT_DEFAULT_POLL_INTERVAL_MS = 2_000; const BETTERCONTACT_MAX_SYNC_WAIT_MS = 30_000; const BETTERCONTACT_SINGLE_PAYLOAD_FIELDS = new Set([ 'first_name', 'last_name', 'company_domain', 'company', 'linkedin_url', 'custom_fields', 'enrich_email_address', 'enrich_phone_number', 'wait_for_completion', 'poll_interval_ms', 'max_wait_ms', ]); type BetterContactCustomFields = Record; type BetterContactContact = { first_name: string; last_name: string; company_domain?: string; company?: string; linkedin_url?: string; custom_fields?: BetterContactCustomFields; }; type BetterContactControls = { enrich_email_address?: boolean; enrich_phone_number?: boolean; wait_for_completion?: boolean; poll_interval_ms?: number; max_wait_ms?: number; }; type BetterContactSinglePayload = BetterContactContact & BetterContactControls; type BetterContactBulkPayload = BetterContactControls & { contacts: BetterContactContact[]; }; type BetterContactResult = Record & { data?: unknown[]; }; type BetterContactBatchResult = BetterContactResult | unknown[]; export type BetterContactScopedRequest = { requestId: string; correlationField: string; correlationValue: string; }; // Fields BetterContact's AsyncGetResponse reports once for the whole batch // request, never per contact: `summary` (a batch-wide breakdown), // `credits_consumed`/`credits_left` (the request's total spend/remaining // balance). Billing settlement reads these from BetterContact's own async // status endpoint independently (see billing.ts's fetchTruth), never from // this per-row public shape, so stripping them here only affects what a // caller sees on each row -- it cannot desync actual charging. Left // unstripped, a 20-contact batch that spent 20 credits total would report // `credits_consumed: 20` on every one of the 20 rows, misrepresenting a // per-request total as a per-row cost. const BETTERCONTACT_BATCH_LEVEL_ONLY_FIELDS = [ 'summary', 'credits_consumed', 'credits_left', ] as const; function withoutBatchLevelSummary( result: BetterContactResult, logicalItemCount: number, ): BetterContactResult { if (logicalItemCount <= 1) { return result; } const perItemResult = { ...result }; for (const field of BETTERCONTACT_BATCH_LEVEL_ONLY_FIELDS) { delete perItemResult[field]; } return perItemResult; } function stableValue(value: unknown): unknown { if (Array.isArray(value)) { return value.map(stableValue); } if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value as Record) .filter(([, entry]) => entry !== undefined) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, entry]) => [key, stableValue(entry)]), ); } return value; } function stableStringify(value: unknown): string { return JSON.stringify(stableValue(value)); } function shortStableHash(value: string): string { let hash = 0x811c9dc5; for (let index = 0; index < value.length; index += 1) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 0x01000193) >>> 0; } return hash.toString(36); } function normalizedControls(payload: BetterContactControls) { const waitForCompletion = payload.wait_for_completion !== false; return { enrich_phone_number: payload.enrich_phone_number === true, wait_for_completion: waitForCompletion, ...(waitForCompletion ? { poll_interval_ms: payload.poll_interval_ms ?? BETTERCONTACT_DEFAULT_POLL_INTERVAL_MS, max_wait_ms: Math.min( payload.max_wait_ms ?? BETTERCONTACT_MAX_SYNC_WAIT_MS, BETTERCONTACT_MAX_SYNC_WAIT_MS, ), } : {}), }; } function controlsKey(payload: BetterContactControls): string { return stableStringify(normalizedControls(payload)); } function copyControls(payload: BetterContactControls): BetterContactControls { return { ...(payload.enrich_email_address !== undefined ? { enrich_email_address: payload.enrich_email_address } : {}), ...(payload.enrich_phone_number !== undefined ? { enrich_phone_number: payload.enrich_phone_number } : {}), ...(payload.wait_for_completion !== undefined ? { wait_for_completion: payload.wait_for_completion } : {}), ...(payload.poll_interval_ms !== undefined ? { poll_interval_ms: payload.poll_interval_ms } : {}), ...(payload.max_wait_ms !== undefined ? { max_wait_ms: payload.max_wait_ms } : {}), }; } function isPlainRecord(value: unknown): value is Record { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function hasValidOptionalBoolean( payload: Record, field: string, ): boolean { return payload[field] === undefined || typeof payload[field] === 'boolean'; } function hasValidOptionalInteger( payload: Record, field: string, minimum: number, maximum: number, ): boolean { const value = payload[field]; return ( value === undefined || (typeof value === 'number' && Number.isInteger(value) && value >= minimum && value <= maximum) ); } function hasValidControls(payload: BetterContactControls): boolean { const record = payload as Record; return ( hasValidOptionalBoolean(record, 'enrich_email_address') && hasValidOptionalBoolean(record, 'enrich_phone_number') && hasValidOptionalBoolean(record, 'wait_for_completion') && hasValidOptionalInteger(record, 'poll_interval_ms', 250, 10_000) && hasValidOptionalInteger(record, 'max_wait_ms', 1_000, 120_000) ); } function hasValidCustomFields(contact: BetterContactContact): boolean { const customFields = (contact as Record).custom_fields; return customFields === undefined || isPlainRecord(customFields); } function isBatchableContact(contact: BetterContactContact): boolean { return hasRequiredContactFields(contact) && hasValidCustomFields(contact); } function hasOnlyKnownSinglePayloadFields( payload: BetterContactSinglePayload, ): boolean { return Object.keys(payload).every((field) => BETTERCONTACT_SINGLE_PAYLOAD_FIELDS.has(field), ); } function isBatchableSinglePayload( payload: BetterContactSinglePayload, ): boolean { return ( isBatchableContact(payload) && hasValidControls(payload) && hasOnlyKnownSinglePayloadFields(payload) ); } function contactIdentity(contact: BetterContactContact): string { return stableStringify({ first_name: contact.first_name, last_name: contact.last_name, company_domain: contact.company_domain ?? null, company: contact.company ?? null, linkedin_url: contact.linkedin_url ?? null, custom_fields: contact.custom_fields ?? null, }); } function buildItemKey(contact: BetterContactContact, index = 0): string { return `dl_bc_${index}_${shortStableHash(contactIdentity(contact))}`; } function withItemKey( contact: BetterContactContact, itemKey: string, ): BetterContactContact { const customFields = contact.custom_fields ?? {}; const correlationFieldBase = `${BETTERCONTACT_BATCH_ITEM_KEY}_${shortStableHash( itemKey, )}`; let correlationField = correlationFieldBase; let suffix = 1; while (Object.prototype.hasOwnProperty.call(customFields, correlationField)) { correlationField = `${correlationFieldBase}_${suffix}`; suffix += 1; } return { ...contact, custom_fields: { ...customFields, [correlationField]: itemKey, }, }; } function resultCustomFieldRecords( row: unknown, ): Array> { if (!row || typeof row !== 'object' || Array.isArray(row)) { return []; } const customFields = (row as Record).custom_fields; return (Array.isArray(customFields) ? customFields : [customFields]).filter( (candidate): candidate is Record => Boolean(candidate) && typeof candidate === 'object' && !Array.isArray(candidate), ); } function encodeScopedRequest(scope: BetterContactScopedRequest): string { return `${BETTERCONTACT_SCOPED_REQUEST_PREFIX}${encodeURIComponent( JSON.stringify([ scope.requestId, scope.correlationField, scope.correlationValue, ]), )}`; } export function parseBetterContactScopedRequest( requestId: string, ): BetterContactScopedRequest | null { if (!requestId.startsWith(BETTERCONTACT_SCOPED_REQUEST_PREFIX)) { return null; } try { const parsed = JSON.parse( decodeURIComponent( requestId.slice(BETTERCONTACT_SCOPED_REQUEST_PREFIX.length), ), ) as unknown; if ( !Array.isArray(parsed) || parsed.length !== 3 || parsed.some((value) => typeof value !== 'string' || value.length === 0) ) { return null; } return { requestId: parsed[0] as string, correlationField: parsed[1] as string, correlationValue: parsed[2] as string, }; } catch { return null; } } function rowMatchesCorrelation( row: unknown, correlationField: string, correlationValue: string, ): boolean { return resultCustomFieldRecords(row).some( (fields) => fields[correlationField] === correlationValue, ); } function withoutCorrelationSelector( row: unknown, correlationField: string, ): unknown { if (!row || typeof row !== 'object' || Array.isArray(row)) { return row; } const record = row as Record; const customFields = record.custom_fields; const cleanRecord = (fields: Record) => { const cleaned = { ...fields }; delete cleaned[correlationField]; return cleaned; }; return { ...record, custom_fields: Array.isArray(customFields) ? customFields .filter(isPlainRecord) .map(cleanRecord) .filter((fields) => Object.keys(fields).length > 0) : isPlainRecord(customFields) ? cleanRecord(customFields) : customFields, }; } export function isolateBetterContactScopedResult( result: unknown, scopedRequestId: string, ): unknown { const scope = parseBetterContactScopedRequest(scopedRequestId); if (!scope || !isPlainRecord(result)) { return result; } const scopedResult: BetterContactResult = { ...result, id: scopedRequestId }; delete scopedResult.summary; if (!Array.isArray(result.data)) { return scopedResult; } if (result.data.length === 0) { return { ...scopedResult, data: [] }; } const matches = result.data.filter((row) => rowMatchesCorrelation(row, scope.correlationField, scope.correlationValue), ); if (matches.length !== 1) { throw new Error( matches.length === 0 ? 'BetterContact scoped result has missing or unmatched correlation identity.' : 'BetterContact scoped result has ambiguous correlation identity.', ); } return { ...scopedResult, data: [withoutCorrelationSelector(matches[0], scope.correlationField)], }; } function correlationEntryForItem(item: { itemKey: string; payload: BetterContactContact; }): [string, unknown] | null { const keyedContact = withItemKey(item.payload, item.itemKey); const originalFields = item.payload.custom_fields ?? {}; return ( Object.entries(keyedContact.custom_fields ?? {}).find( ([field]) => !Object.prototype.hasOwnProperty.call(originalFields, field), ) ?? null ); } function resultMatchesItem( row: unknown, item: { itemKey: string; payload: BetterContactContact }, ): boolean { const correlationEntry = correlationEntryForItem(item); if (!correlationEntry) { return false; } const [correlationField, correlationValue] = correlationEntry; return rowMatchesCorrelation(row, correlationField, String(correlationValue)); } function withoutCorrelationField( row: unknown, item: { itemKey: string; payload: BetterContactContact }, ): unknown { if (!row || typeof row !== 'object' || Array.isArray(row)) { return row; } const correlationField = correlationEntryForItem(item)?.[0]; if (!correlationField) { return row; } return withoutCorrelationSelector(row, correlationField); } function scopedRequestIdForItem( requestId: string, item: { itemKey: string; payload: BetterContactContact }, ): string { const correlationEntry = correlationEntryForItem(item); if (!correlationEntry) { throw new Error( 'BetterContact batch item is missing correlation identity.', ); } return encodeScopedRequest({ requestId, correlationField: correlationEntry[0], correlationValue: String(correlationEntry[1]), }); } function withScopedRequestId>( result: BetterContactResult, item: { itemKey: string; payload: TPayload }, logicalItemCount: number, ): BetterContactResult { const requestId = typeof result.id === 'string' ? result.id.trim() : ''; if (!requestId || logicalItemCount <= 1) { return result; } return { ...result, id: scopedRequestIdForItem(requestId, { itemKey: item.itemKey, payload: item.payload as unknown as BetterContactContact, }), }; } function hasRequiredContactFields(contact: BetterContactContact): boolean { const companyDomain = typeof contact.company_domain === 'string' && contact.company_domain.trim().length > 0 ? contact.company_domain : contact.company; return ( typeof contact.first_name === 'string' && contact.first_name.trim().length > 0 && typeof contact.last_name === 'string' && contact.last_name.trim().length > 0 && typeof companyDomain === 'string' && companyDomain.trim().length > 0 ); } function splitBetterContactResult>( fullResult: BetterContactBatchResult, compiled: { items: Array<{ itemKey: string; payload: TPayload }>; }, ) { const resultRows = Array.isArray(fullResult) ? fullResult : Array.isArray(fullResult.data) ? fullResult.data : null; if (!resultRows) { return compiled.items.map((item) => ({ itemKey: item.itemKey, result: { data: Array.isArray(fullResult) ? fullResult : withScopedRequestId( withoutBatchLevelSummary(fullResult, compiled.items.length), item, compiled.items.length, ), }, rawResult: fullResult, })); } if (resultRows.length === 0) { return compiled.items.map((item) => ({ itemKey: item.itemKey, result: { data: Array.isArray(fullResult) ? [] : withScopedRequestId( { ...withoutBatchLevelSummary(fullResult, compiled.items.length), data: [], }, item, compiled.items.length, ), }, rawResult: null, })); } const remainingItems = new Map( compiled.items.map((item) => [item.itemKey, item] as const), ); const rowsByItemKey = new Map(); for (const row of resultRows) { const matches = [...remainingItems.values()].filter((item) => resultMatchesItem(row, { itemKey: item.itemKey, payload: item.payload as unknown as BetterContactContact, }), ); if (matches.length !== 1) { throw new Error( matches.length === 0 ? 'BetterContact bulk result has missing or unmatched correlation identity.' : 'BetterContact bulk result has ambiguous correlation identity.', ); } const matchedItem = matches[0]!; remainingItems.delete(matchedItem.itemKey); rowsByItemKey.set(matchedItem.itemKey, row); } return compiled.items.map((item) => { const matchedRow = rowsByItemKey.get(item.itemKey); if (!matchedRow) { throw new Error( `BetterContact bulk result is missing result identity ${item.itemKey}.`, ); } const publicRow = withoutCorrelationField(matchedRow, { itemKey: item.itemKey, payload: item.payload as unknown as BetterContactContact, }); const resultData = Array.isArray(fullResult) ? [publicRow] : withScopedRequestId( { ...withoutBatchLevelSummary(fullResult, compiled.items.length), data: [publicRow], }, item, compiled.items.length, ); return { itemKey: item.itemKey, result: { data: resultData }, rawResult: publicRow, }; }); } const sharedStrategyFields = { batchOperation: 'bettercontact_bulk_enrich' as const, kind: 'identifier_batch' as const, maxBatchSize: BETTERCONTACT_BATCH_SIZE, bucketKeyPayloadFields: ['enrich_phone_number'], }; const bettercontactSingleBatchStrategy: BatchOperationStrategy< BetterContactSinglePayload, BetterContactBulkPayload, BetterContactBatchResult, { data: BetterContactBatchResult }, unknown > = { ...sharedStrategyFields, sourceOperation: 'bettercontact_enrich', canBatchWith(left, right) { return ( isBatchableSinglePayload(left) && isBatchableSinglePayload(right) && controlsKey(left) === controlsKey(right) ); }, toBucketKey(payload) { if (!isBatchableSinglePayload(payload)) { return `bettercontact_enrich:invalid:${stableStringify(payload)}`; } return `bettercontact_bulk_enrich:${controlsKey(payload)}`; }, toItemKey(payload) { return buildItemKey(payload); }, compile(payloads) { const first = payloads[0] ?? ({} as BetterContactSinglePayload); const items = payloads.map((payload, index) => ({ itemKey: buildItemKey(payload, index), payload, })); return { batchOperation: 'bettercontact_bulk_enrich', batchPayload: { ...copyControls(first), contacts: items.map((item) => isBatchableSinglePayload(item.payload) ? withItemKey(item.payload, item.itemKey) : item.payload, ), }, items, }; }, splitResult: splitBetterContactResult, }; function readBulkContacts( payload: BetterContactBulkPayload, ): BetterContactContact[] { return Array.isArray(payload.contacts) ? payload.contacts : []; } function isOneContactBulkPayload(payload: BetterContactBulkPayload): boolean { return readBulkContacts(payload).length === 1; } const bettercontactBulkSelfBatchStrategy: BatchOperationStrategy< BetterContactBulkPayload, BetterContactBulkPayload, BetterContactBatchResult, { data: BetterContactBatchResult }, unknown > = { ...sharedStrategyFields, sourceOperation: 'bettercontact_bulk_enrich', canBatchWith(left, right) { return ( isOneContactBulkPayload(left) && isOneContactBulkPayload(right) && isBatchableContact(readBulkContacts(left)[0]!) && isBatchableContact(readBulkContacts(right)[0]!) && hasValidControls(left) && hasValidControls(right) && controlsKey(left) === controlsKey(right) ); }, toBucketKey(payload) { if (!isOneContactBulkPayload(payload)) { return `bettercontact_bulk_enrich:passthrough:${stableStringify(payload)}`; } if ( !isBatchableContact(readBulkContacts(payload)[0]!) || !hasValidControls(payload) ) { return `bettercontact_bulk_enrich:invalid:${stableStringify(payload)}`; } return `bettercontact_bulk_enrich:${controlsKey(payload)}`; }, toItemKey(payload) { return buildItemKey( readBulkContacts(payload)[0] ?? ({} as BetterContactContact), ); }, compile(payloads) { const first = payloads[0] ?? ({ contacts: [] } as BetterContactBulkPayload); if (payloads.length === 1 && !isOneContactBulkPayload(first)) { return { batchOperation: 'bettercontact_bulk_enrich', batchPayload: first, items: [{ itemKey: this.toItemKey(first), payload: first }], }; } const items = payloads.map((payload, index) => ({ itemKey: buildItemKey(readBulkContacts(payload)[0]!, index), payload, })); return { batchOperation: 'bettercontact_bulk_enrich', batchPayload: { ...copyControls(first), contacts: items.map((item) => { const contact = readBulkContacts(item.payload)[0]!; return isBatchableContact(contact) && hasValidControls(item.payload) ? withItemKey(contact, item.itemKey) : contact; }), }, items, }; }, splitResult(fullResult, compiled) { const first = compiled.items[0]; if ( compiled.items.length === 1 && first && !isOneContactBulkPayload(first.payload) ) { return [ { itemKey: first.itemKey, result: { data: fullResult }, rawResult: fullResult, }, ]; } return splitBetterContactResult(fullResult, compiled); }, }; export const bettercontactBatchStrategies = defineBatchStrategyMap({ bettercontact_enrich: bettercontactSingleBatchStrategy, bettercontact_bulk_enrich: bettercontactBulkSelfBatchStrategy, });