@ixiam/n8n-nodes-civicrm 1.1.18 → 1.1.23

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 iXiam Global Solutions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -7,20 +7,25 @@
7
7
  Community Node for **CiviCRM API v4** (Civi-Go compatible)
8
8
  Developed and maintained by **Ixiam Global Solutions**.
9
9
 
10
- This package provides integration between **n8n** and **CiviCRM API v4**, supporting create, update, delete operations, advanced filtering, dynamic location types, and structured sub-entities (email, phone, address).
11
- Custom fields are **not** supported.
10
+ This node enables full integration between **n8n** and **CiviCRM API v4**, supporting create/update/delete operations, smart field mapping, dynamic location types, and advanced filtering on GET operations.
12
11
 
13
12
  ---
14
- # About CiviCRM
13
+
14
+ ## About CiviCRM
15
+
16
+ <p align="center">
17
+ <img src="https://civicrm.org/sites/civicrm.org/files/CiviCRM-logo-2019-F2-200px.png" alt="CiviCRM Logo" width="200"/>
18
+ </p>
15
19
 
16
20
  CiviCRM is an open-source Constituent Relationship Management platform designed for nonprofits, NGOs, and advocacy organizations. It supports contact management, memberships, contributions, event registration, email marketing, case management, and reporting. CiviCRM integrates with WordPress, Drupal, and Joomla.
17
21
 
18
22
  Download: https://civicrm.org/download
19
23
 
24
+ ---
20
25
 
21
- ## Installation
26
+ ## 🚀 Installation
22
27
 
23
- 1. In your n8n instance, open:
28
+ 1. In your n8n instance, go to:
24
29
  **Settings → Community Nodes → Install**
25
30
  2. Enter the package name:
26
31
 
@@ -28,61 +33,82 @@ Download: https://civicrm.org/download
28
33
  @ixiam/n8n-nodes-civicrm
29
34
  ```
30
35
 
31
- 3. Confirm installation and enable Community Nodes.
36
+ 3. Approve installation and enable Community Nodes.
32
37
  4. If running n8n via Docker, restart/rebuild for the node to load.
33
38
 
34
39
  ---
35
40
 
36
- ## Credentials
41
+ ## 🔐 Credentials
37
42
 
38
- Authentication uses **Bearer Token**.
43
+ The node uses **Bearer Token Authentication**.
39
44
 
40
45
  | Field | Description |
41
46
  |-------|-------------|
42
- | **Base URL** | Root URL of your CiviCRM instance (no trailing slash). Example: `https://crm.example.org` |
47
+ | **Base URL** | The root URL of your CiviCRM instance (without trailing slash). Example: `https://crm.example.org` |
43
48
  | **API Token** | Sent as header `X-Civi-Auth: Bearer <token>` |
44
49
 
45
- ---
50
+ After entering credentials, click **Save** to validate the connection.
46
51
 
47
- ## Supported Entities
52
+ ---
48
53
 
49
- This node implements API v4 operations for:
54
+ ## 📦 Supported Entities
50
55
 
51
- - Contact
52
- - Membership
53
- - Group
54
- - Relationship
55
- - Activity
56
- - Custom API Call (raw API4 request)
56
+ The node includes full API v4 support for the following entities:
57
57
 
58
- Each entity supports:
59
- - get
60
- - getMany
61
- - create
62
- - update
63
- - delete
58
+ | Entity | Operations |
59
+ |--------|------------|
60
+ | **Contact** | get, getMany, create, update, delete |
61
+ | **Membership** | get, getMany, create, update, delete |
62
+ | **Group** | get, getMany, create, update, delete |
63
+ | **Relationship** | get, getMany, create, update, delete |
64
+ | **Activity** | get, getMany, create, update, delete |
65
+ | **Custom API Call** | full custom API4 request |
64
66
 
65
67
  ---
66
68
 
67
- ## Features
69
+ ## 🧩 Key Features
70
+
71
+ ### **1. Dynamic Field Mapping**
72
+ Supports any standard or custom field:
73
+
74
+ ```
75
+ first_name = John
76
+ last_name = Doe
77
+ custom_45 = Blue
78
+ ```
68
79
 
69
- ### Email, phone and address with location types
70
- Two mapping modes:
80
+ ### **2. Smart Email, Phone & Address Mapping**
81
+ Two ways to set location-aware fields:
71
82
 
