@rachelallyson/planning-center-people-ts 2.7.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/LICENSE +1 -1
  3. package/README.md +12 -14
  4. package/dist/client.d.ts +8 -8
  5. package/dist/client.js +8 -11
  6. package/dist/core/http.d.ts +1 -1
  7. package/dist/core/http.js +18 -11
  8. package/dist/core/pagination.js +16 -2
  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/helpers.js +5 -4
  15. package/dist/index.d.ts +8 -8
  16. package/dist/index.js +15 -15
  17. package/dist/matching/matcher.d.ts +22 -0
  18. package/dist/matching/matcher.js +260 -27
  19. package/dist/matching/scoring.d.ts +9 -7
  20. package/dist/matching/scoring.js +54 -23
  21. package/dist/matching/strategies.d.ts +1 -0
  22. package/dist/matching/strategies.js +17 -4
  23. package/dist/modules/campus.d.ts +5 -5
  24. package/dist/modules/campus.js +2 -2
  25. package/dist/modules/contacts.d.ts +1 -1
  26. package/dist/modules/contacts.js +2 -2
  27. package/dist/modules/fields.d.ts +4 -4
  28. package/dist/modules/fields.js +2 -2
  29. package/dist/modules/forms.d.ts +7 -4
  30. package/dist/modules/forms.js +5 -2
  31. package/dist/modules/households.d.ts +2 -2
  32. package/dist/modules/households.js +2 -2
  33. package/dist/modules/lists.d.ts +2 -2
  34. package/dist/modules/lists.js +2 -2
  35. package/dist/modules/notes.d.ts +2 -2
  36. package/dist/modules/notes.js +2 -2
  37. package/dist/modules/people.d.ts +57 -5
  38. package/dist/modules/people.js +44 -9
  39. package/dist/modules/reports.d.ts +5 -5
  40. package/dist/modules/reports.js +2 -2
  41. package/dist/modules/service-time.d.ts +5 -5
  42. package/dist/modules/service-time.js +2 -2
  43. package/dist/modules/workflows.d.ts +2 -2
  44. package/dist/modules/workflows.js +2 -2
  45. package/dist/people/contacts.d.ts +1 -1
  46. package/dist/people/core.d.ts +1 -1
  47. package/dist/people/fields.d.ts +1 -1
  48. package/dist/people/fields.js +4 -4
  49. package/dist/people/households.d.ts +1 -1
  50. package/dist/people/lists.d.ts +1 -1
  51. package/dist/people/notes.d.ts +1 -1
  52. package/dist/people/organization.d.ts +1 -1
  53. package/dist/people/workflows.d.ts +1 -1
  54. package/dist/types/client.d.ts +3 -96
  55. package/dist/types/client.js +2 -0
  56. package/dist/types/index.d.ts +1 -2
  57. package/dist/types/index.js +0 -2
  58. package/package.json +16 -19
  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
