@rachelallyson/planning-center-people-ts 2.9.1 โ 2.11.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 +60 -0
- package/dist/helpers.d.ts +11 -0
- package/dist/helpers.js +24 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/matching/matcher.d.ts +9 -0
- package/dist/matching/matcher.js +114 -19
- package/dist/matching/scoring.js +4 -11
- package/dist/modules/people.d.ts +17 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,66 @@ 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.11.0] - 2025-01-15
|
|
9
|
+
|
|
10
|
+
### โจ **New Features**
|
|
11
|
+
|
|
12
|
+
- **๐ Automatic Retry Logic for Contact Verification**: Added built-in retry logic to prevent duplicate person creation
|
|
13
|
+
- Automatically retries when searching for existing persons with email/phone
|
|
14
|
+
- Handles PCO contact verification delays (30-90+ seconds)
|
|
15
|
+
- Uses exponential backoff (10s โ 15s โ 22.5s โ 33.75s โ 50.6s)
|
|
16
|
+
- Configurable retry behavior via `retryConfig` option
|
|
17
|
+
- Default: 5 retries, 120 seconds max wait time
|
|
18
|
+
- Logs retry attempts with `[PERSON_MATCH]` prefix for debugging
|
|
19
|
+
- Prevents duplicate person creation when PCO takes time to verify/index contacts
|
|
20
|
+
|
|
21
|
+
### ๐ง **Improvements**
|
|
22
|
+
|
|
23
|
+
- **Duplicate Prevention**: `findOrCreate` now automatically handles PCO contact verification delays
|
|
24
|
+
- Retry logic activates automatically when `createIfNotFound: false` and email/phone are provided
|
|
25
|
+
- No manual retry code needed - library handles it automatically
|
|
26
|
+
- Prevents race conditions where duplicate persons are created
|
|
27
|
+
|
|
28
|
+
### ๐ **Documentation**
|
|
29
|
+
|
|
30
|
+
- Added `RETRY_LOGIC_FIX.md` - Detailed explanation of the retry logic fix
|
|
31
|
+
- Added `USING_RETRY_LOGIC.md` - Guide for using retry logic in your app
|
|
32
|
+
- Added `MIGRATION_GUIDE_FOR_YOUR_APP.md` - Migration guide to simplify existing code
|
|
33
|
+
- Added `TEST_FAILURE_ANALYSIS.md` - Troubleshooting guide for test failures
|
|
34
|
+
|
|
35
|
+
### ๐งช **Testing**
|
|
36
|
+
|
|
37
|
+
- Added comprehensive integration tests for retry logic
|
|
38
|
+
- Tests verify retry logic prevents duplicate person creation
|
|
39
|
+
- Tests demonstrate bug scenario and fix
|
|
40
|
+
|
|
41
|
+
## [2.10.0] - 2025-01-15
|
|
42
|
+
|
|
43
|
+
### โจ **New Features**
|
|
44
|
+
|
|
45
|
+
- **๐ง Email Normalization & Validation**: Added email normalization and format validation to improve search accuracy
|
|
46
|
+
- New `normalizeEmail()` helper function (lowercase and trim)
|
|
47
|
+
- Email is now normalized before search to improve PCO API search results
|
|
48
|
+
- Email format validation prevents wasted API calls on invalid emails
|
|
49
|
+
- **๐ฑ Phone Normalization**: Added phone normalization to improve search accuracy
|
|
50
|
+
- New `normalizePhone()` helper function (normalizes to `+1XXXXXXXXXX` format)
|
|
51
|
+
- Phone numbers are now normalized before search to improve PCO API search results
|
|
52
|
+
- **โ
First Name Validation**: Added firstName validation in person creation
|
|
53
|
+
- Validates firstName is required before attempting person creation
|
|
54
|
+
- Provides clearer error messages: "First name is required to create a person"
|
|
55
|
+
- Fails fast instead of waiting for API error response
|
|
56
|
+
|
|
57
|
+
### ๐ง **Improvements**
|
|
58
|
+
|
|
59
|
+
- **Normalization Consistency**: Refactored normalization logic into reusable helper functions
|
|
60
|
+
- All email/phone normalization now uses consistent helper functions
|
|
61
|
+
- Updated both `matcher.ts` and `scoring.ts` to use shared normalization functions
|
|
62
|
+
- Removed duplicate inline normalization code
|
|
63
|
+
|
|
64
|
+
### ๐ฆ **Exports**
|
|
65
|
+
|
|
66
|
+
- Exported `normalizeEmail` and `normalizePhone` helper functions from main package index for library users
|
|
67
|
+
|
|
8
68
|
## [2.9.1] - 2025-01-14
|
|
9
69
|
|
|
10
70
|
### ๐ **Bug Fixes**
|
package/dist/helpers.d.ts
CHANGED
|
@@ -44,10 +44,21 @@ export declare function calculateBirthYearFromAge(age: number): number;
|
|
|
44
44
|
* Validate email format
|
|
45
45
|
*/
|
|
46
46
|
export declare function isValidEmail(email: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Normalize email address (lowercase and trim)
|
|
49
|
+
*/
|
|
50
|
+
export declare function normalizeEmail(email: string): string;
|
|
47
51
|
/**
|
|
48
52
|
* Validate phone number format (basic validation)
|
|
49
53
|
*/
|
|
50
54
|
export declare function isValidPhone(phone: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Normalize phone number to +1XXXXXXXXXX format
|
|
57
|
+
* - 10 digits: adds +1 prefix
|
|
58
|
+
* - 11 digits starting with 1: adds + prefix
|
|
59
|
+
* - Other lengths: adds + prefix to all digits
|
|
60
|
+
*/
|
|
61
|
+
export declare function normalizePhone(phone: string): string;
|
|
51
62
|
/**
|
|
52
63
|
* Format person name from attributes
|
|
53
64
|
*/
|
package/dist/helpers.js
CHANGED
|
@@ -8,7 +8,9 @@ exports.isChild = isChild;
|
|
|
8
8
|
exports.matchesAgeCriteria = matchesAgeCriteria;
|
|
9
9
|
exports.calculateBirthYearFromAge = calculateBirthYearFromAge;
|
|
10
10
|
exports.isValidEmail = isValidEmail;
|
|
11
|
+
exports.normalizeEmail = normalizeEmail;
|
|
11
12
|
exports.isValidPhone = isValidPhone;
|
|
13
|
+
exports.normalizePhone = normalizePhone;
|
|
12
14
|
exports.formatPersonName = formatPersonName;
|
|
13
15
|
exports.formatDate = formatDate;
|
|
14
16
|
exports.validatePersonData = validatePersonData;
|
|
@@ -137,6 +139,12 @@ function isValidEmail(email) {
|
|
|
137
139
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
138
140
|
return emailRegex.test(email);
|
|
139
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Normalize email address (lowercase and trim)
|
|
144
|
+
*/
|
|
145
|
+
function normalizeEmail(email) {
|
|
146
|
+
return email.toLowerCase().trim();
|
|
147
|
+
}
|
|
140
148
|
/**
|
|
141
149
|
* Validate phone number format (basic validation)
|
|
142
150
|
*/
|
|
@@ -144,6 +152,22 @@ function isValidPhone(phone) {
|
|
|
144
152
|
const phoneRegex = /^[\+]?[1-9][\d]{6,14}$/;
|
|
145
153
|
return phoneRegex.test(phone.replace(/[\s\-\(\)]/g, ''));
|
|
146
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Normalize phone number to +1XXXXXXXXXX format
|
|
157
|
+
* - 10 digits: adds +1 prefix
|
|
158
|
+
* - 11 digits starting with 1: adds + prefix
|
|
159
|
+
* - Other lengths: adds + prefix to all digits
|
|
160
|
+
*/
|
|
161
|
+
function normalizePhone(phone) {
|
|
162
|
+
const digits = phone.replace(/\D/g, '');
|
|
163
|
+
if (digits.length === 10) {
|
|
164
|
+
return `+1${digits}`;
|
|
165
|
+
}
|
|
166
|
+
if (digits.length === 11 && digits.startsWith('1')) {
|
|
167
|
+
return `+${digits}`;
|
|
168
|
+
}
|
|
169
|
+
return `+${digits}`;
|
|
170
|
+
}
|
|
147
171
|
/**
|
|
148
172
|
* Format person name from attributes
|
|
149
173
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ 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, 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, processFileValue, searchPeople, validatePersonData, } from './helpers';
|
|
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';
|
|
24
24
|
export { AdaptiveRateLimiter, ApiCache, batchFetchPersonDetails, fetchAllPages, getCachedPeople, monitorPerformance, PerformanceMonitor, processInBatches, processLargeDataset, streamPeopleData, } from './performance';
|
|
25
25
|
export { MockPcoClient, MockResponseBuilder, RequestRecorder, createMockClient, createRecordingClient, createTestClient, createErrorMockClient, createSlowMockClient, } from './testing';
|
|
26
26
|
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.getNotes = exports.getNoteCategories = exports.getNote = 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 = void 0;
|
|
19
|
+
exports.ApiCache = exports.AdaptiveRateLimiter = exports.validatePersonData = exports.searchPeople = exports.processFileValue = 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.exportAllPeopleData = exports.createWorkflowCardWithNote = exports.createPersonWithContact = 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 = 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 = 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; } });
|
|
@@ -130,6 +130,8 @@ Object.defineProperty(exports, "isFileUpload", { enumerable: true, get: function
|
|
|
130
130
|
Object.defineProperty(exports, "isFileUrl", { enumerable: true, get: function () { return helpers_1.isFileUrl; } });
|
|
131
131
|
Object.defineProperty(exports, "isValidEmail", { enumerable: true, get: function () { return helpers_1.isValidEmail; } });
|
|
132
132
|
Object.defineProperty(exports, "isValidPhone", { enumerable: true, get: function () { return helpers_1.isValidPhone; } });
|
|
133
|
+
Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: function () { return helpers_1.normalizeEmail; } });
|
|
134
|
+
Object.defineProperty(exports, "normalizePhone", { enumerable: true, get: function () { return helpers_1.normalizePhone; } });
|
|
133
135
|
Object.defineProperty(exports, "processFileValue", { enumerable: true, get: function () { return helpers_1.processFileValue; } });
|
|
134
136
|
Object.defineProperty(exports, "searchPeople", { enumerable: true, get: function () { return helpers_1.searchPeople; } });
|
|
135
137
|
Object.defineProperty(exports, "validatePersonData", { enumerable: true, get: function () { return helpers_1.validatePersonData; } });
|
|
@@ -22,11 +22,20 @@ export declare class PersonMatcher {
|
|
|
22
22
|
* - Verifies email/phone matches by checking actual contact information
|
|
23
23
|
* - Only uses name matching when appropriate (multiple people share contact info, or no contact info provided)
|
|
24
24
|
* - Can automatically add missing contact information when a match is found (if addMissingContactInfo is true)
|
|
25
|
+
* - Retries with exponential backoff when contacts may not be verified yet (PCO takes 30-90+ seconds)
|
|
25
26
|
*
|
|
26
27
|
* @param options - Matching options
|
|
27
28
|
* @param options.addMissingContactInfo - If true, automatically adds missing email/phone to matched person's profile
|
|
29
|
+
* @param options.retryConfig - Configuration for retry logic to handle PCO contact verification delays
|
|
28
30
|
*/
|
|
29
31
|
findOrCreate(options: PersonMatchOptions): Promise<PersonResource>;
|
|
32
|
+
/**
|
|
33
|
+
* Find or create with retry logic to handle PCO contact verification delays
|
|
34
|
+
*
|
|
35
|
+
* PCO takes 30-90+ seconds to verify/index contacts after a person is created.
|
|
36
|
+
* This method retries with exponential backoff to give PCO time to process contacts.
|
|
37
|
+
*/
|
|
38
|
+
private findOrCreateWithRetry;
|
|
30
39
|
/**
|
|
31
40
|
* Find the best match for a person
|
|
32
41
|
*/
|
package/dist/matching/matcher.js
CHANGED
|
@@ -20,12 +20,26 @@ class PersonMatcher {
|
|
|
20
20
|
* - Verifies email/phone matches by checking actual contact information
|
|
21
21
|
* - Only uses name matching when appropriate (multiple people share contact info, or no contact info provided)
|
|
22
22
|
* - Can automatically add missing contact information when a match is found (if addMissingContactInfo is true)
|
|
23
|
+
* - Retries with exponential backoff when contacts may not be verified yet (PCO takes 30-90+ seconds)
|
|
23
24
|
*
|
|
24
25
|
* @param options - Matching options
|
|
25
26
|
* @param options.addMissingContactInfo - If true, automatically adds missing email/phone to matched person's profile
|
|
27
|
+
* @param options.retryConfig - Configuration for retry logic to handle PCO contact verification delays
|
|
26
28
|
*/
|
|
27
29
|
async findOrCreate(options) {
|
|
28
|
-
const { createIfNotFound = true, matchStrategy = 'fuzzy', addMissingContactInfo = false, ...searchOptions } = options;
|
|
30
|
+
const { createIfNotFound = true, matchStrategy = 'fuzzy', addMissingContactInfo = false, retryConfig, ...searchOptions } = options;
|
|
31
|
+
// Determine if retry logic should be enabled
|
|
32
|
+
// Retry is useful when:
|
|
33
|
+
// 1. We have email/phone (these need verification)
|
|
34
|
+
// 2. createIfNotFound is false (we're trying to find existing, not create new)
|
|
35
|
+
// 3. retryConfig.enabled is not explicitly false
|
|
36
|
+
const hasContactInfo = !!(options.email || options.phone);
|
|
37
|
+
const shouldRetry = hasContactInfo &&
|
|
38
|
+
!createIfNotFound &&
|
|
39
|
+
(retryConfig?.enabled !== false);
|
|
40
|
+
if (shouldRetry) {
|
|
41
|
+
return this.findOrCreateWithRetry(options);
|
|
42
|
+
}
|
|
29
43
|
// Try to find existing person
|
|
30
44
|
const match = await this.findMatch({ ...searchOptions, matchStrategy });
|
|
31
45
|
if (match) {
|
|
@@ -42,6 +56,79 @@ class PersonMatcher {
|
|
|
42
56
|
}
|
|
43
57
|
throw new Error(`No matching person found and creation is disabled`);
|
|
44
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Find or create with retry logic to handle PCO contact verification delays
|
|
61
|
+
*
|
|
62
|
+
* PCO takes 30-90+ seconds to verify/index contacts after a person is created.
|
|
63
|
+
* This method retries with exponential backoff to give PCO time to process contacts.
|
|
64
|
+
*/
|
|
65
|
+
async findOrCreateWithRetry(options) {
|
|
66
|
+
const { createIfNotFound = false, matchStrategy = 'fuzzy', addMissingContactInfo = false, retryConfig, ...searchOptions } = options;
|
|
67
|
+
// Default retry configuration
|
|
68
|
+
const maxRetries = retryConfig?.maxRetries ?? 5;
|
|
69
|
+
const maxWaitTime = retryConfig?.maxWaitTime ?? 120000; // 120 seconds
|
|
70
|
+
const initialDelay = retryConfig?.initialDelay ?? 10000; // 10 seconds
|
|
71
|
+
const backoffMultiplier = retryConfig?.backoffMultiplier ?? 1.5;
|
|
72
|
+
let totalWaitTime = 0;
|
|
73
|
+
let lastError = null;
|
|
74
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
75
|
+
try {
|
|
76
|
+
// Try to find existing person
|
|
77
|
+
const match = await this.findMatch({ ...searchOptions, matchStrategy });
|
|
78
|
+
if (match) {
|
|
79
|
+
const person = match.person;
|
|
80
|
+
// Add missing contact information if requested
|
|
81
|
+
if (addMissingContactInfo) {
|
|
82
|
+
await this.addMissingContactInfo(person, options);
|
|
83
|
+
}
|
|
84
|
+
// Log success if we had to retry
|
|
85
|
+
if (attempt > 1) {
|
|
86
|
+
console.log(`[PERSON_MATCH] Found person after ${attempt} attempts (waited ${totalWaitTime}ms)`, {
|
|
87
|
+
personId: person.id,
|
|
88
|
+
attempt,
|
|
89
|
+
totalWaitTime
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return person;
|
|
93
|
+
}
|
|
94
|
+
// No match found - this might be because contacts aren't verified yet
|
|
95
|
+
lastError = new Error(`No matching person found and creation is disabled`);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
99
|
+
}
|
|
100
|
+
// Don't retry on the last attempt
|
|
101
|
+
if (attempt === maxRetries) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
// Calculate delay with exponential backoff
|
|
105
|
+
const delay = Math.min(initialDelay * Math.pow(backoffMultiplier, attempt - 1), maxWaitTime - totalWaitTime // Don't exceed maxWaitTime
|
|
106
|
+
);
|
|
107
|
+
// Check if we've exceeded max wait time
|
|
108
|
+
if (totalWaitTime + delay > maxWaitTime) {
|
|
109
|
+
console.warn(`[PERSON_MATCH] Max wait time (${maxWaitTime}ms) exceeded, stopping retries`, {
|
|
110
|
+
attempt,
|
|
111
|
+
totalWaitTime,
|
|
112
|
+
remainingDelay: maxWaitTime - totalWaitTime
|
|
113
|
+
});
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
totalWaitTime += delay;
|
|
117
|
+
// Log retry attempt
|
|
118
|
+
console.log(`[PERSON_MATCH] Attempt ${attempt} failed, retrying in ${delay}ms`, {
|
|
119
|
+
attempt,
|
|
120
|
+
delay,
|
|
121
|
+
totalWaitTime,
|
|
122
|
+
errorMessage: lastError?.message,
|
|
123
|
+
maxRetries,
|
|
124
|
+
maxWaitTime
|
|
125
|
+
});
|
|
126
|
+
// Wait before retrying
|
|
127
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
128
|
+
}
|
|
129
|
+
// All retries exhausted - throw error
|
|
130
|
+
throw lastError || new Error(`No matching person found after ${maxRetries} attempts (waited ${totalWaitTime}ms) and creation is disabled`);
|
|
131
|
+
}
|
|
45
132
|
/**
|
|
46
133
|
* Find the best match for a person
|
|
47
134
|
*/
|
|
@@ -50,20 +137,30 @@ class PersonMatcher {
|
|
|
50
137
|
// Step 1: Try email/phone search first
|
|
51
138
|
const emailPhoneMatches = [];
|
|
52
139
|
const nameOnlyMatches = [];
|
|
53
|
-
// Search by email
|
|
140
|
+
// Search by email (with normalization and validation)
|
|
54
141
|
if (email) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
142
|
+
// Validate email format to avoid wasted API calls
|
|
143
|
+
if (!(0, helpers_1.isValidEmail)(email)) {
|
|
144
|
+
console.warn('Invalid email format, skipping email search:', email);
|
|
58
145
|
}
|
|
59
|
-
|
|
60
|
-
|
|
146
|
+
else {
|
|
147
|
+
try {
|
|
148
|
+
// Normalize email before search to improve PCO search results
|
|
149
|
+
const normalizedEmail = (0, helpers_1.normalizeEmail)(email);
|
|
150
|
+
const emailResults = await this.peopleModule.search({ email: normalizedEmail });
|
|
151
|
+
emailPhoneMatches.push(...emailResults.data);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.warn('Email search failed:', error);
|
|
155
|
+
}
|
|
61
156
|
}
|
|
62
157
|
}
|
|
63
|
-
// Search by phone
|
|
158
|
+
// Search by phone (with normalization)
|
|
64
159
|
if (phone) {
|
|
65
160
|
try {
|
|
66
|
-
|
|
161
|
+
// Normalize phone before search to improve PCO search results
|
|
162
|
+
const normalizedPhone = (0, helpers_1.normalizePhone)(phone);
|
|
163
|
+
const phoneResults = await this.peopleModule.search({ phone: normalizedPhone });
|
|
67
164
|
emailPhoneMatches.push(...phoneResults.data);
|
|
68
165
|
}
|
|
69
166
|
catch (error) {
|
|
@@ -203,8 +300,8 @@ class PersonMatcher {
|
|
|
203
300
|
async verifyEmailMatch(person, email) {
|
|
204
301
|
try {
|
|
205
302
|
const personEmails = await this.peopleModule.getEmails(person.id);
|
|
206
|
-
const normalizedSearchEmail =
|
|
207
|
-
const emails = personEmails.data?.map(e => e.attributes?.address
|
|
303
|
+
const normalizedSearchEmail = (0, helpers_1.normalizeEmail)(email);
|
|
304
|
+
const emails = personEmails.data?.map(e => (0, helpers_1.normalizeEmail)(e.attributes?.address || '')).filter(Boolean) || [];
|
|
208
305
|
return emails.includes(normalizedSearchEmail);
|
|
209
306
|
}
|
|
210
307
|
catch {
|
|
@@ -217,14 +314,8 @@ class PersonMatcher {
|
|
|
217
314
|
async verifyPhoneMatch(person, phone) {
|
|
218
315
|
try {
|
|
219
316
|
const personPhones = await this.peopleModule.getPhoneNumbers(person.id);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
return digits.length === 10 ? `+1${digits}` :
|
|
223
|
-
digits.length === 11 && digits.startsWith('1') ? `+${digits}` :
|
|
224
|
-
`+${digits}`;
|
|
225
|
-
};
|
|
226
|
-
const normalizedSearchPhone = normalizePhone(phone);
|
|
227
|
-
const phones = personPhones.data?.map(p => normalizePhone(p.attributes?.number || '')).filter(Boolean) || [];
|
|
317
|
+
const normalizedSearchPhone = (0, helpers_1.normalizePhone)(phone);
|
|
318
|
+
const phones = personPhones.data?.map(p => (0, helpers_1.normalizePhone)(p.attributes?.number || '')).filter(Boolean) || [];
|
|
228
319
|
return phones.includes(normalizedSearchPhone);
|
|
229
320
|
}
|
|
230
321
|
catch {
|
|
@@ -294,6 +385,10 @@ class PersonMatcher {
|
|
|
294
385
|
* Create a new person
|
|
295
386
|
*/
|
|
296
387
|
async createPerson(options) {
|
|
388
|
+
// Validate firstName is required for person creation
|
|
389
|
+
if (!options.firstName?.trim()) {
|
|
390
|
+
throw new Error('First name is required to create a person');
|
|
391
|
+
}
|
|
297
392
|
// Create basic person data (only name fields)
|
|
298
393
|
const personData = {};
|
|
299
394
|
if (options.firstName)
|
package/dist/matching/scoring.js
CHANGED
|
@@ -98,9 +98,9 @@ class MatchScorer {
|
|
|
98
98
|
async scoreEmailMatch(person, email) {
|
|
99
99
|
try {
|
|
100
100
|
const personEmails = await this.peopleModule.getEmails(person.id);
|
|
101
|
-
const normalizedSearchEmail =
|
|
101
|
+
const normalizedSearchEmail = (0, helpers_1.normalizeEmail)(email);
|
|
102
102
|
// Check if any of the person's emails match
|
|
103
|
-
const emails = personEmails.data?.map(e => e.attributes?.address
|
|
103
|
+
const emails = personEmails.data?.map(e => (0, helpers_1.normalizeEmail)(e.attributes?.address || '')).filter(Boolean) || [];
|
|
104
104
|
return emails.includes(normalizedSearchEmail) ? 1.0 : 0.0;
|
|
105
105
|
}
|
|
106
106
|
catch (error) {
|
|
@@ -114,15 +114,8 @@ class MatchScorer {
|
|
|
114
114
|
async scorePhoneMatch(person, phone) {
|
|
115
115
|
try {
|
|
116
116
|
const personPhones = await this.peopleModule.getPhoneNumbers(person.id);
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
const digits = num.replace(/\D/g, '');
|
|
120
|
-
return digits.length === 10 ? `+1${digits}` :
|
|
121
|
-
digits.length === 11 && digits.startsWith('1') ? `+${digits}` :
|
|
122
|
-
`+${digits}`;
|
|
123
|
-
};
|
|
124
|
-
const normalizedSearchPhone = normalizePhone(phone);
|
|
125
|
-
const phones = personPhones.data?.map(p => normalizePhone(p.attributes?.number || '')).filter(Boolean) || [];
|
|
117
|
+
const normalizedSearchPhone = (0, helpers_1.normalizePhone)(phone);
|
|
118
|
+
const phones = personPhones.data?.map(p => (0, helpers_1.normalizePhone)(p.attributes?.number || '')).filter(Boolean) || [];
|
|
126
119
|
return phones.includes(normalizedSearchPhone) ? 1.0 : 0.0;
|
|
127
120
|
}
|
|
128
121
|
catch (error) {
|
package/dist/modules/people.d.ts
CHANGED
|
@@ -76,6 +76,23 @@ export interface PersonMatchOptions {
|
|
|
76
76
|
maxAge?: number;
|
|
77
77
|
/** Birth year filter (exact match) */
|
|
78
78
|
birthYear?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Retry configuration for handling PCO contact verification delays.
|
|
81
|
+
* When a person is created, PCO needs time (30-90+ seconds) to verify/index contacts
|
|
82
|
+
* before they become searchable. This retry logic helps prevent duplicate person creation.
|
|
83
|
+
*/
|
|
84
|
+
retryConfig?: {
|
|
85
|
+
/** Maximum number of retry attempts (default: 5) */
|
|
86
|
+
maxRetries?: number;
|
|
87
|
+
/** Maximum total wait time in milliseconds (default: 120000 = 120 seconds) */
|
|
88
|
+
maxWaitTime?: number;
|
|
89
|
+
/** Initial delay in milliseconds before first retry (default: 10000 = 10 seconds) */
|
|
90
|
+
initialDelay?: number;
|
|
91
|
+
/** Multiplier for exponential backoff (default: 1.5) */
|
|
92
|
+
backoffMultiplier?: number;
|
|
93
|
+
/** Whether to enable retry logic (default: true when email/phone provided and createIfNotFound: false) */
|
|
94
|
+
enabled?: boolean;
|
|
95
|
+
};
|
|
79
96
|
}
|
|
80
97
|
export declare class PeopleModule extends BaseModule {
|
|
81
98
|
private personMatcher;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rachelallyson/planning-center-people-ts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.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",
|