72
- Simple:
83
+ **(A) Simple fields**
73
84
  ```
74
85
  email = test@example.org
86
+ phone.mobile = 600123456
75
87
  address.city = Barcelona
76
88
  ```
77
89
 
78
- With location prefixes:
90
+ **(B) Dynamic prefixes matched to CiviCRM Location Types**
79
91
  ```
80
92
  work.email = user@company.org
81
- billing.address.postal_code = 08014
93
+ billing.address.postal_code = 80331
82
94
  home.phone.phone_type_id = 2
83
95
  ```
84
96
 
85
- ### GET MANY with JSON filters
97
+ ### **3. Default Location Type selectors**
98
+ If no prefix is used, default types are applied.
99
+
100
+ ### **4. Birth Date Normalization**
101
+ Accepted input formats:
102
+
103
+ - YYYY-MM-DD
104
+ - DD/MM/YYYY
105
+ - DD-MM-YYYY
106
+ - YYYY/MM/DD
107
+ - YYYY.MM.DD
108
+
109
+ Auto-normalized to `YYYY-MM-DD`.
110
+
111
+ ### **5. GET MANY with JSON Filters**
86
112
  Example:
87
113
  ```json
88
114
  [
@@ -92,17 +118,7 @@ Example:
92
118
  ]
93
119
  ```
94
120
 
95
- ### Birth date normalization
96
- Accepted input formats:
97
- - YYYY-MM-DD
98
- - DD/MM/YYYY
99
- - DD-MM-YYYY
100
- - YYYY/MM/DD
101
- - YYYY.MM.DD
102
-
103
- Normalized to `YYYY-MM-DD`.
104
-
105
- ### Custom API Call
121
+ ### **6. Custom API Call Mode**
106
122
  Example:
107
123
  ```json
108
124
  {
@@ -126,14 +142,14 @@ Example:
126
142
 
127
143
  Clone the repository and run:
128
144
 
129
- ```
145
+ ```bash
130
146
  npm install
131
147
  npm run dev
132
148
  ```
133
149
 
134
150
  Build:
135
151
 
136
- ```
152
+ ```bash
137
153
  npm run build