package/dist/index.js CHANGED
@@ -41,21 +41,21 @@ Object.defineProperty(exports, "attemptTokenRefresh", { enumerable: true, get: f
41
41
  Object.defineProperty(exports, "hasRefreshTokenCapability", { enumerable: true, get: function () { return auth_1.hasRefreshTokenCapability; } });
42
42
  Object.defineProperty(exports, "refreshAccessToken", { enumerable: true, get: function () { return auth_1.refreshAccessToken; } });
43
43
  Object.defineProperty(exports, "updateClientTokens", { enumerable: true, get: function () { return auth_1.updateClientTokens; } });
44
- // Export API error
45
- var api_error_1 = require("./api-error");
46
- Object.defineProperty(exports, "PcoApiError", { enumerable: true, get: function () { return api_error_1.PcoApiError; } });
47
- var rate_limiter_1 = require("./rate-limiter");
48
- Object.defineProperty(exports, "PcoRateLimiter", { enumerable: true, get: function () { return rate_limiter_1.PcoRateLimiter; } });
49
- var error_handling_1 = require("./error-handling");
50
- Object.defineProperty(exports, "ErrorCategory", { enumerable: true, get: function () { return error_handling_1.ErrorCategory; } });
51
- Object.defineProperty(exports, "ErrorSeverity", { enumerable: true, get: function () { return error_handling_1.ErrorSeverity; } });
52
- Object.defineProperty(exports, "handleNetworkError", { enumerable: true, get: function () { return error_handling_1.handleNetworkError; } });
53
- Object.defineProperty(exports, "handleTimeoutError", { enumerable: true, get: function () { return error_handling_1.handleTimeoutError; } });
54
- Object.defineProperty(exports, "handleValidationError", { enumerable: true, get: function () { return error_handling_1.handleValidationError; } });
55
- Object.defineProperty(exports, "PcoError", { enumerable: true, get: function () { return error_handling_1.PcoError; } });
56
- Object.defineProperty(exports, "retryWithBackoff", { enumerable: true, get: function () { return error_handling_1.retryWithBackoff; } });
57
- Object.defineProperty(exports, "shouldNotRetry", { enumerable: true, get: function () { return error_handling_1.shouldNotRetry; } });
58
- Object.defineProperty(exports, "withErrorBoundary", { enumerable: true, get: function () { return error_handling_1.withErrorBoundary; } });
44
+ // Export API error (re-exported from base)
45
+ var planning_center_base_ts_1 = require("@rachelallyson/planning-center-base-ts");
46
+ Object.defineProperty(exports, "PcoApiError", { enumerable: true, get: function () { return planning_center_base_ts_1.PcoApiError; } });
47
+ var planning_center_base_ts_2 = require("@rachelallyson/planning-center-base-ts");
48
+ Object.defineProperty(exports, "PcoRateLimiter", { enumerable: true, get: function () { return planning_center_base_ts_2.PcoRateLimiter; } });
49
+ var planning_center_base_ts_3 = require("@rachelallyson/planning-center-base-ts");
50
+ Object.defineProperty(exports, "ErrorCategory", { enumerable: true, get: function () { return planning_center_base_ts_3.ErrorCategory; } });
51
+ Object.defineProperty(exports, "ErrorSeverity", { enumerable: true, get: function () { return planning_center_base_ts_3.ErrorSeverity; } });
52
+ Object.defineProperty(exports, "handleNetworkError", { enumerable: true, get: function () { return planning_center_base_ts_3.handleNetworkError; } });
53
+ Object.defineProperty(exports, "handleTimeoutError", { enumerable: true, get: function () { return planning_center_base_ts_3.handleTimeoutError; } });
54
+ Object.defineProperty(exports, "handleValidationError", { enumerable: true, get: function () { return planning_center_base_ts_3.handleValidationError; } });
55
+ Object.defineProperty(exports, "PcoError", { enumerable: true, get: function () { return planning_center_base_ts_3.PcoError; } });
56
+ Object.defineProperty(exports, "retryWithBackoff", { enumerable: true, get: function () { return planning_center_base_ts_3.retryWithBackoff; } });
57
+ Object.defineProperty(exports, "shouldNotRetry", { enumerable: true, get: function () { return planning_center_base_ts_3.shouldNotRetry; } });
58
+ Object.defineProperty(exports, "withErrorBoundary", { enumerable: true, get: function () { return planning_center_base_ts_3.withErrorBoundary; } });
59
59
  // Export People-specific functions (deprecated)
60
60
  var people_1 = require("./people");
61
61
  Object.defineProperty(exports, "createFieldDefinition", { enumerable: true, get: function () { return people_1.createFieldDefinition; } });
@@ -8,6 +8,7 @@ export interface MatchResult {
8
8
  person: PersonResource;
9
9
  score: number;
10
10
  reason: string;
11
+ isVerifiedContactMatch?: boolean;
11
12
  }
12
13
  export declare class PersonMatcher {
13
14
  private peopleModule;
@@ -16,6 +17,14 @@ export declare class PersonMatcher {
16
17
  constructor(peopleModule: PeopleModule);
17
18
  /**
18
19
  * Find or create a person with smart matching
20
+ *
21
+ * Uses intelligent matching logic that:
22
+ * - Verifies email/phone matches by checking actual contact information
23
+ * - Only uses name matching when appropriate (multiple people share contact info, or no contact info provided)
24
+ * - Can automatically add missing contact information when a match is found (if addMissingContactInfo is true)
25
+ *
26
+ * @param options - Matching options
27
+ * @param options.addMissingContactInfo - If true, automatically adds missing email/phone to matched person's profile
19
28
  */
20
29
  findOrCreate(options: PersonMatchOptions): Promise<PersonResource>;
21
30
  /**
@@ -24,8 +33,21 @@ export declare class PersonMatcher {
24
33
  findMatch(options: PersonMatchOptions): Promise<MatchResult | null>;
25
34
  /**
26
35
  * Get potential matching candidates
36
+ * @deprecated Use findMatch which has improved logic for separating verified matches from name-only matches
27
37
  */
28
38
  private getCandidates;
39
+ /**
40
+ * Verify if a person's email actually matches the search email
41
+ */
42
+ private verifyEmailMatch;
43
+ /**
44
+ * Verify if a person's phone actually matches the search phone
45
+ */
46
+ private verifyPhoneMatch;
47
+ /**
48
+ * Add missing contact information to a person's profile
49
+ */
50
+ private addMissingContactInfo;
29
51
  /**
30
52
  * Filter candidates by age preferences
31
53
  */
@@ -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 = [];
@@ -64,7 +154,7 @@ class PersonMatcher {
64
154
  candidates.push(...emailMatches.data);
65
155
  }
66
156
  catch (error) {
67
- // Email search failed, continue with other strategies
157
+ console.warn('Email search failed:', error);
68
158
  }
69
159
  }
70
160
  // Strategy 2: Exact phone match
@@ -74,7 +164,7 @@ class PersonMatcher {
74
164
  candidates.push(...phoneMatches.data);
75
165
  }
76
166
  catch (error) {
77
- // Phone search failed, continue with other strategies
167
+ console.warn('Phone search failed:', error);
78
168
  }
79
169
  }
80
170
  // Strategy 3: Name-based search
@@ -86,7 +176,7 @@ class PersonMatcher {
86
176
  candidates.push(...nameMatches.data);
87
177
  }
88
178
  catch (error) {
89
- // Name search failed, continue with other strategies
179
+ console.warn('Name search failed:', error);
90
180
  }
91
181
  }
92
182
  // Strategy 4: Broader search if no exact matches
@@ -98,7 +188,7 @@ class PersonMatcher {
98
188
  candidates.push(...broadMatches.data);
99
189
  }
100
190
  catch (error) {
101
- // Broad search failed
191
+ console.warn('Broad search failed:', error);
102
192
  }
103
193
  }
