@rachelallyson/planning-center-people-ts 2.8.0 → 2.9.1

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +1 -1
  3. package/README.md +12 -14
  4. package/dist/client-manager.js +5 -3
  5. package/dist/client.d.ts +8 -8
  6. package/dist/client.js +8 -11
  7. package/dist/core/http.d.ts +1 -1
  8. package/dist/core/http.js +15 -11
  9. package/dist/core.d.ts +3 -3
  10. package/dist/core.js +4 -4
  11. package/dist/error-handling.d.ts +1 -1
  12. package/dist/error-handling.js +3 -3
  13. package/dist/error-scenarios.js +4 -4
  14. package/dist/index.d.ts +8 -8
  15. package/dist/index.js +15 -15
  16. package/dist/matching/matcher.d.ts +22 -0
  17. package/dist/matching/matcher.js +251 -20
  18. package/dist/matching/scoring.d.ts +9 -7
  19. package/dist/matching/scoring.js +49 -20
  20. package/dist/matching/strategies.d.ts +1 -0
  21. package/dist/matching/strategies.js +15 -2
  22. package/dist/modules/campus.d.ts +5 -5
  23. package/dist/modules/campus.js +2 -2
  24. package/dist/modules/contacts.d.ts +1 -1
  25. package/dist/modules/contacts.js +2 -2
  26. package/dist/modules/fields.d.ts +4 -4
  27. package/dist/modules/fields.js +2 -2
  28. package/dist/modules/forms.d.ts +7 -4
  29. package/dist/modules/forms.js +5 -2
  30. package/dist/modules/households.d.ts +2 -2
  31. package/dist/modules/households.js +2 -2
  32. package/dist/modules/lists.d.ts +2 -2
  33. package/dist/modules/lists.js +2 -2
  34. package/dist/modules/notes.d.ts +2 -2
  35. package/dist/modules/notes.js +2 -2
  36. package/dist/modules/people.d.ts +57 -5
  37. package/dist/modules/people.js +38 -8
  38. package/dist/modules/reports.d.ts +5 -5
  39. package/dist/modules/reports.js +2 -2
  40. package/dist/modules/service-time.d.ts +5 -5
  41. package/dist/modules/service-time.js +2 -2
  42. package/dist/modules/workflows.d.ts +2 -2
  43. package/dist/modules/workflows.js +2 -2
  44. package/dist/people/contacts.d.ts +1 -1
  45. package/dist/people/core.d.ts +1 -1
  46. package/dist/people/fields.d.ts +1 -1
  47. package/dist/people/fields.js +4 -4
  48. package/dist/people/households.d.ts +1 -1
  49. package/dist/people/lists.d.ts +1 -1
  50. package/dist/people/notes.d.ts +1 -1
  51. package/dist/people/organization.d.ts +1 -1
  52. package/dist/people/workflows.d.ts +1 -1
  53. package/dist/types/client.d.ts +3 -96
  54. package/dist/types/client.js +2 -0
  55. package/dist/types/index.d.ts +1 -2
  56. package/dist/types/index.js +0 -2
  57. package/dist/types/people.d.ts +15 -15
  58. package/package.json +15 -17
  59. package/dist/api-error.d.ts +0 -10
  60. package/dist/api-error.js +0 -32
  61. package/dist/batch.d.ts +0 -47
  62. package/dist/batch.js +0 -376
  63. package/dist/modules/base.d.ts +0 -46
  64. package/dist/modules/base.js +0 -82
  65. package/dist/monitoring.d.ts +0 -53
  66. package/dist/monitoring.js +0 -142
  67. package/dist/rate-limiter.d.ts +0 -79
  68. package/dist/rate-limiter.js +0 -137
  69. package/dist/types/batch.d.ts +0 -50
  70. package/dist/types/batch.js +0 -5
  71. package/dist/types/events.d.ts +0 -85
  72. package/dist/types/events.js +0 -5
@@ -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
- return match.person;
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
- // Get all potential matches
38
- const candidates = await this.getCandidates(options);
39
- if (candidates.length === 0) {
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 = candidates.map(candidate => ({
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
- // Sort by score (highest first)
49
- scoredCandidates.sort((a, b) => b.score - a.score);
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
- const candidates = await this.getCandidates(options);
185
- return candidates.map(candidate => ({
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
- })).sort((a, b) => b.score - a.score);
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
- private scoreEmailMatch;
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
- private scorePhoneMatch;
24
+ scorePhoneMatch(person: PersonResource, phone: string): Promise<number>;
23
25
  /**
24
26
  * Score name matching - only exact matches
25
27
  */
@@ -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 && this.scoreEmailMatch(person, options.email) > 0.8) {
51
- reasons.push('exact email match');
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 && this.scorePhoneMatch(person, options.phone) > 0.8) {
54
- reasons.push('exact phone match');
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
- // Check if the person has the email in their relationships or attributes
91
- // For now, we'll assume if the search found this person by email, it's a match
92
- // In a more sophisticated implementation, we'd fetch and compare actual emails
93
- return 1.0; // Perfect match since search found this person by email
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
- // Check if the person has the phone in their relationships or attributes
100
- // For now, we'll assume if the search found this person by phone, it's a match
101
- // In a more sophisticated implementation, we'd fetch and compare actual phones
102
- return 1.0; // Perfect match since search found this person by phone
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
- // Only return matches with score >= 0.8 (lowered from 0.9)
31
- const exactMatches = candidates.filter(c => c.score >= 0.8);
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
  /**
@@ -1,8 +1,8 @@
1
- import { BaseModule } from './base';
2
- import type { PcoHttpClient } from '../core/http';
3
- import type { PaginationHelper } from '../core/pagination';
4
- import type { PcoEventEmitter } from '../monitoring';
5
- import type { PaginationOptions, PaginationResult } from '../core/pagination';
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
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CampusModule = void 0;
4
- const base_1 = require("./base");
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 base_1.BaseModule {
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 './base';
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
  /**
@@ -4,8 +4,8 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ContactsModule = void 0;
7
- const base_1 = require("./base");
8
- class ContactsModule extends base_1.BaseModule {
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
  */
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * v2.0.0 Fields Module
3
3
  */
4
- import { BaseModule } from './base';
5
- import type { PcoHttpClient } from '../core/http';
6
- import type { PaginationHelper } from '../core/pagination';
7
- import type { PcoEventEmitter } from '../monitoring';
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>;
@@ -4,8 +4,8 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.FieldsModule = void 0;
7
- const base_1 = require("./base");
8
- class FieldsModule extends base_1.BaseModule {
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;
@@ -1,7 +1,10 @@
1
- import { BaseModule } from './base';
2
- import type { PcoHttpClient } from '../core/http';
3
- import type { PaginationHelper } from '../core/pagination';
4
- import type { PcoEventEmitter } from '../monitoring';
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
@@ -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 base_1 = require("./base");
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 base_1.BaseModule {
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 './base';
5
- import type { PaginationOptions, PaginationResult } from '../core/pagination';
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 base_1 = require("./base");
8
- class HouseholdsModule extends base_1.BaseModule {
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
  */
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * v2.0.0 Lists Module
3
3
  */
4
- import { BaseModule } from './base';
5
- import type { PaginationOptions, PaginationResult } from '../core/pagination';
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>;