138
154
  ```
139
155
 
@@ -145,12 +161,13 @@ Pull requests and issues are welcome in the GitHub repository.
145
161
 
146
162
  ---
147
163
 
148
- ## About Ixiam Global Solutions
149
- Website: https://www.ixiam.com
150
- Email: info@ixiam.com
164
+ ## 🧑‍💻 About Ixiam Global Solutions
165
+
166
+ Website: **https://www.ixiam.com**
167
+ Contact: **info@ixiam.com**
151
168
 
152
169
  ---
153
170
 
154
- ## License
171
+ ## 📄 License
155
172
 
156
173
  MIT License
@@ -23,7 +23,6 @@ class CiviCrmApi {
23
23
  required: true,
24
24
  },
25
25
  ];
26
- // ← ESTA ES LA PARTE CRÍTICA
27
26
  this.authenticate = {
28
27
  type: 'generic',
29
28
  properties: {
@@ -34,13 +33,8 @@ class CiviCrmApi {
34
33
  };
35
34
  this.test = {
36
35
  request: {
37
- baseURL: '={{ $credentials.baseUrl.replace(/\\/$/, "") }}',
38
- url: '/civicrm/ajax/api4/Contact/get',
36
+ url: '={{ $credentials.baseUrl }}/civicrm/ajax/api4/Contact/get',
39
37
  method: 'POST',
40
- headers: {
41
- 'X-Civi-Auth': '={{ "Bearer " + $credentials.apiToken }}',
42
- 'Content-Type': 'application/json',
43
- },
44
38
  body: {
45
39
  limit: 1,
46
40
  select: ['id']
@@ -242,7 +242,7 @@ class CiviCrm {
242
242
  try {
243
243
  where = JSON.parse(whereJson);
244
244
  }
245
- catch {
245
+ catch (error) {
246
246
  throw new Error('Invalid JSON in whereJson');
247
247
  }
248
248
  }
@@ -277,14 +277,18 @@ class CiviCrm {
277
277
  if (returnAll) {
278
278
  let offset = 0;
279
279
  const page = 500;
280
- while (true) {
280
+ let hasMore = true;
281
+ while (hasMore) {
281
282
  const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, { ...params, limit: page, offset });
282
283
  const vals = (_c = r === null || r === void 0 ? void 0 : r.values) !== null && _c !== void 0 ? _c : [];
283
284
  for (const v of vals)
284
285
  out.push({ json: v });
285
- if (vals.length < page)
286
- break;
287
- offset += page;
286
+ if (vals.length < page) {
287
+ hasMore = false;
288
+ }
289
+ else {
290
+ offset += page;
291
+ }
288
292
  }
289
293
  }
290
294
  else {
@@ -323,31 +327,6 @@ class CiviCrm {
323
327
  const emailData = {};
324
328
  const phoneData = {};
325
329
  const addressData = {};
326
- function normalizeBirthDate(input) {
327
- if (!input)
328
- return input;
329
- let v = input.trim();
330
- if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
331
- }
332
- else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
333
- const [d, m, y] = v.split('/');
334
- v = `${y}-${m}-${d}`;
335
- }
336
- else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
337
- const [d, m, y] = v.split('-');
338
- v = `${y}-${m}-${d}`;
339
- }
340
- else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
341
- v = v.replace(/\//g, '-');
342
- }
343
- else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
344
- v = v.replace(/\./g, '-');
345
- }
346
- const dt = new Date(v);
347
- if (isNaN(dt.getTime()))
348
- throw new Error(`Invalid birth_date: ${input}`);
349
- return v;
350
- }
351
330
  let locationTypeMap = {};
352
331
  if (resource === 'contact') {
353
332
  locationTypeMap = await getLocationTypeMap.call(this);
@@ -551,7 +530,35 @@ function convertValue(val) {
551
530
  if (typeof j === 'object')
552
531
  return j;
553
532
  }
554
- catch { }
533
+ catch (error) {
534
+ // Not valid JSON, return original value
535
+ }
555
536
  return val;
556
537
  }
538
+ function normalizeBirthDate(input) {
539
+ if (!input)
540
+ return input;
541
+ let v = input.trim();
542
+ if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
543
+ // Already in correct format
544
+ }
545
+ else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
546
+ const [d, m, y] = v.split('/');
547
+ v = `${y}-${m}-${d}`;
548
+ }
549
+ else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
550
+ const [d, m, y] = v.split('-');
551
+ v = `${y}-${m}-${d}`;
552
+ }
553
+ else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
554
+ v = v.replace(/\//g, '-');
555
+ }
556
+ else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
557
+ v = v.replace(/\./g, '-');
558
+ }
559
+ const dt = new Date(v);
560
+ if (isNaN(dt.getTime()))
561
+ throw new Error(`Invalid birth_date: ${input}`);
562
+ return v;
563
+ }
557
564
  exports.default = CiviCrm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixiam/n8n-nodes-civicrm",
3
- "version": "1.1.18",
3
+ "version": "1.1.23",
4
4
  "description": "Full-featured CiviCRM API v4 integration for n8n with support for Contacts, Memberships, Groups, Relationships, Activities and structured sub-entities like Email, Phone, and Address.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -60,4 +60,4 @@
60
60
  }
61
61
  },
62
62
  "packageManager": "pnpm@10.18.2+sha512.9fb969fa749b3ade6035e0f109f0b8a60b5d08a1a87fdf72e337da90dcc93336e2280ca4e44f2358a649b83c17959e9993e777c2080879f3801e6f0d999ad3dd"
63
- }
63
+ }
@@ -0,0 +1,68 @@
1
+ # Reload Node in Docker n8n
2
+
3
+ If you're loading this node from a local folder in Docker and seeing cached/old versions, try these steps:
4
+
5
+ ## Option 1: Restart Docker Container (Quick)
6
+ ```bash
7
+ # Restart your n8n container
8
+ docker restart <container-name>
9
+ # or if using docker-compose
10
+ docker-compose restart
11
+ ```
12
+
13
+ ## Option 2: Clear n8n Cache (Better)
14
+ ```bash
15
+ # Stop the container
16
+ docker stop <container-name>
17
+
18
+ # Remove the container (keeps volumes)
19
+ docker rm <container-name>
20
+
21
+ # Start fresh
22
+ docker-compose up -d
23
+ # or your docker run command
24
+ ```
25
+
26
+ ## Option 3: Verify Volume Mount
27
+ If you're using a volume mount like `-v /path/to/n8n-nodes-civicrm:/data/custom-nodes/n8n-nodes-civicrm`:
28
+
29
+ 1. Make sure the path points to THIS directory
30
+ 2. Verify the dist folder is being mounted:
31
+ ```bash
32
+ docker exec <container-name> ls -la /data/custom-nodes/n8n-nodes-civicrm/dist/credentials/
33
+ docker exec <container-name> cat /data/custom-nodes/n8n-nodes-civicrm/dist/credentials/CiviCrmApi.credentials.js | grep baseURL
34
+ ```
35
+
36
+ ## Option 4: Check n8n Custom Nodes Path
37
+ If using `N8N_CUSTOM_EXTENSION_ENV` environment variable:
38
+
39
+ ```bash
40
+ # Check what path n8n is using
41
+ docker exec <container-name> env | grep N8N_CUSTOM
42
+
43
+ # Verify the file exists in that path
44
+ docker exec <container-name> ls -la <custom-path>/dist/credentials/
45
+ ```
46
+
47
+ ## Option 5: Force Rebuild and Reload
48
+ ```bash
49
+ # In this directory, clean and rebuild
50
+ npm run build
51
+
52
+ # If using npm link
53
+ npm link
54
+
55
+ # Then restart n8n container
56
+ docker restart <container-name>
57
+ ```
58
+
59
+ ## Verify the Fix
60
+ After restarting, check if the error is gone. The built file should have:
61
+ ```javascript
62
+ baseURL: '={{ $credentials.baseUrl }}',
63
+ ```
64
+
65
+ NOT:
66
+ ```javascript
67
+ baseURL: '={{ $credentials.baseUrl.replace(/\\/$/, "") }}',
68
+ ```
@@ -28,7 +28,6 @@ export class CiviCrmApi implements ICredentialType {
28
28
  },
29
29
  ];
30
30
 
31
- // ← ESTA ES LA PARTE CRÍTICA
32
31
  authenticate = {
33
32
  type: 'generic' as const,
34
33
  properties: {
@@ -40,17 +39,12 @@ export class CiviCrmApi implements ICredentialType {
40
39
 
41
40
  test: ICredentialTestRequest = {
42
41
  request: {
43
- baseURL: '={{ $credentials.baseUrl.replace(/\\/$/, "") }}',
44
- url: '/civicrm/ajax/api4/Contact/get',
42
+ url: '={{ $credentials.baseUrl }}/civicrm/ajax/api4/Contact/get',
45
43
  method: 'POST',
46
- headers: {
47
- 'X-Civi-Auth': '={{ "Bearer " + $credentials.apiToken }}',
48
- 'Content-Type': 'application/json',
49
- },
50
- body: {
44
+ body: {
51
45
  limit: 1,
52
46
  select: ['id']
53
47
  },
54
48
  },
55
49
  };
56
- }
50
+ }
@@ -251,29 +251,29 @@ export class CiviCrm implements INodeType {
251
251
  const params =
252
252
  resource === 'contact'
253
253
  ? {
254
- where: [['id', '=', id]],
255
- limit: 1,
256
- select: [
257
- 'id',
258
- 'display_name',
259
- 'first_name',
260
- 'last_name',
261
- 'contact_type',
262
- 'gender_id',
263
- 'gender_id:name',
264
- 'birth_date',
265
- ],
266
- chain: {
267
- emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
268
- phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
269
- addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
270
- },
271
- }
254
+ where: [['id', '=', id]],
255
+ limit: 1,
256
+ select: [
257
+ 'id',
258
+ 'display_name',
259
+ 'first_name',
260
+ 'last_name',
261
+ 'contact_type',
262
+ 'gender_id',
263
+ 'gender_id:name',
264
+ 'birth_date',
265
+ ],
266
+ chain: {
267
+ emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
268
+ phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
269
+ addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
270
+ },
271
+ }
272
272
  : {
273
- where: [['id', '=', id]],
274
- limit: 1,
275
- select: ['id', 'name', 'title', 'subject', 'display_name'],
276
- };
273
+ where: [['id', '=', id]],
274
+ limit: 1,
275
+ select: ['id', 'name', 'title', 'subject', 'display_name'],
276
+ };
277
277
 
278
278
  const res = await civicrmApiRequest.call(
279
279
  this,
@@ -298,7 +298,7 @@ export class CiviCrm implements INodeType {
298
298
  if (whereJson) {
299
299
  try {
300
300
  where = JSON.parse(whereJson);
301
- } catch {
301
+ } catch (error) {
302
302
  throw new Error('Invalid JSON in whereJson');
303
303
  }
304
304
  }
@@ -311,33 +311,34 @@ export class CiviCrm implements INodeType {
311
311
  const params =
312
312
  resource === 'contact'
313
313
  ? {
314
- where,
315
- select: [
316
- 'id',
317
- 'display_name',
318
- 'first_name',
319
- 'last_name',
320
- 'contact_type',
321
- 'gender_id',
322
- 'gender_id:name',
323
- 'birth_date',
324
- ],
325
- chain: {
326
- emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
327
- phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
328
- addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
329
- },
330
- }
314
+ where,
315
+ select: [
316
+ 'id',
317
+ 'display_name',
318
+ 'first_name',
319
+ 'last_name',
320
+ 'contact_type',
321
+ 'gender_id',
322
+ 'gender_id:name',
323
+ 'birth_date',
324
+ ],
325
+ chain: {
326
+ emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
327
+ phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
328
+ addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
329
+ },
330
+ }
331
331
  : {
332
- where,
333
- select: ['id', 'name', 'title', 'subject', 'display_name'],
334
- };
332
+ where,
333
+ select: ['id', 'name', 'title', 'subject', 'display_name'],
334
+ };
335
335
 
336
336
  if (returnAll) {
337
337
  let offset = 0;
338
338
  const page = 500;
339
+ let hasMore = true;
339
340
 
340
- while (true) {
341
+ while (hasMore) {
341
342
  const r = await civicrmApiRequest.call(
342
343
  this,
343
344
  'POST',
@@ -348,8 +349,11 @@ export class CiviCrm implements INodeType {
348
349
  const vals = r?.values ?? [];
349
350
  for (const v of vals) out.push({ json: v });
350
351
 
351
- if (vals.length < page) break;
352
- offset += page;
352
+ if (vals.length < page) {
353
+ hasMore = false;
354
+ } else {
355
+ offset += page;
356
+ }
353
357
  }
354
358
  } else {
355
359
  const r = await civicrmApiRequest.call(
@@ -410,28 +414,6 @@ export class CiviCrm implements INodeType {
410
414
  const phoneData: Record<string, unknown> = {};
411
415
  const addressData: Record<string, unknown> = {};
412
416
 
413
- function normalizeBirthDate(input: string): string {
414
- if (!input) return input;
415
- let v = input.trim();
416
-
417
- if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
418
- } else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
419
- const [d, m, y] = v.split('/');
420
- v = `${y}-${m}-${d}`;
421
- } else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
422
- const [d, m, y] = v.split('-');
423
- v = `${y}-${m}-${d}`;
424
- } else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
425
- v = v.replace(/\//g, '-');
426
- } else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
427
- v = v.replace(/\./g, '-');
428
- }
429
-
430
- const dt = new Date(v);
431
- if (isNaN(dt.getTime())) throw new Error(`Invalid birth_date: ${input}`);
432
- return v;
433
- }
434
-
435
417
  let locationTypeMap: Record<string, string> = {};
436
418
  if (resource === 'contact') {
437
419
  locationTypeMap = await getLocationTypeMap.call(this);
@@ -627,12 +609,12 @@ export class CiviCrm implements INodeType {
627
609
  ],
628
610
  ...(resource === 'contact'
629
611
  ? {
630
- chain: {
631
- emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
632
- phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
633
- addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
634
- },
635
- }
612
+ chain: {
613
+ emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
614
+ phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
615
+ addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
616
+ },
617
+ }
636
618
  : {}),
637
619
  },
638
620
  );
@@ -657,8 +639,33 @@ function convertValue(val: string): unknown {
657
639
  try {
658
640
  const j = JSON.parse(t);
659
641
  if (typeof j === 'object') return j;
660
- } catch {}
642
+ } catch (error) {
643
+ // Not valid JSON, return original value
644
+ }
661
645
  return val;
662
646
  }
663
647
 
648
+ function normalizeBirthDate(input: string): string {
649
+ if (!input) return input;
650
+ let v = input.trim();
651
+
652
+ if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
653
+ // Already in correct format
654
+ } else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
655
+ const [d, m, y] = v.split('/');
656
+ v = `${y}-${m}-${d}`;
657
+ } else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
658
+ const [d, m, y] = v.split('-');
659
+ v = `${y}-${m}-${d}`;
660
+ } else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
661
+ v = v.replace(/\//g, '-');
662
+ } else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
663
+ v = v.replace(/\./g, '-');
664
+ }
665
+
666
+ const dt = new Date(v);
667
+ if (isNaN(dt.getTime())) throw new Error(`Invalid birth_date: ${input}`);
668
+ return v;
669
+ }
670
+
664
671
  export default CiviCrm;