104
194
  // Remove duplicates based on person ID
@@ -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
  */
@@ -145,11 +307,12 @@ class PersonMatcher {
145
307
  try {
146
308
  await this.peopleModule.addEmail(person.id, {
147
309
  address: options.email,
310
+ location: 'Home', // Required field
148
311
  primary: true
149
312
  });
150
313
  }
151
314
  catch (error) {
152
- console.warn('Failed to create email contact:', error);
315
+ console.warn(`Failed to create email contact for person ${person.id}:`, error);
153
316
  }
154
317
  }
155
318
  // Add phone contact if provided
@@ -157,11 +320,12 @@ class PersonMatcher {
157
320
  try {
158
321
  await this.peopleModule.addPhoneNumber(person.id, {
159
322
  number: options.phone,
323
+ location: 'Home', // Required field
160
324
  primary: true
161
325
  });
162
326
  }
163
327
  catch (error) {
164
- console.warn('Failed to create phone contact:', error);
328
+ console.warn(`Failed to create phone contact for person ${person.id}:`, error);
165
329
  }
166
330
  }
167
331
  // Set campus if provided
@@ -170,7 +334,7 @@ class PersonMatcher {
170
334
  await this.peopleModule.setPrimaryCampus(person.id, options.campusId);
171
335
  }
