@rachelallyson/planning-center-people-ts 2.8.0 → 2.9.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 +52 -0
- package/LICENSE +1 -1
- package/README.md +12 -14
- package/dist/client.d.ts +8 -8
- package/dist/client.js +8 -11
- package/dist/core/http.d.ts +1 -1
- package/dist/core/http.js +8 -8
- package/dist/core.d.ts +3 -3
- package/dist/core.js +4 -4
- package/dist/error-handling.d.ts +1 -1
- package/dist/error-handling.js +3 -3
- package/dist/error-scenarios.js +4 -4
- package/dist/index.d.ts +8 -8
- package/dist/index.js +15 -15
- package/dist/matching/matcher.d.ts +22 -0
- package/dist/matching/matcher.js +251 -20
- package/dist/matching/scoring.d.ts +9 -7
- package/dist/matching/scoring.js +49 -20
- package/dist/matching/strategies.d.ts +1 -0
- package/dist/matching/strategies.js +15 -2
- package/dist/modules/campus.d.ts +5 -5
- package/dist/modules/campus.js +2 -2
- package/dist/modules/contacts.d.ts +1 -1
- package/dist/modules/contacts.js +2 -2
- package/dist/modules/fields.d.ts +4 -4
- package/dist/modules/fields.js +2 -2
- package/dist/modules/forms.d.ts +7 -4
- package/dist/modules/forms.js +5 -2
- package/dist/modules/households.d.ts +2 -2
- package/dist/modules/households.js +2 -2
- package/dist/modules/lists.d.ts +2 -2
- package/dist/modules/lists.js +2 -2
- package/dist/modules/notes.d.ts +2 -2
- package/dist/modules/notes.js +2 -2
- package/dist/modules/people.d.ts +57 -5
- package/dist/modules/people.js +32 -2
- package/dist/modules/reports.d.ts +5 -5
- package/dist/modules/reports.js +2 -2
- package/dist/modules/service-time.d.ts +5 -5
- package/dist/modules/service-time.js +2 -2
- package/dist/modules/workflows.d.ts +2 -2
- package/dist/modules/workflows.js +2 -2
- package/dist/people/contacts.d.ts +1 -1
- package/dist/people/core.d.ts +1 -1
- package/dist/people/fields.d.ts +1 -1
- package/dist/people/fields.js +4 -4
- package/dist/people/households.d.ts +1 -1
- package/dist/people/lists.d.ts +1 -1
- package/dist/people/notes.d.ts +1 -1
- package/dist/people/organization.d.ts +1 -1
- package/dist/people/workflows.d.ts +1 -1
- package/dist/types/client.d.ts +3 -96
- package/dist/types/client.js +2 -0
- package/dist/types/index.d.ts +1 -2
- package/dist/types/index.js +0 -2
- package/package.json +15 -18
- package/dist/api-error.d.ts +0 -10
- package/dist/api-error.js +0 -32
- package/dist/batch.d.ts +0 -47
- package/dist/batch.js +0 -376
- package/dist/modules/base.d.ts +0 -46
- package/dist/modules/base.js +0 -82
- package/dist/monitoring.d.ts +0 -53
- package/dist/monitoring.js +0 -142
- package/dist/rate-limiter.d.ts +0 -79
- package/dist/rate-limiter.js +0 -137
- package/dist/types/batch.d.ts +0 -50
- package/dist/types/batch.js +0 -5
- package/dist/types/events.d.ts +0 -85
- package/dist/types/events.js +0 -5
package/dist/matching/matcher.js
CHANGED
|
@@ -11,17 +11,30 @@ class PersonMatcher {
|
|
|
11
11
|
constructor(peopleModule) {
|
|
12
12
|
this.peopleModule = peopleModule;
|
|
13
13
|
this.strategies = new strategies_1.MatchStrategies();
|
|
14
|
-
this.scorer = new scoring_1.MatchScorer();
|
|
14
|
+
this.scorer = new scoring_1.MatchScorer(peopleModule);
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Find or create a person with smart matching
|
|
18
|
+
*
|
|
19
|
+
* Uses intelligent matching logic that:
|
|
20
|
+
* - Verifies email/phone matches by checking actual contact information
|
|
21
|
+
* - Only uses name matching when appropriate (multiple people share contact info, or no contact info provided)
|
|
22
|
+
* - Can automatically add missing contact information when a match is found (if addMissingContactInfo is true)
|
|
23
|
+
*
|
|
24
|
+
* @param options - Matching options
|
|
25
|
+
* @param options.addMissingContactInfo - If true, automatically adds missing email/phone to matched person's profile
|
|
18
26
|
*/
|
|
19
27
|
async findOrCreate(options) {
|
|
20
|
-
const { createIfNotFound = true, matchStrategy = 'fuzzy', ...searchOptions } = options;
|
|
28
|
+
const { createIfNotFound = true, matchStrategy = 'fuzzy', addMissingContactInfo = false, ...searchOptions } = options;
|
|
21
29
|
// Try to find existing person
|
|
22
30
|
const match = await this.findMatch({ ...searchOptions, matchStrategy });
|
|
23
31
|
if (match) {
|
|
24
|
-
|
|
32
|
+
const person = match.person;
|
|
33
|
+
// Add missing contact information if requested
|
|
34
|
+
if (addMissingContactInfo) {
|
|
35
|
+
await this.addMissingContactInfo(person, options);
|
|
36
|
+
}
|
|
37
|
+
return person;
|
|
25
38
|
}
|
|
26
39
|
// Create new person if not found and creation is enabled
|
|
27
40
|
if (createIfNotFound) {
|
|
@@ -33,26 +46,103 @@ class PersonMatcher {
|
|
|
33
46
|
* Find the best match for a person
|
|
34
47
|
*/
|
|
35
48
|
async findMatch(options) {
|
|
36
|
-
const { matchStrategy = 'fuzzy' } = options;
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
|
|
49
|
+
const { matchStrategy = 'fuzzy', email, phone, firstName, lastName } = options;
|
|
50
|
+
// Step 1: Try email/phone search first
|
|
51
|
+
const emailPhoneMatches = [];
|
|
52
|
+
const nameOnlyMatches = [];
|
|
53
|
+
// Search by email
|
|
54
|
+
if (email) {
|
|
55
|
+
try {
|
|
56
|
+
const emailResults = await this.peopleModule.search({ email });
|
|
57
|
+
emailPhoneMatches.push(...emailResults.data);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.warn('Email search failed:', error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Search by phone
|
|
64
|
+
if (phone) {
|
|
65
|
+
try {
|
|
66
|
+
const phoneResults = await this.peopleModule.search({ phone });
|
|
67
|
+
emailPhoneMatches.push(...phoneResults.data);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn('Phone search failed:', error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Remove duplicates
|
|
74
|
+
const uniqueEmailPhoneMatches = emailPhoneMatches.filter((person, index, self) => index === self.findIndex(p => p.id === person.id));
|
|
75
|
+
// Step 2: Verify email/phone actually match
|
|
76
|
+
const verifiedMatches = [];
|
|
77
|
+
for (const candidate of uniqueEmailPhoneMatches) {
|
|
78
|
+
let emailMatches = false;
|
|
79
|
+
let phoneMatches = false;
|
|
80
|
+
if (email) {
|
|
81
|
+
emailMatches = await this.verifyEmailMatch(candidate, email);
|
|
82
|
+
}
|
|
83
|
+
if (phone) {
|
|
84
|
+
phoneMatches = await this.verifyPhoneMatch(candidate, phone);
|
|
85
|
+
}
|
|
86
|
+
// Only include if email OR phone matches (at least one required)
|
|
87
|
+
if (emailMatches || phoneMatches) {
|
|
88
|
+
verifiedMatches.push(candidate);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Step 3: Only search by name if:
|
|
92
|
+
// - Email/phone search found multiple verified results (need name to distinguish), OR
|
|
93
|
+
// - No email/phone was provided (name-only matching is OK)
|
|
94
|
+
// NOTE: Do NOT search by name if email/phone were provided but don't match
|
|
95
|
+
if (verifiedMatches.length > 1 || (!email && !phone)) {
|
|
96
|
+
if (firstName && lastName) {
|
|
97
|
+
try {
|
|
98
|
+
const nameResults = await this.peopleModule.search({
|
|
99
|
+
name: `${firstName} ${lastName}`
|
|
100
|
+
});
|
|
101
|
+
nameOnlyMatches.push(...nameResults.data);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.warn('Name search failed:', error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Step 4: Combine verified email/phone matches with name matches
|
|
109
|
+
// Prioritize email/phone matches over name-only matches
|
|
110
|
+
const allCandidates = [...verifiedMatches, ...nameOnlyMatches];
|
|
111
|
+
// Remove duplicates
|
|
112
|
+
const uniqueCandidates = allCandidates.filter((person, index, self) => index === self.findIndex(p => p.id === person.id));
|
|
113
|
+
// Filter by age preferences
|
|
114
|
+
const ageFilteredCandidates = this.filterByAgePreferences(uniqueCandidates, options);
|
|
115
|
+
if (ageFilteredCandidates.length === 0) {
|
|
40
116
|
return null;
|
|
41
117
|
}
|
|
42
118
|
// Score and rank candidates
|
|
43
|
-
const scoredCandidates =
|
|
119
|
+
const scoredCandidates = await Promise.all(ageFilteredCandidates.map(async (candidate) => ({
|
|
44
120
|
person: candidate,
|
|
45
|
-
score: this.scorer.scoreMatch(candidate, options),
|
|
46
|
-
reason: this.scorer.getMatchReason(candidate, options),
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
121
|
+
score: await this.scorer.scoreMatch(candidate, options),
|
|
122
|
+
reason: await this.scorer.getMatchReason(candidate, options),
|
|
123
|
+
// Mark if this is a verified email/phone match
|
|
124
|
+
isVerifiedContactMatch: verifiedMatches.some(v => v.id === candidate.id)
|
|
125
|
+
})));
|
|
126
|
+
// Sort by verified match first, then by score
|
|
127
|
+
scoredCandidates.sort((a, b) => {
|
|
128
|
+
if (a.isVerifiedContactMatch && !b.isVerifiedContactMatch)
|
|
129
|
+
return -1;
|
|
130
|
+
if (!a.isVerifiedContactMatch && b.isVerifiedContactMatch)
|
|
131
|
+
return 1;
|
|
132
|
+
return b.score - a.score;
|
|
133
|
+
});
|
|
134
|
+
// For "exact" strategy, only return verified email/phone matches
|
|
135
|
+
if (matchStrategy === 'exact') {
|
|
136
|
+
const exactMatches = scoredCandidates.filter(c => c.isVerifiedContactMatch && c.score >= 0.8);
|
|
137
|
+
return exactMatches.length > 0 ? exactMatches[0] : null;
|
|
138
|
+
}
|
|
50
139
|
// Apply strategy-specific filtering
|
|
51
140
|
const bestMatch = this.strategies.selectBestMatch(scoredCandidates, matchStrategy);
|
|
52
141
|
return bestMatch;
|
|
53
142
|
}
|
|
54
143
|
/**
|
|
55
144
|
* Get potential matching candidates
|
|
145
|
+
* @deprecated Use findMatch which has improved logic for separating verified matches from name-only matches
|
|
56
146
|
*/
|
|
57
147
|
async getCandidates(options) {
|
|
58
148
|
const candidates = [];
|
|
@@ -107,6 +197,78 @@ class PersonMatcher {
|
|
|
107
197
|
const ageFilteredCandidates = this.filterByAgePreferences(uniqueCandidates, options);
|
|
108
198
|
return ageFilteredCandidates;
|
|
109
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Verify if a person's email actually matches the search email
|
|
202
|
+
*/
|
|
203
|
+
async verifyEmailMatch(person, email) {
|
|
204
|
+
try {
|
|
205
|
+
const personEmails = await this.peopleModule.getEmails(person.id);
|
|
206
|
+
const normalizedSearchEmail = email.toLowerCase().trim();
|
|
207
|
+
const emails = personEmails.data?.map(e => e.attributes?.address?.toLowerCase().trim()).filter(Boolean) || [];
|
|
208
|
+
return emails.includes(normalizedSearchEmail);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Verify if a person's phone actually matches the search phone
|
|
216
|
+
*/
|
|
217
|
+
async verifyPhoneMatch(person, phone) {
|
|
218
|
+
try {
|
|
219
|
+
const personPhones = await this.peopleModule.getPhoneNumbers(person.id);
|
|
220
|
+
const normalizePhone = (num) => {
|
|
221
|
+
const digits = num.replace(/\D/g, '');
|
|
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) || [];
|
|
228
|
+
return phones.includes(normalizedSearchPhone);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Add missing contact information to a person's profile
|
|
236
|
+
*/
|
|
237
|
+
async addMissingContactInfo(person, options) {
|
|
238
|
+
const { email, phone } = options;
|
|
239
|
+
// Check and add email if provided and missing
|
|
240
|
+
if (email) {
|
|
241
|
+
try {
|
|
242
|
+
const hasEmail = await this.verifyEmailMatch(person, email);
|
|
243
|
+
if (!hasEmail) {
|
|
244
|
+
await this.peopleModule.addEmail(person.id, {
|
|
245
|
+
address: email,
|
|
246
|
+
location: 'Home',
|
|
247
|
+
primary: false // Don't override existing primary email
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.warn(`Failed to add email contact for person ${person.id}:`, error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Check and add phone if provided and missing
|
|
256
|
+
if (phone) {
|
|
257
|
+
try {
|
|
258
|
+
const hasPhone = await this.verifyPhoneMatch(person, phone);
|
|
259
|
+
if (!hasPhone) {
|
|
260
|
+
await this.peopleModule.addPhoneNumber(person.id, {
|
|
261
|
+
number: phone,
|
|
262
|
+
location: 'Home',
|
|
263
|
+
primary: false // Don't override existing primary phone
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
console.warn(`Failed to add phone contact for person ${person.id}:`, error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
110
272
|
/**
|
|
111
273
|
* Filter candidates by age preferences
|
|
112
274
|
*/
|
|
@@ -181,24 +343,93 @@ class PersonMatcher {
|
|
|
181
343
|
* Get all potential matches with detailed scoring
|
|
182
344
|
*/
|
|
183
345
|
async getAllMatches(options) {
|
|
184
|
-
|
|
185
|
-
|
|
346
|
+
// Use the improved matching logic from findMatch
|
|
347
|
+
const { matchStrategy = 'fuzzy', email, phone, firstName, lastName } = options;
|
|
348
|
+
const emailPhoneMatches = [];
|
|
349
|
+
const nameOnlyMatches = [];
|
|
350
|
+
if (email) {
|
|
351
|
+
try {
|
|
352
|
+
const emailResults = await this.peopleModule.search({ email });
|
|
353
|
+
emailPhoneMatches.push(...emailResults.data);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
console.warn('Email search failed:', error);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (phone) {
|
|
360
|
+
try {
|
|
361
|
+
const phoneResults = await this.peopleModule.search({ phone });
|
|
362
|
+
emailPhoneMatches.push(...phoneResults.data);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
console.warn('Phone search failed:', error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const uniqueEmailPhoneMatches = emailPhoneMatches.filter((person, index, self) => index === self.findIndex(p => p.id === person.id));
|
|
369
|
+
const verifiedMatches = [];
|
|
370
|
+
for (const candidate of uniqueEmailPhoneMatches) {
|
|
371
|
+
let emailMatches = false;
|
|
372
|
+
let phoneMatches = false;
|
|
373
|
+
if (email) {
|
|
374
|
+
emailMatches = await this.verifyEmailMatch(candidate, email);
|
|
375
|
+
}
|
|
376
|
+
if (phone) {
|
|
377
|
+
phoneMatches = await this.verifyPhoneMatch(candidate, phone);
|
|
378
|
+
}
|
|
379
|
+
if (emailMatches || phoneMatches) {
|
|
380
|
+
verifiedMatches.push(candidate);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (verifiedMatches.length > 1 || (!email && !phone)) {
|
|
384
|
+
if (firstName && lastName) {
|
|
385
|
+
try {
|
|
386
|
+
const nameResults = await this.peopleModule.search({
|
|
387
|
+
name: `${firstName} ${lastName}`
|
|
388
|
+
});
|
|
389
|
+
nameOnlyMatches.push(...nameResults.data);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
console.warn('Name search failed:', error);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const allCandidates = [...verifiedMatches, ...nameOnlyMatches];
|
|
397
|
+
const uniqueCandidates = allCandidates.filter((person, index, self) => index === self.findIndex(p => p.id === person.id));
|
|
398
|
+
const ageFilteredCandidates = this.filterByAgePreferences(uniqueCandidates, options);
|
|
399
|
+
const scoredCandidates = await Promise.all(ageFilteredCandidates.map(async (candidate) => ({
|
|
186
400
|
person: candidate,
|
|
187
|
-
score: this.scorer.scoreMatch(candidate, options),
|
|
188
|
-
reason: this.scorer.getMatchReason(candidate, options),
|
|
189
|
-
|
|
401
|
+
score: await this.scorer.scoreMatch(candidate, options),
|
|
402
|
+
reason: await this.scorer.getMatchReason(candidate, options),
|
|
403
|
+
isVerifiedContactMatch: verifiedMatches.some(v => v.id === candidate.id)
|
|
404
|
+
})));
|
|
405
|
+
return scoredCandidates.sort((a, b) => {
|
|
406
|
+
if (a.isVerifiedContactMatch && !b.isVerifiedContactMatch)
|
|
407
|
+
return -1;
|
|
408
|
+
if (!a.isVerifiedContactMatch && b.isVerifiedContactMatch)
|
|
409
|
+
return 1;
|
|
410
|
+
return b.score - a.score;
|
|
411
|
+
});
|
|
190
412
|
}
|
|
191
413
|
/**
|
|
192
414
|
* Check if a person matches the given criteria
|
|
193
415
|
*/
|
|
194
416
|
async isMatch(personId, options) {
|
|
195
417
|
const person = await this.peopleModule.getById(personId);
|
|
196
|
-
const score = this.scorer.scoreMatch(person, options);
|
|
418
|
+
const score = await this.scorer.scoreMatch(person, options);
|
|
197
419
|
if (score > 0.5) { // Threshold for considering it a match
|
|
420
|
+
// Check if this is a verified contact match
|
|
421
|
+
let isVerifiedContactMatch = false;
|
|
422
|
+
if (options.email) {
|
|
423
|
+
isVerifiedContactMatch = await this.verifyEmailMatch(person, options.email);
|
|
424
|
+
}
|
|
425
|
+
if (!isVerifiedContactMatch && options.phone) {
|
|
426
|
+
isVerifiedContactMatch = await this.verifyPhoneMatch(person, options.phone);
|
|
427
|
+
}
|
|
198
428
|
return {
|
|
199
429
|
person,
|
|
200
430
|
score,
|
|
201
|
-
reason: this.scorer.getMatchReason(person, options),
|
|
431
|
+
reason: await this.scorer.getMatchReason(person, options),
|
|
432
|
+
isVerifiedContactMatch,
|
|
202
433
|
};
|
|
203
434
|
}
|
|
204
435
|
return null;
|
|
@@ -2,24 +2,26 @@
|
|
|
2
2
|
* v2.0.0 Person Match Scoring
|
|
3
3
|
*/
|
|
4
4
|
import type { PersonResource } from '../types';
|
|
5
|
-
import type { PersonMatchOptions } from '../modules/people';
|
|
5
|
+
import type { PersonMatchOptions, PeopleModule } from '../modules/people';
|
|
6
6
|
export declare class MatchScorer {
|
|
7
|
+
private peopleModule;
|
|
8
|
+
constructor(peopleModule: PeopleModule);
|
|
7
9
|
/**
|
|
8
10
|
* Score a person match based on various criteria
|
|
9
11
|
*/
|
|
10
|
-
scoreMatch(person: PersonResource, options: PersonMatchOptions): number
|
|
12
|
+
scoreMatch(person: PersonResource, options: PersonMatchOptions): Promise<number>;
|
|
11
13
|
/**
|
|
12
14
|
* Get a human-readable reason for the match
|
|
13
15
|
*/
|
|
14
|
-
getMatchReason(person: PersonResource, options: PersonMatchOptions): string
|
|
16
|
+
getMatchReason(person: PersonResource, options: PersonMatchOptions): Promise<string>;
|
|
15
17
|
/**
|
|
16
|
-
* Score email matching
|
|
18
|
+
* Score email matching - verifies actual email matches
|
|
17
19
|
*/
|
|
18
|
-
|
|
20
|
+
scoreEmailMatch(person: PersonResource, email: string): Promise<number>;
|
|
19
21
|
/**
|
|
20
|
-
* Score phone matching
|
|
22
|
+
* Score phone matching - verifies actual phone matches
|
|
21
23
|
*/
|
|
22
|
-
|
|
24
|
+
scorePhoneMatch(person: PersonResource, phone: string): Promise<number>;
|
|
23
25
|
/**
|
|
24
26
|
* Score name matching - only exact matches
|
|
25
27
|
*/
|
package/dist/matching/scoring.js
CHANGED
|
@@ -6,21 +6,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.MatchScorer = void 0;
|
|
7
7
|
const helpers_1 = require("../helpers");
|
|
8
8
|
class MatchScorer {
|
|
9
|
+
constructor(peopleModule) {
|
|
10
|
+
this.peopleModule = peopleModule;
|
|
11
|
+
}
|
|
9
12
|
/**
|
|
10
13
|
* Score a person match based on various criteria
|
|
11
14
|
*/
|
|
12
|
-
scoreMatch(person, options) {
|
|
15
|
+
async scoreMatch(person, options) {
|
|
13
16
|
let totalScore = 0;
|
|
14
17
|
let maxScore = 0;
|
|
15
18
|
// Email matching (highest weight)
|
|
16
19
|
if (options.email) {
|
|
17
|
-
const emailScore = this.scoreEmailMatch(person, options.email);
|
|
20
|
+
const emailScore = await this.scoreEmailMatch(person, options.email);
|
|
18
21
|
totalScore += emailScore * 0.35;
|
|
19
22
|
maxScore += 0.35;
|
|
20
23
|
}
|
|
21
24
|
// Phone matching (high weight)
|
|
22
25
|
if (options.phone) {
|
|
23
|
-
const phoneScore = this.scorePhoneMatch(person, options.phone);
|
|
26
|
+
const phoneScore = await this.scorePhoneMatch(person, options.phone);
|
|
24
27
|
totalScore += phoneScore * 0.25;
|
|
25
28
|
maxScore += 0.25;
|
|
26
29
|
}
|
|
@@ -45,13 +48,19 @@ class MatchScorer {
|
|
|
45
48
|
/**
|
|
46
49
|
* Get a human-readable reason for the match
|
|
47
50
|
*/
|
|
48
|
-
getMatchReason(person, options) {
|
|
51
|
+
async getMatchReason(person, options) {
|
|
49
52
|
const reasons = [];
|
|
50
|
-
if (options.email
|
|
51
|
-
|
|
53
|
+
if (options.email) {
|
|
54
|
+
const emailScore = await this.scoreEmailMatch(person, options.email);
|
|
55
|
+
if (emailScore > 0.8) {
|
|
56
|
+
reasons.push('exact email match');
|
|
57
|
+
}
|
|
52
58
|
}
|
|
53
|
-
if (options.phone
|
|
54
|
-
|
|
59
|
+
if (options.phone) {
|
|
60
|
+
const phoneScore = await this.scorePhoneMatch(person, options.phone);
|
|
61
|
+
if (phoneScore > 0.8) {
|
|
62
|
+
reasons.push('exact phone match');
|
|
63
|
+
}
|
|
55
64
|
}
|
|
56
65
|
if (options.firstName || options.lastName) {
|
|
57
66
|
const nameScore = this.scoreNameMatch(person, options);
|
|
@@ -84,22 +93,42 @@ class MatchScorer {
|
|
|
84
93
|
return reasons.join(', ');
|
|
85
94
|
}
|
|
86
95
|
/**
|
|
87
|
-
* Score email matching
|
|
96
|
+
* Score email matching - verifies actual email matches
|
|
88
97
|
*/
|
|
89
|
-
scoreEmailMatch(person, email) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
async scoreEmailMatch(person, email) {
|
|
99
|
+
try {
|
|
100
|
+
const personEmails = await this.peopleModule.getEmails(person.id);
|
|
101
|
+
const normalizedSearchEmail = email.toLowerCase().trim();
|
|
102
|
+
// Check if any of the person's emails match
|
|
103
|
+
const emails = personEmails.data?.map(e => e.attributes?.address?.toLowerCase().trim()).filter(Boolean) || [];
|
|
104
|
+
return emails.includes(normalizedSearchEmail) ? 1.0 : 0.0;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.warn(`Failed to verify email match for person ${person.id}:`, error);
|
|
108
|
+
return 0.0;
|
|
109
|
+
}
|
|
94
110
|
}
|
|
95
111
|
/**
|
|
96
|
-
* Score phone matching
|
|
112
|
+
* Score phone matching - verifies actual phone matches
|
|
97
113
|
*/
|
|
98
|
-
scorePhoneMatch(person, phone) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
async scorePhoneMatch(person, phone) {
|
|
115
|
+
try {
|
|
116
|
+
const personPhones = await this.peopleModule.getPhoneNumbers(person.id);
|
|
117
|
+
// Normalize phone numbers for comparison
|
|
118
|
+
const normalizePhone = (num) => {
|
|
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) || [];
|
|
126
|
+
return phones.includes(normalizedSearchPhone) ? 1.0 : 0.0;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.warn(`Failed to verify phone match for person ${person.id}:`, error);
|
|
130
|
+
return 0.0;
|
|
131
|
+
}
|
|
103
132
|
}
|
|
104
133
|
/**
|
|
105
134
|
* Score name matching - only exact matches
|
|
@@ -10,6 +10,7 @@ export declare class MatchStrategies {
|
|
|
10
10
|
selectBestMatch(candidates: MatchResult[], strategy: MatchStrategy): MatchResult | null;
|
|
11
11
|
/**
|
|
12
12
|
* Exact matching strategy - only return matches with very high confidence
|
|
13
|
+
* Requires verified email/phone matches unless multiple people share the same contact info
|
|
13
14
|
*/
|
|
14
15
|
private selectExactMatch;
|
|
15
16
|
/**
|
|
@@ -25,10 +25,23 @@ class MatchStrategies {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Exact matching strategy - only return matches with very high confidence
|
|
28
|
+
* Requires verified email/phone matches unless multiple people share the same contact info
|
|
28
29
|
*/
|
|
29
30
|
selectExactMatch(candidates) {
|
|
30
|
-
//
|
|
31
|
-
|
|
31
|
+
// For exact match, require verified email/phone match
|
|
32
|
+
// Only allow name-only matches if multiple people share same email/phone (to distinguish)
|
|
33
|
+
const verifiedMatches = candidates.filter(c => c.isVerifiedContactMatch);
|
|
34
|
+
const verifiedCount = verifiedMatches.length;
|
|
35
|
+
const exactMatches = candidates.filter(c => {
|
|
36
|
+
// Must have score >= 0.8
|
|
37
|
+
if (c.score < 0.8)
|
|
38
|
+
return false;
|
|
39
|
+
// Must be a verified contact match (email/phone), OR
|
|
40
|
+
// Must be a name-only match with multiple verified matches (to distinguish)
|
|
41
|
+
const isNameMatch = c.reason.includes('name match');
|
|
42
|
+
return c.isVerifiedContactMatch ||
|
|
43
|
+
(isNameMatch && verifiedCount > 1);
|
|
44
|
+
});
|
|
32
45
|
return exactMatches.length > 0 ? exactMatches[0] : null;
|
|
33
46
|
}
|
|
34
47
|
/**
|
package/dist/modules/campus.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { BaseModule } from '
|
|
2
|
-
import type { PcoHttpClient } from '
|
|
3
|
-
import type { PaginationHelper } from '
|
|
4
|
-
import type { PcoEventEmitter } from '
|
|
5
|
-
import type { PaginationOptions, PaginationResult } from '
|
|
1
|
+
import { BaseModule } from '@rachelallyson/planning-center-base-ts';
|
|
2
|
+
import type { PcoHttpClient } from '@rachelallyson/planning-center-base-ts';
|
|
3
|
+
import type { PaginationHelper } from '@rachelallyson/planning-center-base-ts';
|
|
4
|
+
import type { PcoEventEmitter } from '@rachelallyson/planning-center-base-ts';
|
|
5
|
+
import type { PaginationOptions, PaginationResult } from '@rachelallyson/planning-center-base-ts';
|
|
6
6
|
import type { CampusResource, CampusAttributes, CampusesList } from '../types';
|
|
7
7
|
/**
|
|
8
8
|
* Campus module for managing campus-related operations
|
package/dist/modules/campus.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CampusModule = void 0;
|
|
4
|
-
const
|
|
4
|
+
const planning_center_base_ts_1 = require("@rachelallyson/planning-center-base-ts");
|
|
5
5
|
/**
|
|
6
6
|
* Campus module for managing campus-related operations
|
|
7
7
|
*/
|
|
8
|
-
class CampusModule extends
|
|
8
|
+
class CampusModule extends planning_center_base_ts_1.BaseModule {
|
|
9
9
|
constructor(httpClient, paginationHelper, eventEmitter) {
|
|
10
10
|
super(httpClient, paginationHelper, eventEmitter);
|
|
11
11
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* v2.0.0 Contacts Module
|
|
3
3
|
*/
|
|
4
|
-
import { BaseModule } from '
|
|
4
|
+
import { BaseModule } from '@rachelallyson/planning-center-base-ts';
|
|
5
5
|
import type { EmailResource, EmailAttributes, PhoneNumberResource, PhoneNumberAttributes, AddressResource, AddressAttributes, SocialProfileResource, SocialProfileAttributes } from '../types';
|
|
6
6
|
export declare class ContactsModule extends BaseModule {
|
|
7
7
|
/**
|
package/dist/modules/contacts.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ContactsModule = void 0;
|
|
7
|
-
const
|
|
8
|
-
class ContactsModule extends
|
|
7
|
+
const planning_center_base_ts_1 = require("@rachelallyson/planning-center-base-ts");
|
|
8
|
+
class ContactsModule extends planning_center_base_ts_1.BaseModule {
|
|
9
9
|
/**
|
|
10
10
|
* Get all emails
|
|
11
11
|
*/
|
package/dist/modules/fields.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* v2.0.0 Fields Module
|
|
3
3
|
*/
|
|
4
|
-
import { BaseModule } from '
|
|
5
|
-
import type { PcoHttpClient } from '
|
|
6
|
-
import type { PaginationHelper } from '
|
|
7
|
-
import type { PcoEventEmitter } from '
|
|
4
|
+
import { BaseModule } from '@rachelallyson/planning-center-base-ts';
|
|
5
|
+
import type { PcoHttpClient } from '@rachelallyson/planning-center-base-ts';
|
|
6
|
+
import type { PaginationHelper } from '@rachelallyson/planning-center-base-ts';
|
|
7
|
+
import type { PcoEventEmitter } from '@rachelallyson/planning-center-base-ts';
|
|
8
8
|
import type { FieldDefinitionResource, FieldDefinitionAttributes, FieldDatumResource, FieldOptionResource, FieldOptionAttributes, TabResource, TabAttributes } from '../types';
|
|
9
9
|
export interface FieldDefinitionCache {
|
|
10
10
|
byId: Map<string, FieldDefinitionResource>;
|
package/dist/modules/fields.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.FieldsModule = void 0;
|
|
7
|
-
const
|
|
8
|
-
class FieldsModule extends
|
|
7
|
+
const planning_center_base_ts_1 = require("@rachelallyson/planning-center-base-ts");
|
|
8
|
+
class FieldsModule extends planning_center_base_ts_1.BaseModule {
|
|
9
9
|
constructor(httpClient, paginationHelper, eventEmitter) {
|
|
10
10
|
super(httpClient, paginationHelper, eventEmitter);
|
|
11
11
|
this.fieldDefinitionCache = null;
|
package/dist/modules/forms.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
1
|
+
/**
|
|
2
|
+
* v2.0.0 Forms Module
|
|
3
|
+
*/
|
|
4
|
+
import { BaseModule } from '@rachelallyson/planning-center-base-ts';
|
|
5
|
+
import type { PcoHttpClient } from '@rachelallyson/planning-center-base-ts';
|
|
6
|
+
import type { PaginationHelper } from '@rachelallyson/planning-center-base-ts';
|
|
7
|
+
import type { PcoEventEmitter } from '@rachelallyson/planning-center-base-ts';
|
|
5
8
|
import type { FormResource, FormsList, FormCategoryResource, FormFieldResource, FormFieldOptionResource, FormSubmissionResource, FormSubmissionValueResource } from '../types';
|
|
6
9
|
/**
|
|
7
10
|
* Forms module for managing form-related operations
|
package/dist/modules/forms.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* v2.0.0 Forms Module
|
|
4
|
+
*/
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.FormsModule = void 0;
|
|
4
|
-
const
|
|
7
|
+
const planning_center_base_ts_1 = require("@rachelallyson/planning-center-base-ts");
|
|
5
8
|
/**
|
|
6
9
|
* Forms module for managing form-related operations
|
|
7
10
|
* Most operations are read-only based on API documentation
|
|
8
11
|
*/
|
|
9
|
-
class FormsModule extends
|
|
12
|
+
class FormsModule extends planning_center_base_ts_1.BaseModule {
|
|
10
13
|
constructor(httpClient, paginationHelper, eventEmitter) {
|
|
11
14
|
super(httpClient, paginationHelper, eventEmitter);
|
|
12
15
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* v2.0.0 Households Module
|
|
3
3
|
*/
|
|
4
|
-
import { BaseModule } from '
|
|
5
|
-
import type { PaginationOptions, PaginationResult } from '
|
|
4
|
+
import { BaseModule } from '@rachelallyson/planning-center-base-ts';
|
|
5
|
+
import type { PaginationOptions, PaginationResult } from '@rachelallyson/planning-center-base-ts';
|
|
6
6
|
import type { HouseholdResource, HouseholdAttributes } from '../types';
|
|
7
7
|
export interface HouseholdListOptions {
|
|
8
8
|
where?: Record<string, any>;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.HouseholdsModule = void 0;
|
|
7
|
-
const
|
|
8
|
-
class HouseholdsModule extends
|
|
7
|
+
const planning_center_base_ts_1 = require("@rachelallyson/planning-center-base-ts");
|
|
8
|
+
class HouseholdsModule extends planning_center_base_ts_1.BaseModule {
|
|
9
9
|
/**
|
|
10
10
|
* Get all households
|
|
11
11
|
*/
|
package/dist/modules/lists.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* v2.0.0 Lists Module
|
|
3
3
|
*/
|
|
4
|
-
import { BaseModule } from '
|
|
5
|
-
import type { PaginationOptions, PaginationResult } from '
|
|
4
|
+
import { BaseModule } from '@rachelallyson/planning-center-base-ts';
|
|
5
|
+
import type { PaginationOptions, PaginationResult } from '@rachelallyson/planning-center-base-ts';
|
|
6
6
|
import type { ListResource, ListCategoryResource, ListCategoryAttributes, PersonResource } from '../types';
|
|
7
7
|
export interface ListsListOptions {
|
|
8
8
|
where?: Record<string, any>;
|