@rachelallyson/planning-center-people-ts 2.13.0 โ 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/dist/helpers.d.ts +84 -0
- package/dist/helpers.js +200 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +12 -2
- package/dist/matching/matcher.d.ts +47 -0
- package/dist/matching/matcher.js +283 -17
- package/dist/modules/people.d.ts +80 -11
- package/dist/modules/people.js +59 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,69 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.14.0] - 2026-01-21
|
|
9
|
+
|
|
10
|
+
### โจ **New Features**
|
|
11
|
+
|
|
12
|
+
- **๐ Multi-Step Search Strategy**: New `searchStrategy: 'multi-step'` option for maximum matching success
|
|
13
|
+
- Tries multiple matching approaches in order until a match is found
|
|
14
|
+
- Strategy order: fuzzy+age โ fuzzy โ exact+age โ exact
|
|
15
|
+
- Handles name variations and age preference filtering automatically
|
|
16
|
+
- Significantly reduces duplicate person creation
|
|
17
|
+
|
|
18
|
+
- **๐ Name-Based Search Fallback**: New `fallbackToNameSearch` option with contact validation
|
|
19
|
+
- Falls back to name search when email/phone search fails
|
|
20
|
+
- Validates contact info to prevent wrong-person matches (e.g., two "John Smith"s)
|
|
21
|
+
- Configurable validation: `contactValidation: 'strict' | 'domain' | 'similarity'`
|
|
22
|
+
|
|
23
|
+
- **โก Multi-Phase Retry Configurations**: New `retryConfigs` option for phase-specific retry control
|
|
24
|
+
- `initial`: Quick search phase (default: 30s max wait, 3 retries)
|
|
25
|
+
- `aggressive`: Final search before create (default: 60s max wait, 6 retries)
|
|
26
|
+
- Prevents duplicates when PCO hasn't indexed contacts yet (15-30 min delay)
|
|
27
|
+
|
|
28
|
+
- **โ
Person ID Verification**: New `client.people.verifyPersonExists()` method
|
|
29
|
+
- Verifies a person exists in PCO with configurable timeout
|
|
30
|
+
- Handles merged/deleted persons gracefully (returns false for 404)
|
|
31
|
+
- Useful for validating cached person IDs before use
|
|
32
|
+
|
|
33
|
+
- **๐ Trust-Based Caching Helpers**: New utilities for smart person ID caching
|
|
34
|
+
- `calculateTrust(createdAt, trustWindow)`: Determines if cached personId can be trusted
|
|
35
|
+
- `DEFAULT_TRUST_WINDOW`: 1-hour default trust window constant
|
|
36
|
+
- Skip verification for fresh personIds to avoid race conditions
|
|
37
|
+
|
|
38
|
+
- **๐ง Contact Validation Helpers**: New utilities for contact info matching
|
|
39
|
+
- `emailDomainsMatch(email1, email2)`: Handles aliases (gmail/googlemail) and typos
|
|
40
|
+
- `phoneNumbersSimilar(phone1, phone2)`: Handles format variations and country codes
|
|
41
|
+
- `validateContactSimilarity()`: Validates contact info for name-based matches
|
|
42
|
+
- `extractEmailDomain()`: Extracts domain from email address
|
|
43
|
+
|
|
44
|
+
### ๐ง **Improvements**
|
|
45
|
+
|
|
46
|
+
- **Retry Config Refactoring**: Extracted `RetryConfig` interface for reusability
|
|
47
|
+
- **Better Logging**: Multi-step search logs which strategy found the match
|
|
48
|
+
- **Aggressive Final Search**: When `retryConfigs.aggressive` is set, performs final search before creating
|
|
49
|
+
|
|
50
|
+
### ๐ **Documentation**
|
|
51
|
+
|
|
52
|
+
- Updated README_V2.md with examples for all new features
|
|
53
|
+
- Added code examples for multi-step search, contact validation, and trust calculation
|
|
54
|
+
|
|
55
|
+
### ๐งช **Testing**
|
|
56
|
+
|
|
57
|
+
- Added `tests/helpers/contact-validation.test.ts` - Contact validation helper tests
|
|
58
|
+
- Added `tests/helpers/trust-calculation.test.ts` - Trust calculation tests
|
|
59
|
+
- Added `tests/matching/multi-step.test.ts` - Multi-step search strategy tests
|
|
60
|
+
- Added `tests/modules/people-verify.test.ts` - Person verification tests
|
|
61
|
+
|
|
62
|
+
### ๐ฆ **Exports**
|
|
63
|
+
|
|
64
|
+
New exports from the package:
|
|
65
|
+
|
|
66
|
+
- `emailDomainsMatch`, `extractEmailDomain`, `phoneNumbersSimilar`, `validateContactSimilarity`
|
|
67
|
+
- `calculateTrust`, `DEFAULT_TRUST_WINDOW`, `TrustResult` (type)
|
|
68
|
+
- `RetryConfig` (type), `PersonMatchOptions` (type)
|
|
69
|
+
- `DEFAULT_INITIAL_RETRY_CONFIG`, `DEFAULT_AGGRESSIVE_RETRY_CONFIG`
|
|
70
|
+
|
|
8
71
|
## [2.13.0] - 2026-01-20
|
|
9
72
|
|
|
10
73
|
### โจ **New Features**
|
package/dist/helpers.d.ts
CHANGED
|
@@ -60,6 +60,90 @@ export declare function isValidPhone(phone: string): boolean;
|
|
|
60
60
|
* - Other lengths: adds + prefix to all digits
|
|
61
61
|
*/
|
|
62
62
|
export declare function normalizePhone(phone: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Extract domain from email address
|
|
65
|
+
*/
|
|
66
|
+
export declare function extractEmailDomain(email: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Check if two email domains match or are similar
|
|
69
|
+
* Handles:
|
|
70
|
+
* - Exact domain matches
|
|
71
|
+
* - Common aliases (gmail.com vs googlemail.com)
|
|
72
|
+
* - Prefix matching for similar domains (first 3+ characters)
|
|
73
|
+
*
|
|
74
|
+
* @param email1 - First email address
|
|
75
|
+
* @param email2 - Second email address
|
|
76
|
+
* @returns True if domains match or are similar
|
|
77
|
+
*/
|
|
78
|
+
export declare function emailDomainsMatch(email1: string, email2: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Check if two phone numbers are similar
|
|
81
|
+
* Handles:
|
|
82
|
+
* - Different formats (+1, 1, or just 10 digits)
|
|
83
|
+
* - Country code variations
|
|
84
|
+
*
|
|
85
|
+
* @param phone1 - First phone number
|
|
86
|
+
* @param phone2 - Second phone number
|
|
87
|
+
* @returns True if phone numbers are similar
|
|
88
|
+
*/
|
|
89
|
+
export declare function phoneNumbersSimilar(phone1: string, phone2: string): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Validate contact info similarity for name-based matches
|
|
92
|
+
*
|
|
93
|
+
* This is useful when falling back to name-based search to ensure
|
|
94
|
+
* we don't match the wrong person with the same name.
|
|
95
|
+
*
|
|
96
|
+
* @param searchEmail - Email being searched for
|
|
97
|
+
* @param searchPhone - Phone being searched for
|
|
98
|
+
* @param personEmails - Array of email addresses from the person's profile
|
|
99
|
+
* @param personPhones - Array of phone numbers from the person's profile
|
|
100
|
+
* @returns Object with match results and overall validity
|
|
101
|
+
*/
|
|
102
|
+
export declare function validateContactSimilarity(searchEmail: string | undefined, searchPhone: string | undefined, personEmails: string[], personPhones: string[]): {
|
|
103
|
+
emailMatch: boolean;
|
|
104
|
+
phoneMatch: boolean;
|
|
105
|
+
isValid: boolean;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Default trust window for person IDs (1 hour in milliseconds)
|
|
109
|
+
* If a personId was saved within this time, it can be trusted without verification
|
|
110
|
+
*/
|
|
111
|
+
export declare const DEFAULT_TRUST_WINDOW: number;
|
|
112
|
+
/**
|
|
113
|
+
* Result of trust calculation for a person ID
|
|
114
|
+
*/
|
|
115
|
+
export interface TrustResult {
|
|
116
|
+
/** Whether the person ID should be trusted without verification */
|
|
117
|
+
shouldTrust: boolean;
|
|
118
|
+
/** Age of the person ID in milliseconds (null if no timestamp) */
|
|
119
|
+
age: number | null;
|
|
120
|
+
/** Human-readable reason for the trust decision */
|
|
121
|
+
reason: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Calculate whether a person ID can be trusted based on when it was created/verified
|
|
125
|
+
*
|
|
126
|
+
* This is useful for caching person IDs to avoid unnecessary API calls.
|
|
127
|
+
* PCO takes 15-30 minutes to index new contacts, so recently created person IDs
|
|
128
|
+
* should be trusted without re-verification to avoid race conditions.
|
|
129
|
+
*
|
|
130
|
+
* @param createdAt - ISO timestamp when the person ID was created/saved
|
|
131
|
+
* @param trustWindow - Trust window in milliseconds (default: 1 hour)
|
|
132
|
+
* @returns Object with trust decision, age, and reason
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const trust = calculateTrust(pcoInfo.personIdCreatedAt);
|
|
137
|
+
* if (trust.shouldTrust) {
|
|
138
|
+
* // Use cached personId without verification
|
|
139
|
+
* return cachedPersonId;
|
|
140
|
+
* } else {
|
|
141
|
+
* // Verify personId still exists in PCO
|
|
142
|
+
* await client.people.getById(cachedPersonId);
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export declare function calculateTrust(createdAt: string | undefined, trustWindow?: number): TrustResult;
|
|
63
147
|
/**
|
|
64
148
|
* Format person name from attributes
|
|
65
149
|
*/
|
package/dist/helpers.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TRUST_WINDOW = void 0;
|
|
3
4
|
exports.buildQueryParams = buildQueryParams;
|
|
4
5
|
exports.calculateAge = calculateAge;
|
|
5
6
|
exports.calculateAgeSafe = calculateAgeSafe;
|
|
@@ -11,6 +12,11 @@ exports.isValidEmail = isValidEmail;
|
|
|
11
12
|
exports.normalizeEmail = normalizeEmail;
|
|
12
13
|
exports.isValidPhone = isValidPhone;
|
|
13
14
|
exports.normalizePhone = normalizePhone;
|
|
15
|
+
exports.extractEmailDomain = extractEmailDomain;
|
|
16
|
+
exports.emailDomainsMatch = emailDomainsMatch;
|
|
17
|
+
exports.phoneNumbersSimilar = phoneNumbersSimilar;
|
|
18
|
+
exports.validateContactSimilarity = validateContactSimilarity;
|
|
19
|
+
exports.calculateTrust = calculateTrust;
|
|
14
20
|
exports.formatPersonName = formatPersonName;
|
|
15
21
|
exports.formatDate = formatDate;
|
|
16
22
|
exports.validatePersonData = validatePersonData;
|
|
@@ -173,6 +179,200 @@ function normalizePhone(phone) {
|
|
|
173
179
|
}
|
|
174
180
|
return `+${digits}`;
|
|
175
181
|
}
|
|
182
|
+
// ===== Contact Validation Helpers =====
|
|
183
|
+
/**
|
|
184
|
+
* Extract domain from email address
|
|
185
|
+
*/
|
|
186
|
+
function extractEmailDomain(email) {
|
|
187
|
+
const normalized = normalizeEmail(email);
|
|
188
|
+
const atIndex = normalized.indexOf('@');
|
|
189
|
+
return atIndex >= 0 ? normalized.substring(atIndex + 1) : '';
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Common email domain aliases (e.g., gmail.com and googlemail.com are the same)
|
|
193
|
+
*/
|
|
194
|
+
const EMAIL_DOMAIN_ALIASES = {
|
|
195
|
+
'googlemail.com': 'gmail.com',
|
|
196
|
+
'google.com': 'gmail.com',
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Normalize email domain to handle common aliases
|
|
200
|
+
*/
|
|
201
|
+
function normalizeEmailDomain(domain) {
|
|
202
|
+
const lowerDomain = domain.toLowerCase();
|
|
203
|
+
return EMAIL_DOMAIN_ALIASES[lowerDomain] || lowerDomain;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if two email domains match or are similar
|
|
207
|
+
* Handles:
|
|
208
|
+
* - Exact domain matches
|
|
209
|
+
* - Common aliases (gmail.com vs googlemail.com)
|
|
210
|
+
* - Prefix matching for similar domains (first 3+ characters)
|
|
211
|
+
*
|
|
212
|
+
* @param email1 - First email address
|
|
213
|
+
* @param email2 - Second email address
|
|
214
|
+
* @returns True if domains match or are similar
|
|
215
|
+
*/
|
|
216
|
+
function emailDomainsMatch(email1, email2) {
|
|
217
|
+
const domain1 = normalizeEmailDomain(extractEmailDomain(email1));
|
|
218
|
+
const domain2 = normalizeEmailDomain(extractEmailDomain(email2));
|
|
219
|
+
if (!domain1 || !domain2) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
// Exact match after normalization
|
|
223
|
+
if (domain1 === domain2) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
// Check if domains share a common prefix (at least 3 characters)
|
|
227
|
+
// This helps catch typos like "gmial.com" vs "gmail.com"
|
|
228
|
+
const minPrefixLength = 3;
|
|
229
|
+
if (domain1.length >= minPrefixLength && domain2.length >= minPrefixLength) {
|
|
230
|
+
const prefix1 = domain1.substring(0, minPrefixLength);
|
|
231
|
+
const prefix2 = domain2.substring(0, minPrefixLength);
|
|
232
|
+
if (prefix1 === prefix2) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Normalize phone number to digits only, stripping country code if present
|
|
240
|
+
*/
|
|
241
|
+
function normalizePhoneDigits(phone) {
|
|
242
|
+
const digits = phone.replace(/\D/g, '');
|
|
243
|
+
// Strip leading 1 for US numbers (11 digits starting with 1)
|
|
244
|
+
if (digits.length === 11 && digits.startsWith('1')) {
|
|
245
|
+
return digits.substring(1);
|
|
246
|
+
}
|
|
247
|
+
return digits;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Check if two phone numbers are similar
|
|
251
|
+
* Handles:
|
|
252
|
+
* - Different formats (+1, 1, or just 10 digits)
|
|
253
|
+
* - Country code variations
|
|
254
|
+
*
|
|
255
|
+
* @param phone1 - First phone number
|
|
256
|
+
* @param phone2 - Second phone number
|
|
257
|
+
* @returns True if phone numbers are similar
|
|
258
|
+
*/
|
|
259
|
+
function phoneNumbersSimilar(phone1, phone2) {
|
|
260
|
+
if (!phone1 || !phone2) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const normalized1 = normalizePhoneDigits(phone1);
|
|
264
|
+
const normalized2 = normalizePhoneDigits(phone2);
|
|
265
|
+
// Empty after normalization
|
|
266
|
+
if (!normalized1 || !normalized2) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
// Exact match after normalization
|
|
270
|
+
if (normalized1 === normalized2) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
// Also check raw digits (handles international numbers)
|
|
274
|
+
const digits1 = phone1.replace(/\D/g, '');
|
|
275
|
+
const digits2 = phone2.replace(/\D/g, '');
|
|
276
|
+
return digits1 === digits2;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Validate contact info similarity for name-based matches
|
|
280
|
+
*
|
|
281
|
+
* This is useful when falling back to name-based search to ensure
|
|
282
|
+
* we don't match the wrong person with the same name.
|
|
283
|
+
*
|
|
284
|
+
* @param searchEmail - Email being searched for
|
|
285
|
+
* @param searchPhone - Phone being searched for
|
|
286
|
+
* @param personEmails - Array of email addresses from the person's profile
|
|
287
|
+
* @param personPhones - Array of phone numbers from the person's profile
|
|
288
|
+
* @returns Object with match results and overall validity
|
|
289
|
+
*/
|
|
290
|
+
function validateContactSimilarity(searchEmail, searchPhone, personEmails, personPhones) {
|
|
291
|
+
let emailMatch = false;
|
|
292
|
+
let phoneMatch = false;
|
|
293
|
+
// Check email domain match
|
|
294
|
+
if (searchEmail) {
|
|
295
|
+
emailMatch = personEmails.some(personEmail => emailDomainsMatch(searchEmail, personEmail));
|
|
296
|
+
}
|
|
297
|
+
// Check phone similarity
|
|
298
|
+
if (searchPhone) {
|
|
299
|
+
phoneMatch = personPhones.some(personPhone => phoneNumbersSimilar(searchPhone, personPhone));
|
|
300
|
+
}
|
|
301
|
+
// Valid if either email domain matches or phone is similar
|
|
302
|
+
// (or if we didn't have search criteria to check)
|
|
303
|
+
const hasSearchCriteria = !!(searchEmail || searchPhone);
|
|
304
|
+
const isValid = !hasSearchCriteria || emailMatch || phoneMatch;
|
|
305
|
+
return { emailMatch, phoneMatch, isValid };
|
|
306
|
+
}
|
|
307
|
+
// ===== Person ID Trust Calculation =====
|
|
308
|
+
/**
|
|
309
|
+
* Default trust window for person IDs (1 hour in milliseconds)
|
|
310
|
+
* If a personId was saved within this time, it can be trusted without verification
|
|
311
|
+
*/
|
|
312
|
+
exports.DEFAULT_TRUST_WINDOW = 60 * 60 * 1000; // 1 hour
|
|
313
|
+
/**
|
|
314
|
+
* Calculate whether a person ID can be trusted based on when it was created/verified
|
|
315
|
+
*
|
|
316
|
+
* This is useful for caching person IDs to avoid unnecessary API calls.
|
|
317
|
+
* PCO takes 15-30 minutes to index new contacts, so recently created person IDs
|
|
318
|
+
* should be trusted without re-verification to avoid race conditions.
|
|
319
|
+
*
|
|
320
|
+
* @param createdAt - ISO timestamp when the person ID was created/saved
|
|
321
|
+
* @param trustWindow - Trust window in milliseconds (default: 1 hour)
|
|
322
|
+
* @returns Object with trust decision, age, and reason
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```typescript
|
|
326
|
+
* const trust = calculateTrust(pcoInfo.personIdCreatedAt);
|
|
327
|
+
* if (trust.shouldTrust) {
|
|
328
|
+
* // Use cached personId without verification
|
|
329
|
+
* return cachedPersonId;
|
|
330
|
+
* } else {
|
|
331
|
+
* // Verify personId still exists in PCO
|
|
332
|
+
* await client.people.getById(cachedPersonId);
|
|
333
|
+
* }
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
function calculateTrust(createdAt, trustWindow = exports.DEFAULT_TRUST_WINDOW) {
|
|
337
|
+
if (!createdAt) {
|
|
338
|
+
return {
|
|
339
|
+
shouldTrust: false,
|
|
340
|
+
age: null,
|
|
341
|
+
reason: 'No timestamp (legacy data or never saved)',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const createdDate = new Date(createdAt);
|
|
345
|
+
if (isNaN(createdDate.getTime())) {
|
|
346
|
+
return {
|
|
347
|
+
shouldTrust: false,
|
|
348
|
+
age: null,
|
|
349
|
+
reason: 'Invalid timestamp format',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const age = Date.now() - createdDate.getTime();
|
|
353
|
+
if (age < 0) {
|
|
354
|
+
return {
|
|
355
|
+
shouldTrust: false,
|
|
356
|
+
age,
|
|
357
|
+
reason: 'Timestamp is in the future (clock skew)',
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (age < trustWindow) {
|
|
361
|
+
const ageSeconds = Math.round(age / 1000);
|
|
362
|
+
const trustWindowMinutes = Math.round(trustWindow / 1000 / 60);
|
|
363
|
+
return {
|
|
364
|
+
shouldTrust: true,
|
|
365
|
+
age,
|
|
366
|
+
reason: `Fresh personId (${ageSeconds}s old, within ${trustWindowMinutes}min trust window)`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const ageMinutes = Math.round(age / 1000 / 60);
|
|
370
|
+
return {
|
|
371
|
+
shouldTrust: false,
|
|
372
|
+
age,
|
|
373
|
+
reason: `Old personId (${ageMinutes}min old, needs verification)`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
176
376
|
/**
|
|
177
377
|
* Format person name from attributes
|
|
178
378
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,10 @@ export type { ErrorContext } from '@rachelallyson/planning-center-base-ts';
|
|
|
20
20
|
export { ErrorCategory, ErrorSeverity, handleNetworkError, handleTimeoutError, handleValidationError, PcoError, retryWithBackoff, shouldNotRetry, withErrorBoundary, } from '@rachelallyson/planning-center-base-ts';
|
|
21
21
|
export { createFieldDefinition, createFieldOption, createPerson, createPersonAddress, createPersonEmail, createPersonFieldData, createPersonPhoneNumber, createPersonSocialProfile, createWorkflowCard, createWorkflowCardNote, deleteFieldDefinition, deletePerson, deletePersonFieldData, deleteSocialProfile, getFieldDefinitions, getFieldOptions, getHousehold, getHouseholds, getTabs, getListById, getListCategories, getLists, runList, getNote, getNoteCategories, getNotes, getOrganization, getPeople, getPerson, getPersonAddresses, getPersonEmails, getPersonFieldData, getPersonPhoneNumbers, getPersonSocialProfiles, getWorkflow, getWorkflowCardNotes, getWorkflowCards, getWorkflows, updatePerson, updatePersonAddress, } from './people';
|
|
22
22
|
export { attemptRecovery, CircuitBreaker, classifyError, createErrorReport, DEFAULT_RETRY_CONFIG, executeBulkOperation, retryWithExponentialBackoff, TIMEOUT_CONFIG, withTimeout, } from './error-scenarios';
|
|
23
|
-
export { buildQueryParams, calculateAge, createPersonWithContact, createWorkflowCardWithNote, exportAllPeopleData, extractFileUrl, formatDate, formatPersonName, getCompletePersonProfile, getFileExtension, getFilename, getListsWithCategories, getOrganizationInfo, getPeopleByHousehold, getPersonWorkflowCardsWithNotes, getPrimaryContact, isFileUpload, isFileUrl, isValidEmail, isValidPhone, normalizeEmail, normalizePhone, processFileValue, searchPeople, validatePersonData, } from './helpers';
|
|
23
|
+
export { buildQueryParams, calculateAge, calculateTrust, createPersonWithContact, createWorkflowCardWithNote, DEFAULT_TRUST_WINDOW, emailDomainsMatch, exportAllPeopleData, extractEmailDomain, extractFileUrl, formatDate, formatPersonName, getCompletePersonProfile, getFileExtension, getFilename, getListsWithCategories, getOrganizationInfo, getPeopleByHousehold, getPersonWorkflowCardsWithNotes, getPrimaryContact, isFileUpload, isFileUrl, isValidEmail, isValidPhone, normalizeEmail, normalizePhone, phoneNumbersSimilar, processFileValue, searchPeople, validateContactSimilarity, validatePersonData, } from './helpers';
|
|
24
|
+
export type { TrustResult } from './helpers';
|
|
25
|
+
export { DEFAULT_INITIAL_RETRY_CONFIG, DEFAULT_AGGRESSIVE_RETRY_CONFIG, } from './modules/people';
|
|
26
|
+
export type { RetryConfig, PersonMatchOptions } from './modules/people';
|
|
24
27
|
export { AdaptiveRateLimiter, ApiCache, batchFetchPersonDetails, fetchAllPages, getCachedPeople, monitorPerformance, PerformanceMonitor, processInBatches, processLargeDataset, streamPeopleData, } from './performance';
|
|
25
28
|
export { MockPcoClient, MockResponseBuilder, RequestRecorder, createMockClient, createRecordingClient, createTestClient, createErrorMockClient, createSlowMockClient, } from './testing';
|
|
26
29
|
export type { MockClientConfig, RecordingConfig } from './testing';
|
package/dist/index.js
CHANGED
|
@@ -16,8 +16,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.getNoteCategories = exports.getNote = exports.runList = exports.getLists = exports.getListCategories = exports.getListById = exports.getTabs = exports.getHouseholds = exports.getHousehold = exports.getFieldOptions = exports.getFieldDefinitions = exports.deleteSocialProfile = exports.deletePersonFieldData = exports.deletePerson = exports.deleteFieldDefinition = exports.createWorkflowCardNote = exports.createWorkflowCard = exports.createPersonSocialProfile = exports.createPersonPhoneNumber = exports.createPersonFieldData = exports.createPersonEmail = exports.createPersonAddress = exports.createPerson = exports.createFieldOption = exports.createFieldDefinition = exports.withErrorBoundary = exports.shouldNotRetry = exports.retryWithBackoff = exports.PcoError = exports.handleValidationError = exports.handleTimeoutError = exports.handleNetworkError = exports.ErrorSeverity = exports.ErrorCategory = exports.PcoRateLimiter = exports.PcoApiError = exports.updateClientTokens = exports.refreshAccessToken = exports.hasRefreshTokenCapability = exports.attemptTokenRefresh = exports.post = exports.patch = exports.getSingle = exports.getRateLimitInfo = exports.getList = exports.getAllPages = exports.del = exports.createPcoClient = exports.PcoClientManager = exports.PcoClient = void 0;
|
|
19
|
-
exports.
|
|
20
|
-
exports.createSlowMockClient = exports.createErrorMockClient = exports.createTestClient = exports.createRecordingClient = exports.createMockClient = exports.RequestRecorder = exports.MockResponseBuilder = exports.MockPcoClient = exports.streamPeopleData = exports.processLargeDataset = exports.processInBatches = exports.PerformanceMonitor = exports.monitorPerformance = exports.getCachedPeople = exports.fetchAllPages = exports.batchFetchPersonDetails = exports.ApiCache = void 0;
|
|
19
|
+
exports.normalizePhone = exports.normalizeEmail = exports.isValidPhone = exports.isValidEmail = exports.isFileUrl = exports.isFileUpload = exports.getPrimaryContact = exports.getPersonWorkflowCardsWithNotes = exports.getPeopleByHousehold = exports.getOrganizationInfo = exports.getListsWithCategories = exports.getFilename = exports.getFileExtension = exports.getCompletePersonProfile = exports.formatPersonName = exports.formatDate = exports.extractFileUrl = exports.extractEmailDomain = exports.exportAllPeopleData = exports.emailDomainsMatch = exports.DEFAULT_TRUST_WINDOW = exports.createWorkflowCardWithNote = exports.createPersonWithContact = exports.calculateTrust = exports.calculateAge = exports.buildQueryParams = exports.withTimeout = exports.TIMEOUT_CONFIG = exports.retryWithExponentialBackoff = exports.executeBulkOperation = exports.DEFAULT_RETRY_CONFIG = exports.createErrorReport = exports.classifyError = exports.CircuitBreaker = exports.attemptRecovery = exports.updatePersonAddress = exports.updatePerson = exports.getWorkflows = exports.getWorkflowCards = exports.getWorkflowCardNotes = exports.getWorkflow = exports.getPersonSocialProfiles = exports.getPersonPhoneNumbers = exports.getPersonFieldData = exports.getPersonEmails = exports.getPersonAddresses = exports.getPerson = exports.getPeople = exports.getOrganization = exports.getNotes = void 0;
|
|
20
|
+
exports.createSlowMockClient = exports.createErrorMockClient = exports.createTestClient = exports.createRecordingClient = exports.createMockClient = exports.RequestRecorder = exports.MockResponseBuilder = exports.MockPcoClient = exports.streamPeopleData = exports.processLargeDataset = exports.processInBatches = exports.PerformanceMonitor = exports.monitorPerformance = exports.getCachedPeople = exports.fetchAllPages = exports.batchFetchPersonDetails = exports.ApiCache = exports.AdaptiveRateLimiter = exports.DEFAULT_AGGRESSIVE_RETRY_CONFIG = exports.DEFAULT_INITIAL_RETRY_CONFIG = exports.validatePersonData = exports.validateContactSimilarity = exports.searchPeople = exports.processFileValue = exports.phoneNumbersSimilar = void 0;
|
|
21
21
|
// Main client class
|
|
22
22
|
var client_1 = require("./client");
|
|
23
23
|
Object.defineProperty(exports, "PcoClient", { enumerable: true, get: function () { return client_1.PcoClient; } });
|
|
@@ -113,9 +113,13 @@ Object.defineProperty(exports, "withTimeout", { enumerable: true, get: function
|
|
|
113
113
|
var helpers_1 = require("./helpers");
|
|
114
114
|
Object.defineProperty(exports, "buildQueryParams", { enumerable: true, get: function () { return helpers_1.buildQueryParams; } });
|
|
115
115
|
Object.defineProperty(exports, "calculateAge", { enumerable: true, get: function () { return helpers_1.calculateAge; } });
|
|
116
|
+
Object.defineProperty(exports, "calculateTrust", { enumerable: true, get: function () { return helpers_1.calculateTrust; } });
|
|
116
117
|
Object.defineProperty(exports, "createPersonWithContact", { enumerable: true, get: function () { return helpers_1.createPersonWithContact; } });
|
|
117
118
|
Object.defineProperty(exports, "createWorkflowCardWithNote", { enumerable: true, get: function () { return helpers_1.createWorkflowCardWithNote; } });
|
|
119
|
+
Object.defineProperty(exports, "DEFAULT_TRUST_WINDOW", { enumerable: true, get: function () { return helpers_1.DEFAULT_TRUST_WINDOW; } });
|
|
120
|
+
Object.defineProperty(exports, "emailDomainsMatch", { enumerable: true, get: function () { return helpers_1.emailDomainsMatch; } });
|
|
118
121
|
Object.defineProperty(exports, "exportAllPeopleData", { enumerable: true, get: function () { return helpers_1.exportAllPeopleData; } });
|
|
122
|
+
Object.defineProperty(exports, "extractEmailDomain", { enumerable: true, get: function () { return helpers_1.extractEmailDomain; } });
|
|
119
123
|
Object.defineProperty(exports, "extractFileUrl", { enumerable: true, get: function () { return helpers_1.extractFileUrl; } });
|
|
120
124
|
Object.defineProperty(exports, "formatDate", { enumerable: true, get: function () { return helpers_1.formatDate; } });
|
|
121
125
|
Object.defineProperty(exports, "formatPersonName", { enumerable: true, get: function () { return helpers_1.formatPersonName; } });
|
|
@@ -133,9 +137,15 @@ Object.defineProperty(exports, "isValidEmail", { enumerable: true, get: function
|
|
|
133
137
|
Object.defineProperty(exports, "isValidPhone", { enumerable: true, get: function () { return helpers_1.isValidPhone; } });
|
|
134
138
|
Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: function () { return helpers_1.normalizeEmail; } });
|
|
135
139
|
Object.defineProperty(exports, "normalizePhone", { enumerable: true, get: function () { return helpers_1.normalizePhone; } });
|
|
140
|
+
Object.defineProperty(exports, "phoneNumbersSimilar", { enumerable: true, get: function () { return helpers_1.phoneNumbersSimilar; } });
|
|
136
141
|
Object.defineProperty(exports, "processFileValue", { enumerable: true, get: function () { return helpers_1.processFileValue; } });
|
|
137
142
|
Object.defineProperty(exports, "searchPeople", { enumerable: true, get: function () { return helpers_1.searchPeople; } });
|
|
143
|
+
Object.defineProperty(exports, "validateContactSimilarity", { enumerable: true, get: function () { return helpers_1.validateContactSimilarity; } });
|
|
138
144
|
Object.defineProperty(exports, "validatePersonData", { enumerable: true, get: function () { return helpers_1.validatePersonData; } });
|
|
145
|
+
// ===== Matching Configuration =====
|
|
146
|
+
var people_2 = require("./modules/people");
|
|
147
|
+
Object.defineProperty(exports, "DEFAULT_INITIAL_RETRY_CONFIG", { enumerable: true, get: function () { return people_2.DEFAULT_INITIAL_RETRY_CONFIG; } });
|
|
148
|
+
Object.defineProperty(exports, "DEFAULT_AGGRESSIVE_RETRY_CONFIG", { enumerable: true, get: function () { return people_2.DEFAULT_AGGRESSIVE_RETRY_CONFIG; } });
|
|
139
149
|
// ===== Performance Optimization =====
|
|
140
150
|
var performance_1 = require("./performance");
|
|
141
151
|
Object.defineProperty(exports, "AdaptiveRateLimiter", { enumerable: true, get: function () { return performance_1.AdaptiveRateLimiter; } });
|
|
@@ -15,6 +15,10 @@ export declare class PersonMatcher {
|
|
|
15
15
|
private strategies;
|
|
16
16
|
private scorer;
|
|
17
17
|
constructor(peopleModule: PeopleModule);
|
|
18
|
+
/**
|
|
19
|
+
* Resolve retry configuration from options, with defaults
|
|
20
|
+
*/
|
|
21
|
+
private resolveRetryConfig;
|
|
18
22
|
/**
|
|
19
23
|
* Find or create a person with smart matching
|
|
20
24
|
*
|
|
@@ -23,23 +27,66 @@ export declare class PersonMatcher {
|
|
|
23
27
|
* - Only uses name matching when appropriate (multiple people share contact info, or no contact info provided)
|
|
24
28
|
* - Can automatically add missing contact information when a match is found (if addMissingContactInfo is true)
|
|
25
29
|
* - Retries with exponential backoff when contacts may not be verified yet (PCO takes 30-90+ seconds)
|
|
30
|
+
* - Supports multi-step search strategy for maximum matching success
|
|
26
31
|
*
|
|
27
32
|
* @param options - Matching options
|
|
28
33
|
* @param options.addMissingContactInfo - If true, automatically adds missing email/phone to matched person's profile
|
|
29
34
|
* @param options.retryConfig - Configuration for retry logic to handle PCO contact verification delays
|
|
35
|
+
* @param options.searchStrategy - 'single' for standard search, 'multi-step' for trying multiple strategies
|
|
30
36
|
*/
|
|
31
37
|
findOrCreate(options: PersonMatchOptions): Promise<PersonResource>;
|
|
38
|
+
/**
|
|
39
|
+
* Final aggressive search before creating a new person
|
|
40
|
+
*
|
|
41
|
+
* This is a safeguard to prevent duplicate person creation when:
|
|
42
|
+
* - PCO hasn't indexed contacts yet (15-30 minute delay)
|
|
43
|
+
* - Multiple workers are processing the same person
|
|
44
|
+
* - The fast path didn't find an existing person
|
|
45
|
+
*/
|
|
46
|
+
private findWithAggressiveRetry;
|
|
47
|
+
/**
|
|
48
|
+
* Multi-step search strategy configuration
|
|
49
|
+
*/
|
|
50
|
+
private static readonly MULTI_STEP_STRATEGIES;
|
|
51
|
+
/**
|
|
52
|
+
* Find a match using multi-step search strategy
|
|
53
|
+
*
|
|
54
|
+
* Tries multiple matching strategies in order until a match is found:
|
|
55
|
+
* 1. Fuzzy with age preference (handles name variations, prefers adults)
|
|
56
|
+
* 2. Fuzzy without age preference (catches single matches filtered by age)
|
|
57
|
+
* 3. Exact with age preference (high confidence, prefers adults)
|
|
58
|
+
* 4. Exact without age preference (high confidence, any age)
|
|
59
|
+
*
|
|
60
|
+
* This approach maximizes matching success while maintaining quality.
|
|
61
|
+
*/
|
|
62
|
+
findMatchMultiStep(options: PersonMatchOptions): Promise<MatchResult | null>;
|
|
32
63
|
/**
|
|
33
64
|
* Find or create with retry logic to handle PCO contact verification delays
|
|
34
65
|
*
|
|
35
66
|
* PCO takes 30-90+ seconds to verify/index contacts after a person is created.
|
|
36
67
|
* This method retries with exponential backoff to give PCO time to process contacts.
|
|
68
|
+
*
|
|
69
|
+
* Supports phase-specific retry configurations via retryConfigs:
|
|
70
|
+
* - initial: Quick search (default 30s)
|
|
71
|
+
* - aggressive: Final search before create (default 60s)
|
|
37
72
|
*/
|
|
38
73
|
private findOrCreateWithRetry;
|
|
39
74
|
/**
|
|
40
75
|
* Find the best match for a person
|
|
41
76
|
*/
|
|
42
77
|
findMatch(options: PersonMatchOptions): Promise<MatchResult | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Find a match by name with contact validation
|
|
80
|
+
*
|
|
81
|
+
* This is a fallback when email/phone search fails. It searches by name
|
|
82
|
+
* but validates that the person's contact info is similar to prevent
|
|
83
|
+
* wrong-person matches (e.g., two people named "John Smith").
|
|
84
|
+
*/
|
|
85
|
+
private findMatchByNameWithContactValidation;
|
|
86
|
+
/**
|
|
87
|
+
* Validate a candidate's contact info based on the validation strategy
|
|
88
|
+
*/
|
|
89
|
+
private validateCandidateContact;
|
|
43
90
|
/**
|
|
44
91
|
* Get potential matching candidates
|
|
45
92
|
* @deprecated Use findMatch which has improved logic for separating verified matches from name-only matches
|
package/dist/matching/matcher.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PersonMatcher = void 0;
|
|
7
|
+
const people_1 = require("../modules/people");
|
|
7
8
|
const strategies_1 = require("./strategies");
|
|
8
9
|
const scoring_1 = require("./scoring");
|
|
9
10
|
const helpers_1 = require("../helpers");
|
|
@@ -13,6 +14,19 @@ class PersonMatcher {
|
|
|
13
14
|
this.strategies = new strategies_1.MatchStrategies();
|
|
14
15
|
this.scorer = new scoring_1.MatchScorer(peopleModule);
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve retry configuration from options, with defaults
|
|
19
|
+
*/
|
|
20
|
+
resolveRetryConfig(explicitConfig, phaseConfig, defaults = people_1.DEFAULT_INITIAL_RETRY_CONFIG) {
|
|
21
|
+
// Phase config takes precedence over explicit config
|
|
22
|
+
const config = phaseConfig || explicitConfig;
|
|
23
|
+
return {
|
|
24
|
+
maxRetries: config?.maxRetries ?? defaults.maxRetries,
|
|
25
|
+
maxWaitTime: config?.maxWaitTime ?? defaults.maxWaitTime,
|
|
26
|
+
initialDelay: config?.initialDelay ?? defaults.initialDelay,
|
|
27
|
+
backoffMultiplier: config?.backoffMultiplier ?? defaults.backoffMultiplier,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
16
30
|
/**
|
|
17
31
|
* Find or create a person with smart matching
|
|
18
32
|
*
|
|
@@ -21,13 +35,15 @@ class PersonMatcher {
|
|
|
21
35
|
* - Only uses name matching when appropriate (multiple people share contact info, or no contact info provided)
|
|
22
36
|
* - Can automatically add missing contact information when a match is found (if addMissingContactInfo is true)
|
|
23
37
|
* - Retries with exponential backoff when contacts may not be verified yet (PCO takes 30-90+ seconds)
|
|
38
|
+
* - Supports multi-step search strategy for maximum matching success
|
|
24
39
|
*
|
|
25
40
|
* @param options - Matching options
|
|
26
41
|
* @param options.addMissingContactInfo - If true, automatically adds missing email/phone to matched person's profile
|
|
27
42
|
* @param options.retryConfig - Configuration for retry logic to handle PCO contact verification delays
|
|
43
|
+
* @param options.searchStrategy - 'single' for standard search, 'multi-step' for trying multiple strategies
|
|
28
44
|
*/
|
|
29
45
|
async findOrCreate(options) {
|
|
30
|
-
const { createIfNotFound = true, matchStrategy = 'fuzzy', addMissingContactInfo = false, retryConfig, ...searchOptions } = options;
|
|
46
|
+
const { createIfNotFound = true, matchStrategy = 'fuzzy', searchStrategy = 'single', addMissingContactInfo = false, retryConfig, ...searchOptions } = options;
|
|
31
47
|
// Determine if retry logic should be enabled
|
|
32
48
|
// Retry is useful when:
|
|
33
49
|
// 1. We have email/phone (these need verification)
|
|
@@ -40,8 +56,14 @@ class PersonMatcher {
|
|
|
40
56
|
if (shouldRetry) {
|
|
41
57
|
return this.findOrCreateWithRetry(options);
|
|
42
58
|
}
|
|
43
|
-
// Try to find existing person
|
|
44
|
-
|
|
59
|
+
// Try to find existing person using appropriate search strategy
|
|
60
|
+
let match = null;
|
|
61
|
+
if (searchStrategy === 'multi-step') {
|
|
62
|
+
match = await this.findMatchMultiStep(options);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
match = await this.findMatch({ ...searchOptions, matchStrategy });
|
|
66
|
+
}
|
|
45
67
|
if (match) {
|
|
46
68
|
const person = match.person;
|
|
47
69
|
// Add missing contact information if requested
|
|
@@ -52,29 +74,150 @@ class PersonMatcher {
|
|
|
52
74
|
}
|
|
53
75
|
// Create new person if not found and creation is enabled
|
|
54
76
|
if (createIfNotFound) {
|
|
77
|
+
// If aggressive retry config is provided, do a final aggressive search before creating
|
|
78
|
+
// This is a safeguard to prevent duplicates when PCO hasn't indexed contacts yet
|
|
79
|
+
if (options.retryConfigs?.aggressive) {
|
|
80
|
+
const aggressiveMatch = await this.findWithAggressiveRetry(options);
|
|
81
|
+
if (aggressiveMatch) {
|
|
82
|
+
const person = aggressiveMatch.person;
|
|
83
|
+
if (addMissingContactInfo) {
|
|
84
|
+
await this.addMissingContactInfo(person, options);
|
|
85
|
+
}
|
|
86
|
+
return person;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
55
89
|
return this.createPerson(options);
|
|
56
90
|
}
|
|
57
91
|
throw new Error(`No matching person found and creation is disabled`);
|
|
58
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Final aggressive search before creating a new person
|
|
95
|
+
*
|
|
96
|
+
* This is a safeguard to prevent duplicate person creation when:
|
|
97
|
+
* - PCO hasn't indexed contacts yet (15-30 minute delay)
|
|
98
|
+
* - Multiple workers are processing the same person
|
|
99
|
+
* - The fast path didn't find an existing person
|
|
100
|
+
*/
|
|
101
|
+
async findWithAggressiveRetry(options) {
|
|
102
|
+
const { matchStrategy = 'fuzzy', searchStrategy = 'single', retryConfigs } = options;
|
|
103
|
+
const aggressiveConfig = this.resolveRetryConfig(undefined, retryConfigs?.aggressive, people_1.DEFAULT_AGGRESSIVE_RETRY_CONFIG);
|
|
104
|
+
console.log(`[PERSON_MATCH] Starting aggressive final search before create`, {
|
|
105
|
+
maxWaitTime: aggressiveConfig.maxWaitTime,
|
|
106
|
+
maxRetries: aggressiveConfig.maxRetries,
|
|
107
|
+
});
|
|
108
|
+
let totalWaitTime = 0;
|
|
109
|
+
for (let attempt = 1; attempt <= aggressiveConfig.maxRetries; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
let match = null;
|
|
112
|
+
if (searchStrategy === 'multi-step') {
|
|
113
|
+
match = await this.findMatchMultiStep(options);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
match = await this.findMatch({ ...options, matchStrategy });
|
|
117
|
+
}
|
|
118
|
+
if (match) {
|
|
119
|
+
console.log(`[PERSON_MATCH] Aggressive search found person (would have created duplicate)`, {
|
|
120
|
+
personId: match.person.id,
|
|
121
|
+
attempt,
|
|
122
|
+
totalWaitTime,
|
|
123
|
+
});
|
|
124
|
+
return match;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.warn(`[PERSON_MATCH] Aggressive search attempt ${attempt} failed:`, error);
|
|
129
|
+
}
|
|
130
|
+
// Don't wait on the last attempt
|
|
131
|
+
if (attempt === aggressiveConfig.maxRetries) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
// Calculate delay with exponential backoff
|
|
135
|
+
const delay = Math.min(aggressiveConfig.initialDelay * Math.pow(aggressiveConfig.backoffMultiplier, attempt - 1), aggressiveConfig.maxWaitTime - totalWaitTime);
|
|
136
|
+
if (totalWaitTime + delay > aggressiveConfig.maxWaitTime) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
totalWaitTime += delay;
|
|
140
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
141
|
+
}
|
|
142
|
+
console.log(`[PERSON_MATCH] Aggressive search completed - no match found, safe to create`, {
|
|
143
|
+
totalWaitTime,
|
|
144
|
+
maxRetries: aggressiveConfig.maxRetries,
|
|
145
|
+
});
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Find a match using multi-step search strategy
|
|
150
|
+
*
|
|
151
|
+
* Tries multiple matching strategies in order until a match is found:
|
|
152
|
+
* 1. Fuzzy with age preference (handles name variations, prefers adults)
|
|
153
|
+
* 2. Fuzzy without age preference (catches single matches filtered by age)
|
|
154
|
+
* 3. Exact with age preference (high confidence, prefers adults)
|
|
155
|
+
* 4. Exact without age preference (high confidence, any age)
|
|
156
|
+
*
|
|
157
|
+
* This approach maximizes matching success while maintaining quality.
|
|
158
|
+
*/
|
|
159
|
+
async findMatchMultiStep(options) {
|
|
160
|
+
const { agePreference, agePreferenceLenient, ...baseOptions } = options;
|
|
161
|
+
for (const strategy of PersonMatcher.MULTI_STEP_STRATEGIES) {
|
|
162
|
+
try {
|
|
163
|
+
const searchOptions = {
|
|
164
|
+
...baseOptions,
|
|
165
|
+
matchStrategy: strategy.matchStrategy,
|
|
166
|
+
};
|
|
167
|
+
// Apply age preference only when specified by strategy
|
|
168
|
+
if (strategy.useAgePreference && agePreference) {
|
|
169
|
+
searchOptions.agePreference = agePreference;
|
|
170
|
+
// Use lenient mode when specified, so profiles without birthdates are included
|
|
171
|
+
searchOptions.agePreferenceLenient = agePreferenceLenient ?? true;
|
|
172
|
+
}
|
|
173
|
+
const match = await this.findMatch(searchOptions);
|
|
174
|
+
if (match) {
|
|
175
|
+
console.log(`[PERSON_MATCH] Multi-step search found match using ${strategy.description}`, {
|
|
176
|
+
personId: match.person.id,
|
|
177
|
+
score: match.score,
|
|
178
|
+
reason: match.reason,
|
|
179
|
+
});
|
|
180
|
+
return match;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
// Log but continue to next strategy
|
|
185
|
+
console.warn(`[PERSON_MATCH] Multi-step strategy "${strategy.description}" failed:`, error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
59
190
|
/**
|
|
60
191
|
* Find or create with retry logic to handle PCO contact verification delays
|
|
61
192
|
*
|
|
62
193
|
* PCO takes 30-90+ seconds to verify/index contacts after a person is created.
|
|
63
194
|
* This method retries with exponential backoff to give PCO time to process contacts.
|
|
195
|
+
*
|
|
196
|
+
* Supports phase-specific retry configurations via retryConfigs:
|
|
197
|
+
* - initial: Quick search (default 30s)
|
|
198
|
+
* - aggressive: Final search before create (default 60s)
|
|
64
199
|
*/
|
|
65
200
|
async findOrCreateWithRetry(options) {
|
|
66
|
-
const { createIfNotFound = false, matchStrategy = 'fuzzy', addMissingContactInfo = false, retryConfig, ...searchOptions } = options;
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
201
|
+
const { createIfNotFound = false, matchStrategy = 'fuzzy', searchStrategy = 'single', addMissingContactInfo = false, retryConfig, retryConfigs, ...searchOptions } = options;
|
|
202
|
+
// Determine which retry configuration to use
|
|
203
|
+
// Priority: retryConfigs.initial > retryConfig > defaults
|
|
204
|
+
const effectiveRetryConfig = this.resolveRetryConfig(retryConfig, retryConfigs?.initial);
|
|
205
|
+
const maxRetries = effectiveRetryConfig.maxRetries;
|
|
206
|
+
const maxWaitTime = effectiveRetryConfig.maxWaitTime;
|
|
207
|
+
const initialDelay = effectiveRetryConfig.initialDelay;
|
|
208
|
+
const backoffMultiplier = effectiveRetryConfig.backoffMultiplier;
|
|
72
209
|
let totalWaitTime = 0;
|
|
73
210
|
let lastError = null;
|
|
74
211
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
75
212
|
try {
|
|
76
|
-
// Try to find existing person
|
|
77
|
-
|
|
213
|
+
// Try to find existing person using appropriate search strategy
|
|
214
|
+
let match = null;
|
|
215
|
+
if (searchStrategy === 'multi-step') {
|
|
216
|
+
match = await this.findMatchMultiStep(options);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
match = await this.findMatch({ ...searchOptions, matchStrategy });
|
|
220
|
+
}
|
|
78
221
|
if (match) {
|
|
79
222
|
const person = match.person;
|
|
80
223
|
// Add missing contact information if requested
|
|
@@ -86,7 +229,8 @@ class PersonMatcher {
|
|
|
86
229
|
console.log(`[PERSON_MATCH] Found person after ${attempt} attempts (waited ${totalWaitTime}ms)`, {
|
|
87
230
|
personId: person.id,
|
|
88
231
|
attempt,
|
|
89
|
-
totalWaitTime
|
|
232
|
+
totalWaitTime,
|
|
233
|
+
searchStrategy,
|
|
90
234
|
});
|
|
91
235
|
}
|
|
92
236
|
return person;
|
|
@@ -133,7 +277,7 @@ class PersonMatcher {
|
|
|
133
277
|
* Find the best match for a person
|
|
134
278
|
*/
|
|
135
279
|
async findMatch(options) {
|
|
136
|
-
const { matchStrategy = 'fuzzy', email, phone, firstName, lastName } = options;
|
|
280
|
+
const { matchStrategy = 'fuzzy', email, phone, firstName, lastName, fallbackToNameSearch = false, contactValidation = 'similarity' } = options;
|
|
137
281
|
// Step 1: Try email/phone search first
|
|
138
282
|
const emailPhoneMatches = [];
|
|
139
283
|
const nameOnlyMatches = [];
|
|
@@ -231,11 +375,124 @@ class PersonMatcher {
|
|
|
231
375
|
// For "exact" strategy, only return verified email/phone matches
|
|
232
376
|
if (matchStrategy === 'exact') {
|
|
233
377
|
const exactMatches = scoredCandidates.filter(c => c.isVerifiedContactMatch && c.score >= 0.8);
|
|
234
|
-
|
|
378
|
+
if (exactMatches.length > 0) {
|
|
379
|
+
return exactMatches[0];
|
|
380
|
+
}
|
|
381
|
+
// Continue to fallback if enabled
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
// Apply strategy-specific filtering
|
|
385
|
+
const bestMatch = this.strategies.selectBestMatch(scoredCandidates, matchStrategy);
|
|
386
|
+
if (bestMatch) {
|
|
387
|
+
return bestMatch;
|
|
388
|
+
}
|
|
389
|
+
// Continue to fallback if enabled
|
|
390
|
+
}
|
|
391
|
+
// Step 5: Fallback to name-based search with contact validation
|
|
392
|
+
// Only used when email/phone search fails to find a match
|
|
393
|
+
if (fallbackToNameSearch && firstName && lastName && (email || phone)) {
|
|
394
|
+
const nameSearchMatch = await this.findMatchByNameWithContactValidation(firstName, lastName, email, phone, contactValidation, options);
|
|
395
|
+
if (nameSearchMatch) {
|
|
396
|
+
return nameSearchMatch;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Find a match by name with contact validation
|
|
403
|
+
*
|
|
404
|
+
* This is a fallback when email/phone search fails. It searches by name
|
|
405
|
+
* but validates that the person's contact info is similar to prevent
|
|
406
|
+
* wrong-person matches (e.g., two people named "John Smith").
|
|
407
|
+
*/
|
|
408
|
+
async findMatchByNameWithContactValidation(firstName, lastName, searchEmail, searchPhone, validationStrategy, options) {
|
|
409
|
+
try {
|
|
410
|
+
console.log(`[PERSON_MATCH] Attempting name-based search fallback for ${firstName} ${lastName}`);
|
|
411
|
+
const nameResults = await this.peopleModule.search({
|
|
412
|
+
name: `${firstName} ${lastName}`
|
|
413
|
+
});
|
|
414
|
+
if (nameResults.data.length === 0) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
// Validate each candidate's contact info
|
|
418
|
+
for (const candidate of nameResults.data) {
|
|
419
|
+
const isValid = await this.validateCandidateContact(candidate, searchEmail, searchPhone, validationStrategy);
|
|
420
|
+
if (isValid) {
|
|
421
|
+
const score = await this.scorer.scoreMatch(candidate, options);
|
|
422
|
+
const reason = await this.scorer.getMatchReason(candidate, options);
|
|
423
|
+
console.log(`[PERSON_MATCH] Name-based search found validated match`, {
|
|
424
|
+
personId: candidate.id,
|
|
425
|
+
validationStrategy,
|
|
426
|
+
score,
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
person: candidate,
|
|
430
|
+
score,
|
|
431
|
+
reason: `name match with ${validationStrategy} contact validation, ${reason}`,
|
|
432
|
+
isVerifiedContactMatch: false, // Not a direct contact match
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
console.log(`[PERSON_MATCH] Name-based search found candidates but none passed contact validation`, {
|
|
437
|
+
candidateCount: nameResults.data.length,
|
|
438
|
+
validationStrategy,
|
|
439
|
+
});
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
console.warn('[PERSON_MATCH] Name-based search fallback failed:', error);
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Validate a candidate's contact info based on the validation strategy
|
|
449
|
+
*/
|
|
450
|
+
async validateCandidateContact(candidate, searchEmail, searchPhone, validationStrategy) {
|
|
451
|
+
try {
|
|
452
|
+
// Get person's contact info
|
|
453
|
+
const [personEmails, personPhones] = await Promise.all([
|
|
454
|
+
this.peopleModule.getEmails(candidate.id).then(r => r.data?.map(e => e.attributes?.address || '').filter(Boolean) || []).catch(() => []),
|
|
455
|
+
this.peopleModule.getPhoneNumbers(candidate.id).then(r => r.data?.map(p => p.attributes?.number || '').filter(Boolean) || []).catch(() => []),
|
|
456
|
+
]);
|
|
457
|
+
switch (validationStrategy) {
|
|
458
|
+
case 'strict':
|
|
459
|
+
// Require exact match
|
|
460
|
+
if (searchEmail) {
|
|
461
|
+
const normalizedSearch = (0, helpers_1.normalizeEmail)(searchEmail);
|
|
462
|
+
if (personEmails.some(e => (0, helpers_1.normalizeEmail)(e) === normalizedSearch)) {
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (searchPhone) {
|
|
467
|
+
const normalizedSearch = (0, helpers_1.normalizePhone)(searchPhone);
|
|
468
|
+
if (personPhones.some(p => (0, helpers_1.normalizePhone)(p) === normalizedSearch)) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return false;
|
|
473
|
+
case 'domain':
|
|
474
|
+
// Require domain match for email or exact match for phone
|
|
475
|
+
if (searchEmail && personEmails.some(e => (0, helpers_1.emailDomainsMatch)(searchEmail, e))) {
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
if (searchPhone) {
|
|
479
|
+
const normalizedSearch = (0, helpers_1.normalizePhone)(searchPhone);
|
|
480
|
+
if (personPhones.some(p => (0, helpers_1.normalizePhone)(p) === normalizedSearch)) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
case 'similarity':
|
|
486
|
+
default:
|
|
487
|
+
// Use domain matching for email and similarity for phone
|
|
488
|
+
const validation = (0, helpers_1.validateContactSimilarity)(searchEmail, searchPhone, personEmails, personPhones);
|
|
489
|
+
return validation.isValid;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
console.warn(`[PERSON_MATCH] Contact validation failed for person ${candidate.id}:`, error);
|
|
494
|
+
return false;
|
|
235
495
|
}
|
|
236
|
-
// Apply strategy-specific filtering
|
|
237
|
-
const bestMatch = this.strategies.selectBestMatch(scoredCandidates, matchStrategy);
|
|
238
|
-
return bestMatch;
|
|
239
496
|
}
|
|
240
497
|
/**
|
|
241
498
|
* Get potential matching candidates
|
|
@@ -532,3 +789,12 @@ class PersonMatcher {
|
|
|
532
789
|
}
|
|
533
790
|
}
|
|
534
791
|
exports.PersonMatcher = PersonMatcher;
|
|
792
|
+
/**
|
|
793
|
+
* Multi-step search strategy configuration
|
|
794
|
+
*/
|
|
795
|
+
PersonMatcher.MULTI_STEP_STRATEGIES = [
|
|
796
|
+
{ matchStrategy: 'fuzzy', useAgePreference: true, description: 'fuzzy with age preference' },
|
|
797
|
+
{ matchStrategy: 'fuzzy', useAgePreference: false, description: 'fuzzy without age preference' },
|
|
798
|
+
{ matchStrategy: 'exact', useAgePreference: true, description: 'exact with age preference' },
|
|
799
|
+
{ matchStrategy: 'exact', useAgePreference: false, description: 'exact without age preference' },
|
|
800
|
+
];
|
package/dist/modules/people.d.ts
CHANGED
|
@@ -58,6 +58,16 @@ export interface PersonMatchOptions {
|
|
|
58
58
|
* - 'aggressive': Return best match with lower threshold
|
|
59
59
|
*/
|
|
60
60
|
matchStrategy?: 'exact' | 'fuzzy' | 'aggressive';
|
|
61
|
+
/**
|
|
62
|
+
* Search strategy for finding matches:
|
|
63
|
+
* - 'single': Use only the specified matchStrategy (default)
|
|
64
|
+
* - 'multi-step': Try multiple strategies in order until a match is found:
|
|
65
|
+
* 1. Fuzzy with age preference
|
|
66
|
+
* 2. Fuzzy without age preference
|
|
67
|
+
* 3. Exact with age preference
|
|
68
|
+
* 4. Exact without age preference
|
|
69
|
+
*/
|
|
70
|
+
searchStrategy?: 'single' | 'multi-step';
|
|
61
71
|
/** Campus ID to associate with the person */
|
|
62
72
|
campusId?: string;
|
|
63
73
|
/** If true, create a new person if no match is found (default: true) */
|
|
@@ -87,19 +97,54 @@ export interface PersonMatchOptions {
|
|
|
87
97
|
* When a person is created, PCO needs time (30-90+ seconds) to verify/index contacts
|
|
88
98
|
* before they become searchable. This retry logic helps prevent duplicate person creation.
|
|
89
99
|
*/
|
|
90
|
-
retryConfig?:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
enabled?: boolean;
|
|
100
|
+
retryConfig?: RetryConfig;
|
|
101
|
+
/**
|
|
102
|
+
* Phase-specific retry configurations for advanced control.
|
|
103
|
+
* When both retryConfig and retryConfigs are provided, retryConfigs takes precedence.
|
|
104
|
+
*/
|
|
105
|
+
retryConfigs?: {
|
|
106
|
+
/** Configuration for initial/quick search phase (default: 30s max wait) */
|
|
107
|
+
initial?: RetryConfig;
|
|
108
|
+
/** Configuration for aggressive search before creating (default: 60s max wait) */
|
|
109
|
+
aggressive?: RetryConfig;
|
|
101
110
|
};
|
|
111
|
+
/**
|
|
112
|
+
* If true, fall back to name-based search when email/phone search fails.
|
|
113
|
+
* Requires both firstName and lastName to be provided.
|
|
114
|
+
* Uses contact validation to avoid wrong-person matches. (default: false)
|
|
115
|
+
*/
|
|
116
|
+
fallbackToNameSearch?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Contact validation strategy for name-based search fallback:
|
|
119
|
+
* - 'strict': Requires exact email or phone match
|
|
120
|
+
* - 'domain': Requires matching email domain or similar phone
|
|
121
|
+
* - 'similarity': Uses domain matching for email, similarity for phone (default)
|
|
122
|
+
*/
|
|
123
|
+
contactValidation?: 'strict' | 'domain' | 'similarity';
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Retry configuration for handling PCO contact verification delays
|
|
127
|
+
*/
|
|
128
|
+
export interface RetryConfig {
|
|
129
|
+
/** Maximum number of retry attempts (default: 5) */
|
|
130
|
+
maxRetries?: number;
|
|
131
|
+
/** Maximum total wait time in milliseconds (default: 120000 = 120 seconds) */
|
|
132
|
+
maxWaitTime?: number;
|
|
133
|
+
/** Initial delay in milliseconds before first retry (default: 10000 = 10 seconds) */
|
|
134
|
+
initialDelay?: number;
|
|
135
|
+
/** Multiplier for exponential backoff (default: 1.5) */
|
|
136
|
+
backoffMultiplier?: number;
|
|
137
|
+
/** Whether to enable retry logic (default: true when email/phone provided and createIfNotFound: false) */
|
|
138
|
+
enabled?: boolean;
|
|
102
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Default retry configuration for initial search phase
|
|
142
|
+
*/
|
|
143
|
+
export declare const DEFAULT_INITIAL_RETRY_CONFIG: Required<Omit<RetryConfig, 'enabled'>>;
|
|
144
|
+
/**
|
|
145
|
+
* Default retry configuration for aggressive search phase (before creating)
|
|
146
|
+
*/
|
|
147
|
+
export declare const DEFAULT_AGGRESSIVE_RETRY_CONFIG: Required<Omit<RetryConfig, 'enabled'>>;
|
|
103
148
|
export declare class PeopleModule extends BaseModule {
|
|
104
149
|
private personMatcher;
|
|
105
150
|
constructor(httpClient: PcoHttpClient, paginationHelper: PaginationHelper, eventEmitter: PcoEventEmitter);
|
|
@@ -119,6 +164,30 @@ export declare class PeopleModule extends BaseModule {
|
|
|
119
164
|
* Get a single person by ID
|
|
120
165
|
*/
|
|
121
166
|
getById(id: string, include?: string[]): Promise<PersonResource>;
|
|
167
|
+
/**
|
|
168
|
+
* Verify that a person exists in PCO
|
|
169
|
+
*
|
|
170
|
+
* This is useful for validating cached person IDs before use,
|
|
171
|
+
* especially when person records may have been merged or deleted.
|
|
172
|
+
*
|
|
173
|
+
* @param personId - The person ID to verify
|
|
174
|
+
* @param options - Optional configuration
|
|
175
|
+
* @param options.timeout - Timeout in milliseconds (default: 30000)
|
|
176
|
+
* @returns True if person exists, false if not found
|
|
177
|
+
* @throws Error if request times out or other error occurs (except 404)
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* const exists = await client.people.verifyPersonExists(cachedPersonId);
|
|
182
|
+
* if (!exists) {
|
|
183
|
+
* // Person was merged or deleted, need to search again
|
|
184
|
+
* const person = await client.people.findOrCreate(options);
|
|
185
|
+
* }
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
verifyPersonExists(personId: string, options?: {
|
|
189
|
+
timeout?: number;
|
|
190
|
+
}): Promise<boolean>;
|
|
122
191
|
/**
|
|
123
192
|
* Create a new person
|
|
124
193
|
*/
|
package/dist/modules/people.js
CHANGED
|
@@ -3,9 +3,27 @@
|
|
|
3
3
|
* v2.0.0 People Module
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PeopleModule = void 0;
|
|
6
|
+
exports.PeopleModule = exports.DEFAULT_AGGRESSIVE_RETRY_CONFIG = exports.DEFAULT_INITIAL_RETRY_CONFIG = void 0;
|
|
7
7
|
const planning_center_base_ts_1 = require("@rachelallyson/planning-center-base-ts");
|
|
8
8
|
const matcher_1 = require("../matching/matcher");
|
|
9
|
+
/**
|
|
10
|
+
* Default retry configuration for initial search phase
|
|
11
|
+
*/
|
|
12
|
+
exports.DEFAULT_INITIAL_RETRY_CONFIG = {
|
|
13
|
+
maxRetries: 3,
|
|
14
|
+
maxWaitTime: 30000, // 30 seconds
|
|
15
|
+
initialDelay: 3000,
|
|
16
|
+
backoffMultiplier: 2,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Default retry configuration for aggressive search phase (before creating)
|
|
20
|
+
*/
|
|
21
|
+
exports.DEFAULT_AGGRESSIVE_RETRY_CONFIG = {
|
|
22
|
+
maxRetries: 6,
|
|
23
|
+
maxWaitTime: 60000, // 60 seconds
|
|
24
|
+
initialDelay: 5000,
|
|
25
|
+
backoffMultiplier: 2,
|
|
26
|
+
};
|
|
9
27
|
class PeopleModule extends planning_center_base_ts_1.BaseModule {
|
|
10
28
|
constructor(httpClient, paginationHelper, eventEmitter) {
|
|
11
29
|
super(httpClient, paginationHelper, eventEmitter);
|
|
@@ -57,6 +75,46 @@ class PeopleModule extends planning_center_base_ts_1.BaseModule {
|
|
|
57
75
|
}
|
|
58
76
|
return this.getSingle(`/people/${id}`, params);
|
|
59
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Verify that a person exists in PCO
|
|
80
|
+
*
|
|
81
|
+
* This is useful for validating cached person IDs before use,
|
|
82
|
+
* especially when person records may have been merged or deleted.
|
|
83
|
+
*
|
|
84
|
+
* @param personId - The person ID to verify
|
|
85
|
+
* @param options - Optional configuration
|
|
86
|
+
* @param options.timeout - Timeout in milliseconds (default: 30000)
|
|
87
|
+
* @returns True if person exists, false if not found
|
|
88
|
+
* @throws Error if request times out or other error occurs (except 404)
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const exists = await client.people.verifyPersonExists(cachedPersonId);
|
|
93
|
+
* if (!exists) {
|
|
94
|
+
* // Person was merged or deleted, need to search again
|
|
95
|
+
* const person = await client.people.findOrCreate(options);
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
async verifyPersonExists(personId, options) {
|
|
100
|
+
const timeout = options?.timeout ?? 30000;
|
|
101
|
+
const verificationPromise = this.getById(personId)
|
|
102
|
+
.then(() => true)
|
|
103
|
+
.catch((error) => {
|
|
104
|
+
// 404 means person doesn't exist (merged or deleted)
|
|
105
|
+
if (error?.status === 404 || error?.response?.status === 404) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
// Re-throw other errors
|
|
109
|
+
throw error;
|
|
110
|
+
});
|
|
111
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
reject(new Error(`Person verification timed out after ${timeout}ms`));
|
|
114
|
+
}, timeout);
|
|
115
|
+
});
|
|
116
|
+
return Promise.race([verificationPromise, timeoutPromise]);
|
|
117
|
+
}
|
|
60
118
|
/**
|
|
61
119
|
* Create a new person
|
|
62
120
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rachelallyson/planning-center-people-ts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"description": "A strictly typed TypeScript client for Planning Center Online People API with comprehensive functionality and enhanced developer experience",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|