@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 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**
@@ -45,4 +45,8 @@ export declare class PcoHttpClient {
45
45
  remaining: number;
46
46
  resetTime: number;
47
47
  }>;
48
+ /**
49
+ * Get authentication header for external services (like file uploads)
50
+ */
51
+ getAuthHeader(): string;
48
52
  }
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
  */
@@ -26,6 +26,10 @@ export declare class PersonMatcher {
26
26
  * Get potential matching candidates
27
27
  */
28
28
  private getCandidates;
29
+ /**
30
+ * Filter candidates by age preferences
31
+ */
32
+ private filterByAgePreferences;
29
33
  /**
30
34
  * Create a new person
31
35
  */
@@ -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
- return uniqueCandidates;
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
  */
@@ -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.4;
18
- maxScore += 0.4;
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.3;
24
- maxScore += 0.3;
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.1;
35
- maxScore += 0.1;
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.6) {
55
- reasons.push('similar name match');
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 firstNameScore = this.calculateStringSimilarity(options.firstName.toLowerCase(), attrs.first_name.toLowerCase());
92
- score += firstNameScore * 0.5;
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 lastNameScore = this.calculateStringSimilarity(options.lastName.toLowerCase(), attrs.last_name.toLowerCase());
97
- score += lastNameScore * 0.5;
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
  */
@@ -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
  */
@@ -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 this.getAllPages('/campuses', queryParams, paginationOptions);
93
+ return super.getAllPages('/campuses', queryParams, paginationOptions);
75
94
  }
76
95
  }
77
96
  exports.CampusModule = CampusModule;
@@ -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
  }
@@ -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
- // This would implement the file upload logic from the original implementation
226
- // For now, return a placeholder
227
- throw new Error('File upload functionality not yet implemented in v2.0');
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;
@@ -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.0",
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",