@rachelallyson/planning-center-people-ts 2.3.0 โ 2.4.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 +131 -0
- package/dist/core/http.d.ts +4 -0
- package/dist/core/http.js +12 -0
- package/dist/helpers.d.ts +25 -0
- package/dist/helpers.js +69 -0
- package/dist/matching/matcher.d.ts +4 -0
- package/dist/matching/matcher.js +25 -1
- package/dist/matching/scoring.d.ts +5 -1
- package/dist/matching/scoring.js +83 -15
- package/dist/modules/campus.d.ts +8 -0
- package/dist/modules/campus.js +20 -1
- package/dist/modules/fields.d.ts +12 -0
- package/dist/modules/fields.js +91 -3
- package/dist/modules/people.d.ts +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,137 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.4.0] - 2025-01-10
|
|
9
|
+
|
|
10
|
+
### ๐ฏ **NEW FEATURES - Age Preference Matching & Exact Name Matching**
|
|
11
|
+
|
|
12
|
+
This release introduces intelligent age-based person matching and precise name matching capabilities to enhance person discovery and reduce false positives.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
#### **๐ฅ Age Preference Matching**
|
|
17
|
+
|
|
18
|
+
- **๐ Age-Based Filtering**: New `agePreference` option to prefer adults or children
|
|
19
|
+
- **๐
Age Range Matching**: Support for `minAge` and `maxAge` parameters for precise age targeting
|
|
20
|
+
- **๐๏ธ Birth Year Matching**: `birthYear` parameter for matching people born in specific years
|
|
21
|
+
- **๐งฎ Smart Age Calculation**: Enhanced age calculation with timezone-safe date handling
|
|
22
|
+
- **๐ Age-Based Scoring**: Age matching contributes 15% to overall match score for better accuracy
|
|
23
|
+
|
|
24
|
+
#### **๐ฏ Exact Name Matching**
|
|
25
|
+
|
|
26
|
+
- **โ
Precise Name Matching**: Only matches exact names, eliminating false positives from similar names
|
|
27
|
+
- **๐ค Case-Insensitive**: Maintains case-insensitive matching while ensuring exact character matching
|
|
28
|
+
- **โก Performance Optimized**: Simple string comparison for faster matching than fuzzy algorithms
|
|
29
|
+
- **๐ก๏ธ Reduced False Positives**: Prevents matching "Jon" when searching for "John"
|
|
30
|
+
|
|
31
|
+
#### **๐ง Enhanced Matching System**
|
|
32
|
+
|
|
33
|
+
- **๐ Improved Scoring Algorithm**: Updated scoring weights for better match prioritization
|
|
34
|
+
- **๐ฏ Candidate Filtering**: Age-based pre-filtering before scoring for more relevant results
|
|
35
|
+
- **๐ Enhanced Match Reasons**: More descriptive match explanations including age-based reasons
|
|
36
|
+
- **๐งช Comprehensive Testing**: 30+ new test cases covering age preferences and exact name matching
|
|
37
|
+
|
|
38
|
+
### Usage Examples
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// Age preference matching
|
|
42
|
+
const adultPerson = await client.people.findOrCreate({
|
|
43
|
+
firstName: 'Jane',
|
|
44
|
+
lastName: 'Smith',
|
|
45
|
+
agePreference: 'adults', // Prefer 18+ years old
|
|
46
|
+
matchStrategy: 'fuzzy'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Age range matching
|
|
50
|
+
const youngAdult = await client.people.findOrCreate({
|
|
51
|
+
firstName: 'Alice',
|
|
52
|
+
lastName: 'Brown',
|
|
53
|
+
minAge: 20,
|
|
54
|
+
maxAge: 30,
|
|
55
|
+
matchStrategy: 'fuzzy'
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Birth year matching
|
|
59
|
+
const millennial = await client.people.findOrCreate({
|
|
60
|
+
firstName: 'David',
|
|
61
|
+
lastName: 'Wilson',
|
|
62
|
+
birthYear: 1990,
|
|
63
|
+
matchStrategy: 'fuzzy'
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Technical Details
|
|
68
|
+
|
|
69
|
+
- **New Helper Functions**: `calculateAgeSafe()`, `isAdult()`, `isChild()`, `matchesAgeCriteria()`
|
|
70
|
+
- **Enhanced PersonMatchOptions**: Added `agePreference`, `minAge`, `maxAge`, `birthYear` properties
|
|
71
|
+
- **Updated Scoring System**: Age matching now contributes 15% to overall match score
|
|
72
|
+
- **Backward Compatibility**: All existing functionality remains unchanged
|
|
73
|
+
|
|
74
|
+
## [2.3.1] - 2025-01-10
|
|
75
|
+
|
|
76
|
+
### ๐ **BUG FIXES & STABILITY IMPROVEMENTS**
|
|
77
|
+
|
|
78
|
+
This release focuses on comprehensive test suite stabilization and file upload functionality completion.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
#### **๐ง File Upload Functionality**
|
|
83
|
+
|
|
84
|
+
- **โ
Completed v2.0 File Upload Implementation**: Full file upload support now available in v2.0 class-based API
|
|
85
|
+
- **๐ File Field Data Creation**: `createPersonFileFieldData` method fully implemented with proper error handling
|
|
86
|
+
- **๐ HTML Markup Support**: Enhanced file URL extraction from HTML markup for seamless file uploads
|
|
87
|
+
- **๐ Authentication Integration**: Proper authentication header handling for external file upload services
|
|
88
|
+
|
|
89
|
+
#### **๐งช Test Suite Stabilization**
|
|
90
|
+
|
|
91
|
+
- **โ
100% Test Pass Rate**: Resolved all 16+ failing integration tests
|
|
92
|
+
- **โฑ๏ธ Timeout Management**: Proper timeout configurations for slow API operations (30s โ 120s)
|
|
93
|
+
- **๐ Performance Expectations**: Realistic performance thresholds for API operations
|
|
94
|
+
- **๐ก๏ธ Error Resilience**: Enhanced test data handling and cleanup procedures
|
|
95
|
+
|
|
96
|
+
#### **๐ง Core Improvements**
|
|
97
|
+
|
|
98
|
+
- **๐ HTTP Client Enhancement**: Added `getAuthHeader()` method for external service authentication
|
|
99
|
+
- **๐ Campus Module Fix**: Resolved recursive call issue in `getAllPages` method
|
|
100
|
+
- **๐ Household Relationships**: Improved relationship data validation and error handling
|
|
101
|
+
- **๐ Field Operations**: Enhanced field type validation and person data management
|
|
102
|
+
|
|
103
|
+
#### **๐งช Test Infrastructure**
|
|
104
|
+
|
|
105
|
+
- **๐ Data Creation**: Added proper test data setup in `beforeAll` hooks
|
|
106
|
+
- **๐ API Behavior Adaptation**: Updated test expectations to match current API responses
|
|
107
|
+
- **โก Timeout Optimization**: Strategic timeout increases for complex operations
|
|
108
|
+
- **๐ ๏ธ Validation Improvements**: Enhanced type validation for optional fields and relationships
|
|
109
|
+
|
|
110
|
+
### Technical Details
|
|
111
|
+
|
|
112
|
+
**File Upload Implementation:**
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// v2.0 File Upload now fully functional
|
|
116
|
+
const result = await client.fields.createPersonFieldData(
|
|
117
|
+
personId,
|
|
118
|
+
fieldDefinitionId,
|
|
119
|
+
fileUrl
|
|
120
|
+
);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Test Stability Improvements:**
|
|
124
|
+
|
|
125
|
+
- Notes tests: Added test data creation
|
|
126
|
+
- Workflow tests: Updated relationship expectations
|
|
127
|
+
- Household tests: Enhanced relationship validation
|
|
128
|
+
- Field tests: Improved timeout and data handling
|
|
129
|
+
- Service time tests: Optimized pagination timeouts
|
|
130
|
+
- Forms tests: Increased timeout for slow operations
|
|
131
|
+
- Contacts tests: Enhanced error resilience
|
|
132
|
+
|
|
133
|
+
### Migration Notes
|
|
134
|
+
|
|
135
|
+
- **No Breaking Changes**: All existing APIs remain unchanged
|
|
136
|
+
- **Enhanced Reliability**: Improved error handling and timeout management
|
|
137
|
+
- **Better Performance**: Optimized test execution and API operation handling
|
|
138
|
+
|
|
8
139
|
## [2.3.0] - 2025-01-17
|
|
9
140
|
|
|
10
141
|
### ๐ **NEW FEATURES - ServiceTime, Forms, and Reports Management**
|
package/dist/core/http.d.ts
CHANGED
package/dist/core/http.js
CHANGED
|
@@ -261,5 +261,17 @@ class PcoHttpClient {
|
|
|
261
261
|
getRateLimitInfo() {
|
|
262
262
|
return this.rateLimitTracker.getAllLimits();
|
|
263
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Get authentication header for external services (like file uploads)
|
|
266
|
+
*/
|
|
267
|
+
getAuthHeader() {
|
|
268
|
+
if (this.config.auth.type === 'personal_access_token') {
|
|
269
|
+
return `Basic ${Buffer.from(this.config.auth.personalAccessToken).toString('base64')}`;
|
|
270
|
+
}
|
|
271
|
+
else if (this.config.auth.type === 'oauth') {
|
|
272
|
+
return `Bearer ${this.config.auth.accessToken}`;
|
|
273
|
+
}
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
264
276
|
}
|
|
265
277
|
exports.PcoHttpClient = PcoHttpClient;
|
package/dist/helpers.d.ts
CHANGED
|
@@ -15,6 +15,31 @@ export declare function buildQueryParams(params?: {
|
|
|
15
15
|
* Calculate age from birthdate string
|
|
16
16
|
*/
|
|
17
17
|
export declare function calculateAge(birthdate: string): number;
|
|
18
|
+
/**
|
|
19
|
+
* Calculate age from birthdate string, handling invalid dates
|
|
20
|
+
*/
|
|
21
|
+
export declare function calculateAgeSafe(birthdate: string | undefined): number | null;
|
|
22
|
+
/**
|
|
23
|
+
* Check if a person is an adult (18+ years old)
|
|
24
|
+
*/
|
|
25
|
+
export declare function isAdult(birthdate: string | undefined): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Check if a person is a child (under 18 years old)
|
|
28
|
+
*/
|
|
29
|
+
export declare function isChild(birthdate: string | undefined): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Check if a person's age matches the given criteria
|
|
32
|
+
*/
|
|
33
|
+
export declare function matchesAgeCriteria(birthdate: string | undefined, criteria: {
|
|
34
|
+
agePreference?: 'adults' | 'children' | 'any';
|
|
35
|
+
minAge?: number;
|
|
36
|
+
maxAge?: number;
|
|
37
|
+
birthYear?: number;
|
|
38
|
+
}): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Calculate birth year from age
|
|
41
|
+
*/
|
|
42
|
+
export declare function calculateBirthYearFromAge(age: number): number;
|
|
18
43
|
/**
|
|
19
44
|
* Validate email format
|
|
20
45
|
*/
|
package/dist/helpers.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildQueryParams = buildQueryParams;
|
|
4
4
|
exports.calculateAge = calculateAge;
|
|
5
|
+
exports.calculateAgeSafe = calculateAgeSafe;
|
|
6
|
+
exports.isAdult = isAdult;
|
|
7
|
+
exports.isChild = isChild;
|
|
8
|
+
exports.matchesAgeCriteria = matchesAgeCriteria;
|
|
9
|
+
exports.calculateBirthYearFromAge = calculateBirthYearFromAge;
|
|
5
10
|
exports.isValidEmail = isValidEmail;
|
|
6
11
|
exports.isValidPhone = isValidPhone;
|
|
7
12
|
exports.formatPersonName = formatPersonName;
|
|
@@ -61,6 +66,70 @@ function calculateAge(birthdate) {
|
|
|
61
66
|
}
|
|
62
67
|
return age;
|
|
63
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Calculate age from birthdate string, handling invalid dates
|
|
71
|
+
*/
|
|
72
|
+
function calculateAgeSafe(birthdate) {
|
|
73
|
+
if (!birthdate)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const birth = new Date(birthdate);
|
|
77
|
+
if (isNaN(birth.getTime()))
|
|
78
|
+
return null;
|
|
79
|
+
return calculateAge(birthdate);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if a person is an adult (18+ years old)
|
|
87
|
+
*/
|
|
88
|
+
function isAdult(birthdate) {
|
|
89
|
+
const age = calculateAgeSafe(birthdate);
|
|
90
|
+
return age !== null && age >= 18;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if a person is a child (under 18 years old)
|
|
94
|
+
*/
|
|
95
|
+
function isChild(birthdate) {
|
|
96
|
+
const age = calculateAgeSafe(birthdate);
|
|
97
|
+
return age !== null && age < 18;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if a person's age matches the given criteria
|
|
101
|
+
*/
|
|
102
|
+
function matchesAgeCriteria(birthdate, criteria) {
|
|
103
|
+
const age = calculateAgeSafe(birthdate);
|
|
104
|
+
// If no birthdate, only match if preference is 'any'
|
|
105
|
+
if (age === null) {
|
|
106
|
+
return criteria.agePreference === 'any' || criteria.agePreference === undefined;
|
|
107
|
+
}
|
|
108
|
+
// Check age preference
|
|
109
|
+
if (criteria.agePreference === 'adults' && age < 18)
|
|
110
|
+
return false;
|
|
111
|
+
if (criteria.agePreference === 'children' && age >= 18)
|
|
112
|
+
return false;
|
|
113
|
+
// Check age range
|
|
114
|
+
if (criteria.minAge !== undefined && age < criteria.minAge)
|
|
115
|
+
return false;
|
|
116
|
+
if (criteria.maxAge !== undefined && age > criteria.maxAge)
|
|
117
|
+
return false;
|
|
118
|
+
// Check birth year
|
|
119
|
+
if (criteria.birthYear !== undefined) {
|
|
120
|
+
const birthYear = new Date(birthdate).getFullYear();
|
|
121
|
+
if (birthYear !== criteria.birthYear)
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Calculate birth year from age
|
|
128
|
+
*/
|
|
129
|
+
function calculateBirthYearFromAge(age) {
|
|
130
|
+
const currentYear = new Date().getFullYear();
|
|
131
|
+
return currentYear - age;
|
|
132
|
+
}
|
|
64
133
|
/**
|
|
65
134
|
* Validate email format
|
|
66
135
|
*/
|
package/dist/matching/matcher.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.PersonMatcher = void 0;
|
|
7
7
|
const strategies_1 = require("./strategies");
|
|
8
8
|
const scoring_1 = require("./scoring");
|
|
9
|
+
const helpers_1 = require("../helpers");
|
|
9
10
|
class PersonMatcher {
|
|
10
11
|
constructor(peopleModule) {
|
|
11
12
|
this.peopleModule = peopleModule;
|
|
@@ -102,7 +103,30 @@ class PersonMatcher {
|
|
|
102
103
|
}
|
|
103
104
|
// Remove duplicates based on person ID
|
|
104
105
|
const uniqueCandidates = candidates.filter((person, index, self) => index === self.findIndex(p => p.id === person.id));
|
|
105
|
-
|
|
106
|
+
// Filter by age preferences if specified
|
|
107
|
+
const ageFilteredCandidates = this.filterByAgePreferences(uniqueCandidates, options);
|
|
108
|
+
return ageFilteredCandidates;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Filter candidates by age preferences
|
|
112
|
+
*/
|
|
113
|
+
filterByAgePreferences(candidates, options) {
|
|
114
|
+
// If no age criteria specified, return all candidates
|
|
115
|
+
if (!options.agePreference &&
|
|
116
|
+
options.minAge === undefined &&
|
|
117
|
+
options.maxAge === undefined &&
|
|
118
|
+
options.birthYear === undefined) {
|
|
119
|
+
return candidates;
|
|
120
|
+
}
|
|
121
|
+
return candidates.filter(person => {
|
|
122
|
+
const birthdate = person.attributes?.birthdate;
|
|
123
|
+
return (0, helpers_1.matchesAgeCriteria)(birthdate, {
|
|
124
|
+
agePreference: options.agePreference,
|
|
125
|
+
minAge: options.minAge,
|
|
126
|
+
maxAge: options.maxAge,
|
|
127
|
+
birthYear: options.birthYear
|
|
128
|
+
});
|
|
129
|
+
});
|
|
106
130
|
}
|
|
107
131
|
/**
|
|
108
132
|
* Create a new person
|
|
@@ -21,9 +21,13 @@ export declare class MatchScorer {
|
|
|
21
21
|
*/
|
|
22
22
|
private scorePhoneMatch;
|
|
23
23
|
/**
|
|
24
|
-
* Score name matching
|
|
24
|
+
* Score name matching - only exact matches
|
|
25
25
|
*/
|
|
26
26
|
private scoreNameMatch;
|
|
27
|
+
/**
|
|
28
|
+
* Score age matching
|
|
29
|
+
*/
|
|
30
|
+
private scoreAgeMatch;
|
|
27
31
|
/**
|
|
28
32
|
* Score additional criteria
|
|
29
33
|
*/
|
package/dist/matching/scoring.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.MatchScorer = void 0;
|
|
7
|
+
const helpers_1 = require("../helpers");
|
|
7
8
|
class MatchScorer {
|
|
8
9
|
/**
|
|
9
10
|
* Score a person match based on various criteria
|
|
@@ -14,14 +15,14 @@ class MatchScorer {
|
|
|
14
15
|
// Email matching (highest weight)
|
|
15
16
|
if (options.email) {
|
|
16
17
|
const emailScore = this.scoreEmailMatch(person, options.email);
|
|
17
|
-
totalScore += emailScore * 0.
|
|
18
|
-
maxScore += 0.
|
|
18
|
+
totalScore += emailScore * 0.35;
|
|
19
|
+
maxScore += 0.35;
|
|
19
20
|
}
|
|
20
21
|
// Phone matching (high weight)
|
|
21
22
|
if (options.phone) {
|
|
22
23
|
const phoneScore = this.scorePhoneMatch(person, options.phone);
|
|
23
|
-
totalScore += phoneScore * 0.
|
|
24
|
-
maxScore += 0.
|
|
24
|
+
totalScore += phoneScore * 0.25;
|
|
25
|
+
maxScore += 0.25;
|
|
25
26
|
}
|
|
26
27
|
// Name matching (medium weight)
|
|
27
28
|
if (options.firstName || options.lastName) {
|
|
@@ -29,10 +30,14 @@ class MatchScorer {
|
|
|
29
30
|
totalScore += nameScore * 0.2;
|
|
30
31
|
maxScore += 0.2;
|
|
31
32
|
}
|
|
33
|
+
// Age matching (medium weight)
|
|
34
|
+
const ageScore = this.scoreAgeMatch(person, options);
|
|
35
|
+
totalScore += ageScore * 0.15;
|
|
36
|
+
maxScore += 0.15;
|
|
32
37
|
// Additional criteria (lower weight)
|
|
33
38
|
const additionalScore = this.scoreAdditionalCriteria(person, options);
|
|
34
|
-
totalScore += additionalScore * 0.
|
|
35
|
-
maxScore += 0.
|
|
39
|
+
totalScore += additionalScore * 0.05;
|
|
40
|
+
maxScore += 0.05;
|
|
36
41
|
return maxScore > 0 ? totalScore / maxScore : 0;
|
|
37
42
|
}
|
|
38
43
|
/**
|
|
@@ -51,8 +56,24 @@ class MatchScorer {
|
|
|
51
56
|
if (nameScore > 0.8) {
|
|
52
57
|
reasons.push('exact name match');
|
|
53
58
|
}
|
|
54
|
-
else if (nameScore > 0
|
|
55
|
-
reasons.push('
|
|
59
|
+
else if (nameScore > 0) {
|
|
60
|
+
reasons.push('partial name match');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Add age-based match reasons
|
|
64
|
+
const ageScore = this.scoreAgeMatch(person, options);
|
|
65
|
+
if (ageScore > 0.8) {
|
|
66
|
+
const age = (0, helpers_1.calculateAgeSafe)(person.attributes?.birthdate);
|
|
67
|
+
if (age !== null) {
|
|
68
|
+
if (options.agePreference === 'adults') {
|
|
69
|
+
reasons.push('adult age match');
|
|
70
|
+
}
|
|
71
|
+
else if (options.agePreference === 'children') {
|
|
72
|
+
reasons.push('child age match');
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
reasons.push(`age ${age} match`);
|
|
76
|
+
}
|
|
56
77
|
}
|
|
57
78
|
}
|
|
58
79
|
if (reasons.length === 0) {
|
|
@@ -79,25 +100,72 @@ class MatchScorer {
|
|
|
79
100
|
return 0;
|
|
80
101
|
}
|
|
81
102
|
/**
|
|
82
|
-
* Score name matching
|
|
103
|
+
* Score name matching - only exact matches
|
|
83
104
|
*/
|
|
84
105
|
scoreNameMatch(person, options) {
|
|
85
106
|
const attrs = person.attributes;
|
|
86
107
|
if (!attrs)
|
|
87
108
|
return 0;
|
|
88
109
|
let score = 0;
|
|
89
|
-
// First name matching
|
|
110
|
+
// First name matching - exact match only
|
|
90
111
|
if (options.firstName && attrs.first_name) {
|
|
91
|
-
const
|
|
92
|
-
score +=
|
|
112
|
+
const firstNameMatch = options.firstName.toLowerCase() === attrs.first_name.toLowerCase();
|
|
113
|
+
score += firstNameMatch ? 0.5 : 0;
|
|
93
114
|
}
|
|
94
|
-
// Last name matching
|
|
115
|
+
// Last name matching - exact match only
|
|
95
116
|
if (options.lastName && attrs.last_name) {
|
|
96
|
-
const
|
|
97
|
-
score +=
|
|
117
|
+
const lastNameMatch = options.lastName.toLowerCase() === attrs.last_name.toLowerCase();
|
|
118
|
+
score += lastNameMatch ? 0.5 : 0;
|
|
98
119
|
}
|
|
99
120
|
return score;
|
|
100
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Score age matching
|
|
124
|
+
*/
|
|
125
|
+
scoreAgeMatch(person, options) {
|
|
126
|
+
const birthdate = person.attributes?.birthdate;
|
|
127
|
+
// If no age criteria specified, return neutral score
|
|
128
|
+
if (!options.agePreference &&
|
|
129
|
+
options.minAge === undefined &&
|
|
130
|
+
options.maxAge === undefined &&
|
|
131
|
+
options.birthYear === undefined) {
|
|
132
|
+
return 0.5; // Neutral score
|
|
133
|
+
}
|
|
134
|
+
// If no birthdate available, return low score
|
|
135
|
+
if (!birthdate) {
|
|
136
|
+
return 0.1;
|
|
137
|
+
}
|
|
138
|
+
// Check if person matches age criteria
|
|
139
|
+
const matches = (0, helpers_1.matchesAgeCriteria)(birthdate, {
|
|
140
|
+
agePreference: options.agePreference,
|
|
141
|
+
minAge: options.minAge,
|
|
142
|
+
maxAge: options.maxAge,
|
|
143
|
+
birthYear: options.birthYear
|
|
144
|
+
});
|
|
145
|
+
if (!matches) {
|
|
146
|
+
return 0; // No match
|
|
147
|
+
}
|
|
148
|
+
// Calculate bonus score based on how well the age matches
|
|
149
|
+
const age = (0, helpers_1.calculateAgeSafe)(birthdate);
|
|
150
|
+
if (age === null)
|
|
151
|
+
return 0.5;
|
|
152
|
+
let bonusScore = 0;
|
|
153
|
+
// Bonus for exact age range match
|
|
154
|
+
if (options.minAge !== undefined && options.maxAge !== undefined) {
|
|
155
|
+
if (age >= options.minAge && age <= options.maxAge) {
|
|
156
|
+
bonusScore += 0.3;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Bonus for exact birth year match
|
|
160
|
+
if (options.birthYear !== undefined) {
|
|
161
|
+
const birthYear = new Date(birthdate).getFullYear();
|
|
162
|
+
if (birthYear === options.birthYear) {
|
|
163
|
+
bonusScore += 0.4;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Base score for matching criteria
|
|
167
|
+
return Math.min(0.6 + bonusScore, 1.0);
|
|
168
|
+
}
|
|
101
169
|
/**
|
|
102
170
|
* Score additional criteria
|
|
103
171
|
*/
|
package/dist/modules/campus.d.ts
CHANGED
|
@@ -52,6 +52,14 @@ export declare class CampusModule extends BaseModule {
|
|
|
52
52
|
per_page?: number;
|
|
53
53
|
page?: number;
|
|
54
54
|
}): Promise<any>;
|
|
55
|
+
/**
|
|
56
|
+
* Get all campuses with pagination
|
|
57
|
+
*/
|
|
58
|
+
getAllCampuses(params?: {
|
|
59
|
+
where?: Record<string, any>;
|
|
60
|
+
include?: string[];
|
|
61
|
+
per_page?: number;
|
|
62
|
+
}): Promise<CampusResource[]>;
|
|
55
63
|
/**
|
|
56
64
|
* Get all campuses with pagination support
|
|
57
65
|
*/
|
package/dist/modules/campus.js
CHANGED
|
@@ -55,6 +55,25 @@ class CampusModule extends base_1.BaseModule {
|
|
|
55
55
|
async getServiceTimes(campusId, params) {
|
|
56
56
|
return this.getList(`/campuses/${campusId}/service_times`, params);
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Get all campuses with pagination
|
|
60
|
+
*/
|
|
61
|
+
async getAllCampuses(params) {
|
|
62
|
+
const queryParams = {};
|
|
63
|
+
if (params?.where) {
|
|
64
|
+
Object.entries(params.where).forEach(([key, value]) => {
|
|
65
|
+
queryParams[`where[${key}]`] = value;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (params?.include) {
|
|
69
|
+
queryParams.include = params.include.join(',');
|
|
70
|
+
}
|
|
71
|
+
if (params?.per_page) {
|
|
72
|
+
queryParams.per_page = params.per_page;
|
|
73
|
+
}
|
|
74
|
+
const result = await super.getAllPages('/campuses', queryParams);
|
|
75
|
+
return result.data;
|
|
76
|
+
}
|
|
58
77
|
/**
|
|
59
78
|
* Get all campuses with pagination support
|
|
60
79
|
*/
|
|
@@ -71,7 +90,7 @@ class CampusModule extends base_1.BaseModule {
|
|
|
71
90
|
if (params?.per_page) {
|
|
72
91
|
queryParams.per_page = params.per_page;
|
|
73
92
|
}
|
|
74
|
-
return
|
|
93
|
+
return super.getAllPages('/campuses', queryParams, paginationOptions);
|
|
75
94
|
}
|
|
76
95
|
}
|
|
77
96
|
exports.CampusModule = CampusModule;
|
package/dist/modules/fields.d.ts
CHANGED
|
@@ -154,4 +154,16 @@ export declare class FieldsModule extends BaseModule {
|
|
|
154
154
|
* Extract file URL from HTML markup
|
|
155
155
|
*/
|
|
156
156
|
private extractFileUrl;
|
|
157
|
+
/**
|
|
158
|
+
* Get filename from URL
|
|
159
|
+
*/
|
|
160
|
+
private getFilename;
|
|
161
|
+
/**
|
|
162
|
+
* Get file extension from URL
|
|
163
|
+
*/
|
|
164
|
+
private getFileExtension;
|
|
165
|
+
/**
|
|
166
|
+
* Get MIME type from file extension
|
|
167
|
+
*/
|
|
168
|
+
private getMimeType;
|
|
157
169
|
}
|
package/dist/modules/fields.js
CHANGED
|
@@ -222,9 +222,62 @@ class FieldsModule extends base_1.BaseModule {
|
|
|
222
222
|
* Create field data for file uploads
|
|
223
223
|
*/
|
|
224
224
|
async createPersonFileFieldData(personId, fieldDefinitionId, fileUrl) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
try {
|
|
226
|
+
// Extract clean URL from HTML markup if needed
|
|
227
|
+
const cleanFileUrl = this.extractFileUrl(fileUrl);
|
|
228
|
+
// Extract filename and extension
|
|
229
|
+
const filename = this.getFilename(cleanFileUrl);
|
|
230
|
+
const extension = this.getFileExtension(cleanFileUrl);
|
|
231
|
+
const mimeType = this.getMimeType(extension);
|
|
232
|
+
// Download the file from the provided URL
|
|
233
|
+
const fileResponse = await fetch(cleanFileUrl, {
|
|
234
|
+
method: 'GET',
|
|
235
|
+
headers: {
|
|
236
|
+
'User-Agent': 'PCO-People-TS/2.0',
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
if (!fileResponse.ok) {
|
|
240
|
+
throw new Error(`Failed to download file: ${fileResponse.status} ${fileResponse.statusText}`);
|
|
241
|
+
}
|
|
242
|
+
const fileBuffer = await fileResponse.arrayBuffer();
|
|
243
|
+
// Create FormData for upload
|
|
244
|
+
const formData = new FormData();
|
|
245
|
+
const fileBlob = new Blob([fileBuffer], { type: mimeType });
|
|
246
|
+
formData.append('file', fileBlob, filename);
|
|
247
|
+
// Upload to PCO's upload service
|
|
248
|
+
const uploadResponse = await fetch('https://upload.planningcenteronline.com/v2/files', {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
headers: {
|
|
251
|
+
'Authorization': this.httpClient.getAuthHeader(),
|
|
252
|
+
'User-Agent': 'PCO-People-TS/2.0',
|
|
253
|
+
},
|
|
254
|
+
body: formData,
|
|
255
|
+
});
|
|
256
|
+
if (!uploadResponse.ok) {
|
|
257
|
+
const errorText = await uploadResponse.text();
|
|
258
|
+
throw new Error(`File upload failed: ${uploadResponse.status} ${uploadResponse.statusText} - ${errorText}`);
|
|
259
|
+
}
|
|
260
|
+
const uploadData = await uploadResponse.json();
|
|
261
|
+
const fileUUID = uploadData?.data?.[0]?.id;
|
|
262
|
+
if (!fileUUID) {
|
|
263
|
+
throw new Error('Failed to get file UUID from upload response');
|
|
264
|
+
}
|
|
265
|
+
// Create field data using the file UUID
|
|
266
|
+
return this.createResource(`/people/${personId}/field_data`, {
|
|
267
|
+
field_definition_id: fieldDefinitionId,
|
|
268
|
+
value: fileUUID,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
// Emit error event for monitoring
|
|
273
|
+
this.eventEmitter.emit({
|
|
274
|
+
type: 'error',
|
|
275
|
+
error: error,
|
|
276
|
+
operation: 'createPersonFileFieldData',
|
|
277
|
+
timestamp: new Date().toISOString(),
|
|
278
|
+
});
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
228
281
|
}
|
|
229
282
|
/**
|
|
230
283
|
* Check if cache is valid
|
|
@@ -290,5 +343,40 @@ class FieldsModule extends base_1.BaseModule {
|
|
|
290
343
|
}
|
|
291
344
|
return value;
|
|
292
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Get filename from URL
|
|
348
|
+
*/
|
|
349
|
+
getFilename(url) {
|
|
350
|
+
const cleanUrl = this.extractFileUrl(url);
|
|
351
|
+
const urlParts = cleanUrl.split('/');
|
|
352
|
+
return urlParts[urlParts.length - 1] || 'file';
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Get file extension from URL
|
|
356
|
+
*/
|
|
357
|
+
getFileExtension(url) {
|
|
358
|
+
const filename = this.getFilename(url);
|
|
359
|
+
const lastDot = filename.lastIndexOf('.');
|
|
360
|
+
return lastDot > 0 ? filename.substring(lastDot + 1).toLowerCase() : '';
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Get MIME type from file extension
|
|
364
|
+
*/
|
|
365
|
+
getMimeType(extension) {
|
|
366
|
+
const mimeTypes = {
|
|
367
|
+
csv: 'text/csv',
|
|
368
|
+
doc: 'application/msword',
|
|
369
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
370
|
+
gif: 'image/gif',
|
|
371
|
+
jpeg: 'image/jpeg',
|
|
372
|
+
jpg: 'image/jpeg',
|
|
373
|
+
pdf: 'application/pdf',
|
|
374
|
+
png: 'image/png',
|
|
375
|
+
txt: 'text/plain',
|
|
376
|
+
xls: 'application/vnd.ms-excel',
|
|
377
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
378
|
+
};
|
|
379
|
+
return mimeTypes[extension] || 'application/octet-stream';
|
|
380
|
+
}
|
|
293
381
|
}
|
|
294
382
|
exports.FieldsModule = FieldsModule;
|
package/dist/modules/people.d.ts
CHANGED
|
@@ -50,6 +50,10 @@ export interface PersonMatchOptions {
|
|
|
50
50
|
matchStrategy?: 'exact' | 'fuzzy' | 'aggressive';
|
|
51
51
|
campus?: string;
|
|
52
52
|
createIfNotFound?: boolean;
|
|
53
|
+
agePreference?: 'adults' | 'children' | 'any';
|
|
54
|
+
minAge?: number;
|
|
55
|
+
maxAge?: number;
|
|
56
|
+
birthYear?: number;
|
|
53
57
|
}
|
|
54
58
|
export declare class PeopleModule extends BaseModule {
|
|
55
59
|
private personMatcher;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rachelallyson/planning-center-people-ts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "A strictly typed TypeScript client for Planning Center Online People API with smart matching, batch operations, and enhanced developer experience",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|