172
336
  catch (error) {
173
- console.warn('Failed to set campus:', error);
337
+ console.warn(`Failed to set campus for person ${person.id}:`, error);
174
338
  }
175
339
  }
176
340
  return person;
@@ -179,24 +343,93 @@ class PersonMatcher {
179
343
  * Get all potential matches with detailed scoring
180
344
  */
181
345
  async getAllMatches(options) {
182
- const candidates = await this.getCandidates(options);
183
- 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) => ({
184
400
  person: candidate,
185
- score: this.scorer.scoreMatch(candidate, options),
186
- reason: this.scorer.getMatchReason(candidate, options),
187
- })).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
+ });
188
412
  }
189
413
  /**
190
414
  * Check if a person matches the given criteria
191
415
  */
192
416
  async isMatch(personId, options) {
193
417
  const person = await this.peopleModule.getById(personId);
194
- const score = this.scorer.scoreMatch(person, options);
418
+ const score = await this.scorer.scoreMatch(person, options);
195
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
+ }
196
428
  return {
197
429
  person,
198
430
  score,
199
- reason: this.scorer.getMatchReason(person, options),
431
+ reason: await this.scorer.getMatchReason(person, options),
432
+ isVerifiedContactMatch,
200
433
  };
201
434
  }
202
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,29 +6,34 @@ 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
  }
27
- // Name matching (medium weight)
30
+ // Name matching (increased weight for name-only matches)
28
31
  if (options.firstName || options.lastName) {
29
32
  const nameScore = this.scoreNameMatch(person, options);
30
- totalScore += nameScore * 0.2;
31
- maxScore += 0.2;
33
+ // Increase weight to 0.4 for name-only matches, 0.2 when combined with other criteria
34
+ const nameWeight = (!options.email && !options.phone) ? 0.4 : 0.2;
35
+ totalScore += nameScore * nameWeight;
36
+ maxScore += nameWeight;
32
37
  }
33
38
  // Age matching (medium weight)
34
39
  const ageScore = this.scoreAgeMatch(person, options);
@@ -43,13 +48,19 @@ class MatchScorer {
43
48
  /**
44
49
  * Get a human-readable reason for the match
45
50
  */
46
- getMatchReason(person, options) {
51
+ async getMatchReason(person, options) {
47
52
  const reasons = [];
48
- if (options.email && this.scoreEmailMatch(person, options.email) > 0.8) {
49
- 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
+ }
50
58
  }
51
- if (options.phone && this.scorePhoneMatch(person, options.phone) > 0.8) {
52
- 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
+ }
53
64
  }
54
65
  if (options.firstName || options.lastName) {
55
66
  const nameScore = this.scoreNameMatch(person, options);
@@ -82,22 +93,42 @@ class MatchScorer {
82
93
  return reasons.join(', ');
83
94
  }
84
95
  /**
85
- * Score email matching
96
+ * Score email matching - verifies actual email matches
86
97
  */
87
- scoreEmailMatch(person, email) {
88
- // This would need to check the person's emails
89
- // For now, return a placeholder score
90
- // In a real implementation, you'd fetch the person's emails and compare
91
- return 0;
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
+ }
92
110
  }
93
111
  /**
94
- * Score phone matching
112
+ * Score phone matching - verifies actual phone matches
95
113
  */
96
- scorePhoneMatch(person, phone) {
97
- // This would need to check the person's phone numbers
98
- // For now, return a placeholder score
99
- // In a real implementation, you'd fetch the person's phone numbers and compare
100
- return 0;
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
+ }
101
132
  }
102
133
  /**
103
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
  /**