ti2-ventrata 1.0.36 → 1.0.38

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/index.js CHANGED
@@ -8,6 +8,7 @@ const { translateProduct } = require('./resolvers/product');
8
8
  const { translateAvailability } = require('./resolvers/availability');
9
9
  const { translateBooking } = require('./resolvers/booking');
10
10
  const { translatePickupPoint } = require('./resolvers/pickup-point');
11
+ const { validateCountry } = require('./utils/countrymapping');
11
12
 
12
13
  const CONCURRENCY = 3; // is this ok ?
13
14
 
@@ -71,6 +72,24 @@ class Plugin {
71
72
  this.errorPathsAxiosAny = () => ([]); // 200's that should be errors
72
73
  }
73
74
 
75
+ static getSettlementMethod(reference, availableSettlementMethods = []) {
76
+ // Determine settlement method from product's available methods
77
+ let settlementMethod = 'DEFERRED'; // default fallback
78
+
79
+ if (reference && availableSettlementMethods.includes('VOUCHER')) {
80
+ settlementMethod = 'VOUCHER';
81
+ } else if (reference && availableSettlementMethods.includes('DIRECT')) {
82
+ settlementMethod = 'DIRECT';
83
+ } else if (availableSettlementMethods.includes('DEFERRED')) {
84
+ settlementMethod = 'DEFERRED';
85
+ } else if (availableSettlementMethods.length > 0) {
86
+ // Use the first available settlement method if none of the preferred ones are available
87
+ [settlementMethod] = availableSettlementMethods;
88
+ }
89
+
90
+ return settlementMethod;
91
+ }
92
+
74
93
  async validateToken({
75
94
  axios,
76
95
  token: {
@@ -223,12 +242,22 @@ class Plugin {
223
242
  const url = `${endpoint || this.endpoint}/availability`;
224
243
  const availability = (
225
244
  await Promise.map(productIds, async (productId, ix) => {
245
+ // Fetch product data to get settlement methods
246
+ const productUrl = `${endpoint || this.endpoint}/products/${productId}`;
247
+ const productResult = await axios({
248
+ method: 'get',
249
+ url: productUrl,
250
+ headers,
251
+ });
252
+ const settlementMethods = R.path(['data', 'settlementMethods'], productResult) || [];
253
+ // eslint-disable-next-line max-len
254
+ const unitsWithId = units[ix].filter(u => u.unitId).map(u => ({ id: u.unitId, quantity: u.quantity }));
226
255
  const data = {
227
256
  productId,
228
257
  optionId: optionIds[ix],
229
258
  localDateStart,
230
259
  localDateEnd,
231
- units: units[ix].map(u => ({ id: u.unitId, quantity: u.quantity })),
260
+ ...(unitsWithId && unitsWithId.length > 0 && { units: unitsWithId }),
232
261
  };
233
262
  if (currency) data.currency = currency;
234
263
  const result = R.path(['data'], await axios({
@@ -247,6 +276,7 @@ class Plugin {
247
276
  currency,
248
277
  unitsWithQuantity: units[ix],
249
278
  jwtKey: this.jwtKey,
279
+ settlementMethods,
250
280
  },
251
281
  }));
252
282
  }, { concurrency: CONCURRENCY })
@@ -297,18 +327,28 @@ class Plugin {
297
327
  acceptLanguage,
298
328
  resellerId,
299
329
  });
300
- const url = `${endpoint || this.endpoint}/availability/calendar`;
301
330
  const availability = (
302
331
  await Promise.map(productIds, async (productId, ix) => {
332
+ // Fetch product data to get settlement methods
333
+ const productUrl = `${endpoint || this.endpoint}/products/${productId}`;
334
+ const productResult = await axios({
335
+ method: 'get',
336
+ url: productUrl,
337
+ headers,
338
+ });
339
+ const settlementMethods = R.path(['data', 'settlementMethods'], productResult) || [];
340
+ // eslint-disable-next-line max-len
341
+ const unitsWithId = units[ix].filter(u => u.unitId).map(u => ({ id: u.unitId, quantity: u.quantity }));
303
342
  const data = {
304
343
  productId,
305
344
  optionId: optionIds[ix],
306
345
  localDateStart,
307
346
  localDateEnd,
308
347
  // units is required here to get the total pricing for the calendar
309
- units: units[ix].map(u => ({ id: u.unitId, quantity: u.quantity })),
348
+ ...(unitsWithId && unitsWithId.length > 0 && { units: unitsWithId }),
310
349
  };
311
350
  if (currency) data.currency = currency;
351
+ const url = `${endpoint || this.endpoint}/availability/calendar`;
312
352
  const result = await axios({
313
353
  method: 'post',
314
354
  url,
@@ -319,6 +359,14 @@ class Plugin {
319
359
  rootValue: avail,
320
360
  typeDefs: availTypeDefs,
321
361
  query: availQuery,
362
+ variableValues: {
363
+ productId,
364
+ optionId: optionIds[ix],
365
+ currency,
366
+ unitsWithQuantity: units[ix],
367
+ jwtKey: this.jwtKey,
368
+ settlementMethods,
369
+ },
322
370
  }));
323
371
  }, { concurrency: CONCURRENCY })
324
372
  );
@@ -359,68 +407,100 @@ class Plugin {
359
407
  acceptLanguage,
360
408
  });
361
409
  const dataForCreateBooking = await jwt.verify(availabilityKey, this.jwtKey);
410
+ // if customFieldValues contains perUnitItem fields
411
+ // we need to overwrite unitItems from the availabilityKey
412
+ if (customFieldValues && customFieldValues.length) {
413
+ const productCFV = customFieldValues.filter(o => !R.isNil(o.value) && !o.field.isPerUnitItem);
414
+ const unitItemCFV = customFieldValues.filter(o => !R.isNil(o.value) && o.field.isPerUnitItem);
415
+ if (productCFV.length) {
416
+ dataForCreateBooking.questionAnswers = productCFV.map(o => ({
417
+ questionId: o.field.id,
418
+ value: o.value,
419
+ }));
420
+ }
421
+ if (unitItemCFV.length) {
422
+ dataForCreateBooking.unitItems = R.call(R.compose(
423
+ R.map(arr => ({
424
+ unitId: arr[0].field.unitId,
425
+ questionAnswers: arr.map(o => ({
426
+ questionId: o.field.id.split('|')[0],
427
+ value: o.value,
428
+ })),
429
+ })),
430
+ R.values,
431
+ R.groupBy(o => {
432
+ const [, unitItemIndex] = o.field.id.split('|');
433
+ return unitItemIndex;
434
+ }),
435
+ ), unitItemCFV);
436
+ }
437
+ }
438
+
439
+ const settlementMethod = Plugin.getSettlementMethod(
440
+ reference,
441
+ dataForCreateBooking.settlementMethods,
442
+ );
362
443
  let booking = R.path(['data'], await axios({
363
444
  method: rebookingId ? 'patch' : 'post',
364
445
  url: `${endpoint || this.endpoint}/bookings${rebookingId ? `/${rebookingId}` : ''}`,
365
446
  data: {
366
447
  orderId,
367
- settlementMethod: reference ? 'VOUCHER' : 'DEFERRED',
448
+ settlementMethod,
368
449
  ...dataForCreateBooking,
369
450
  notes,
370
451
  ...(pickupPoint ? { pickupRequested: true, pickupPointId: pickupPoint } : {}),
371
452
  },
372
453
  headers,
373
454
  }));
374
- const answers = customFieldValues && customFieldValues.length
375
- ? customFieldValues.filter(o => !R.isNil(o.value)).map(o => ({
376
- questionId: o.field.id,
377
- value: o.value,
378
- })) : [];
379
- // for answers to custom fields, ventrata suggest to modify the booking
380
- if (answers.length) {
381
- let modifierBooking = {
382
- questionAnswers: booking.questionAnswers.map(o => {
383
- const answer = answers.find(a => a.questionId === o.questionId);
384
- return answer || o;
385
- }),
386
- unitItems: booking.unitItems.map(u => ({
387
- ...u,
388
- questionAnswers: u.questionAnswers.map(o => {
389
- const answer = answers.find(a => a.questionId === o.questionId);
390
- return answer || o;
391
- }),
392
- })),
393
- };
394
- if (!modifierBooking.unitItems.find(u => u.questionAnswers && u.questionAnswers.length)) {
395
- modifierBooking = R.omit(['unitItems'], modifierBooking);
396
- }
397
- booking = R.path(['data'], await axios({
398
- method: 'patch',
399
- url: `${endpoint || this.endpoint}/bookings/${booking.uuid}`,
400
- data: modifierBooking,
401
- headers,
402
- }));
403
- }
404
455
  // for booking update, we may not need to confirm again
405
456
  if (!booking.utcConfirmedAt && !partialOrder) {
457
+ const validatedCountry = validateCountry(R.pathOr('', ['country'], holder));
458
+
459
+ // Ensure required contact fields are present
460
+ const firstName = R.pathOr('', ['name'], holder);
461
+ const lastName = R.pathOr('', ['surname'], holder);
462
+ // Validate required fields
463
+ if (!firstName.trim() || !lastName.trim()) {
464
+ console.error('Customer validation failed: First and last name are required');
465
+ throw new Error('Customer validation failed: First and last name are required');
466
+ }
467
+
468
+ const fullName = `${firstName.trim()} ${lastName.trim()}`;
469
+ const emailAddress = R.pathOr(null, ['emailAddress'], holder);
470
+ const phoneNumber = R.pathOr('', ['phone'], holder);
471
+
472
+ // Basic email validation
473
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
474
+ if (emailAddress && !emailRegex.test(emailAddress)) {
475
+ console.error(`Customer validation failed: Invalid email format: ${emailAddress}`);
476
+ throw new Error('Customer validation failed: Invalid email address format');
477
+ }
406
478
  const dataForConfirmBooking = {
407
479
  contact: {
408
- fullName: `${holder.name} ${holder.surname}`,
409
- emailAddress: R.path(['emailAddress'], holder),
410
- phoneNumber: R.path(['phone'], holder),
411
- locales: R.path(['locales'], holder),
412
- country: R.path(['country'], holder),
480
+ fullName,
481
+ emailAddress,
482
+ phoneNumber: phoneNumber || '',
483
+ locales: R.path(['locales'], holder) || ['en'],
484
+ country: validatedCountry,
485
+ postalCode: R.path(['zipCode'], holder) || '',
413
486
  },
414
487
  notes,
415
488
  resellerReference: reference,
416
- settlementMethod: reference ? 'VOUCHER' : 'DEFERRED',
489
+ settlementMethod,
490
+ ...(emailAddress ? { emailReceipt: true } : {}), // Send email receipt since we have a valid email address
417
491
  };
418
- booking = R.path(['data'], await axios({
419
- method: 'post',
420
- url: `${endpoint || this.endpoint}/bookings/${booking.uuid}/confirm`,
421
- data: dataForConfirmBooking,
422
- headers,
423
- }));
492
+
493
+ try {
494
+ booking = R.path(['data'], await axios({
495
+ method: 'post',
496
+ url: `${endpoint || this.endpoint}/bookings/${booking.uuid}/confirm`,
497
+ data: dataForConfirmBooking,
498
+ headers,
499
+ }));
500
+ } catch (error) {
501
+ console.error('Booking confirmation failed:', (error.response && error.response.data) ? error.response.data : error.message);
502
+ throw error;
503
+ }
424
504
  }
425
505
  return ({
426
506
  booking: await translateBooking({
@@ -623,8 +703,6 @@ class Plugin {
623
703
  },
624
704
  query: {
625
705
  productId,
626
- date,
627
- dateFormat,
628
706
  },
629
707
  }) {
630
708
  const headers = getHeaders({
@@ -647,6 +725,7 @@ class Plugin {
647
725
  return 'long';
648
726
  })(),
649
727
  options: o.selectOptions,
728
+ isPerUnitItem: o.isPerUnitItem,
650
729
  });
651
730
  if (!productId) {
652
731
  const allProducts = R.pathOr([], ['data'], await axios({
@@ -655,13 +734,15 @@ class Plugin {
655
734
  headers,
656
735
  }));
657
736
  const allOptions = R.chain(R.propOr([], 'options'), allProducts);
658
- const allUnits = R.chain(R.propOr([], 'units'), allOptions);
737
+ const allUnits = R.chain(R.propOr([], 'units'), allOptions).map(o => ({ ...o, isPerUnitItem: true }));
659
738
  const allQuestions = R.call(R.compose(
660
739
  R.uniqBy(R.prop('id')),
661
- R.chain(R.propOr([], 'questions')),
740
+ R.chain(obj => (obj.questions || []).map(q => ({
741
+ ...q,
742
+ isPerUnitItem: Boolean(obj.isPerUnitItem),
743
+ }))),
662
744
  R.filter(o => o.questions && o.questions.length),
663
745
  ), [...allOptions, ...allUnits]);
664
- console.log(allQuestions);
665
746
  return {
666
747
  fields: [],
667
748
  customFields: allQuestions.map(convertQToField),
@@ -677,11 +758,14 @@ class Plugin {
677
758
  const allOptions = R.propOr([], 'options', product);
678
759
  const allQuestions = R.call(R.compose(
679
760
  R.uniqBy(R.prop('id')),
680
- R.chain(R.propOr([], 'questions')),
761
+ R.chain(obj => (obj.questions || []).map(q => ({
762
+ ...q,
763
+ isPerUnitItem: Boolean(obj.isPerUnitItem),
764
+ }))),
681
765
  R.filter(o => o.questions && o.questions.length),
682
766
  ), [
683
767
  ...allOptions,
684
- ...R.chain(R.propOr([], 'units'), allOptions),
768
+ ...R.chain(R.propOr([], 'units'), allOptions).map(o => ({ ...o, isPerUnitItem: true })),
685
769
  ]);
686
770
  if (!product) return { fields: [] };
687
771
  return {
package/index.test.js CHANGED
@@ -33,7 +33,7 @@ const app = new Plugin({
33
33
  describe('search tests', () => {
34
34
  let products;
35
35
  let testProduct = {
36
- productName: 'Edinburgh Pub Crawl Tour',
36
+ productName: '[ED3D] Edinburgh 3 Day Pass',
37
37
  };
38
38
  const token = {
39
39
  apiKey: process.env.ti2_ventrata_apiKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-ventrata",
3
- "version": "1.0.36",
3
+ "version": "1.0.38",
4
4
  "description": "Ventrata's TI2 Plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12,6 +12,7 @@ const resolvers = {
12
12
  currency,
13
13
  unitsWithQuantity,
14
14
  jwtKey,
15
+ settlementMethods,
15
16
  } = args;
16
17
  if (!jwtKey) return null;
17
18
  return jwt.sign({
@@ -19,6 +20,7 @@ const resolvers = {
19
20
  optionId,
20
21
  availabilityId: root.id,
21
22
  currency,
23
+ settlementMethods,
22
24
  unitItems: R.chain(u => new Array(u.quantity).fill(1).map(() => ({
23
25
  unitId: u.unitId,
24
26
  })), unitsWithQuantity),
@@ -35,6 +35,9 @@ const resolvers = {
35
35
  fullName: R.pathOr('', ['contact', 'fullName'], root),
36
36
  phoneNumber: R.pathOr('', ['contact', 'phoneNumber'], root),
37
37
  emailAddress: R.pathOr('', ['contact', 'emailAddress'], root),
38
+ country: R.pathOr('', ['contact', 'country'], root),
39
+ locales: R.pathOr('', ['locales'], root),
40
+ postalCode: R.pathOr('', ['contact', 'postalCode'], root),
38
41
  }),
39
42
  notes: root => root.notes || '',
40
43
  price: root => root.pricing,
@@ -9,6 +9,7 @@ const resolvers = {
9
9
  availableCurrencies: R.path(['availableCurrencies']),
10
10
  defaultCurrency: R.path(['defaultCurrency']),
11
11
  options: R.propOr([], 'options'),
12
+ settlementMethods: R.path(['settlementMethods']),
12
13
  },
13
14
  Option: {
14
15
  optionId: R.prop('id'),
@@ -0,0 +1,206 @@
1
+ // Country name to ISO 3166-1 alpha-2 code mapping
2
+ const countryNameToCode = {
3
+ // Major countries
4
+ 'UNITED STATES': 'US',
5
+ 'USA': 'US',
6
+ 'AMERICA': 'US',
7
+ 'UNITED STATES OF AMERICA': 'US',
8
+ 'UNITED KINGDOM': 'GB',
9
+ 'UK': 'GB',
10
+ 'GREAT BRITAIN': 'GB',
11
+ 'ENGLAND': 'GB',
12
+ 'BRITAIN': 'GB',
13
+ 'CANADA': 'CA',
14
+ 'FRANCE': 'FR',
15
+ 'GERMANY': 'DE',
16
+ 'ITALY': 'IT',
17
+ 'SPAIN': 'ES',
18
+ 'AUSTRALIA': 'AU',
19
+ 'NEW ZEALAND': 'NZ',
20
+ 'JAPAN': 'JP',
21
+ 'CHINA': 'CN',
22
+ 'INDIA': 'IN',
23
+ 'BRAZIL': 'BR',
24
+ 'MEXICO': 'MX',
25
+ 'RUSSIA': 'RU',
26
+ 'SOUTH AFRICA': 'ZA',
27
+ 'ARGENTINA': 'AR',
28
+ 'CHILE': 'CL',
29
+ 'COLOMBIA': 'CO',
30
+ 'PERU': 'PE',
31
+ 'VENEZUELA': 'VE',
32
+ 'ECUADOR': 'EC',
33
+ 'BOLIVIA': 'BO',
34
+ 'PARAGUAY': 'PY',
35
+ 'URUGUAY': 'UY',
36
+ 'GUYANA': 'GY',
37
+ 'SURINAME': 'SR',
38
+ 'NETHERLANDS': 'NL',
39
+ 'BELGIUM': 'BE',
40
+ 'SWITZERLAND': 'CH',
41
+ 'AUSTRIA': 'AT',
42
+ 'PORTUGAL': 'PT',
43
+ 'SWEDEN': 'SE',
44
+ 'NORWAY': 'NO',
45
+ 'DENMARK': 'DK',
46
+ 'FINLAND': 'FI',
47
+ 'ICELAND': 'IS',
48
+ 'IRELAND': 'IE',
49
+ 'POLAND': 'PL',
50
+ 'CZECH REPUBLIC': 'CZ',
51
+ 'SLOVAKIA': 'SK',
52
+ 'HUNGARY': 'HU',
53
+ 'ROMANIA': 'RO',
54
+ 'BULGARIA': 'BG',
55
+ 'CROATIA': 'HR',
56
+ 'SLOVENIA': 'SI',
57
+ 'SERBIA': 'RS',
58
+ 'BOSNIA AND HERZEGOVINA': 'BA',
59
+ 'MONTENEGRO': 'ME',
60
+ 'ALBANIA': 'AL',
61
+ 'MACEDONIA': 'MK',
62
+ 'GREECE': 'GR',
63
+ 'TURKEY': 'TR',
64
+ 'CYPRUS': 'CY',
65
+ 'MALTA': 'MT',
66
+ 'LUXEMBOURG': 'LU',
67
+ 'ESTONIA': 'EE',
68
+ 'LATVIA': 'LV',
69
+ 'LITHUANIA': 'LT',
70
+ 'UKRAINE': 'UA',
71
+ 'BELARUS': 'BY',
72
+ 'MOLDOVA': 'MD',
73
+ 'GEORGIA': 'GE',
74
+ 'ARMENIA': 'AM',
75
+ 'AZERBAIJAN': 'AZ',
76
+ 'KAZAKHSTAN': 'KZ',
77
+ 'UZBEKISTAN': 'UZ',
78
+ 'TURKMENISTAN': 'TM',
79
+ 'KYRGYZSTAN': 'KG',
80
+ 'TAJIKISTAN': 'TJ',
81
+ 'AFGHANISTAN': 'AF',
82
+ 'PAKISTAN': 'PK',
83
+ 'BANGLADESH': 'BD',
84
+ 'SRI LANKA': 'LK',
85
+ 'NEPAL': 'NP',
86
+ 'BHUTAN': 'BT',
87
+ 'MALDIVES': 'MV',
88
+ 'MYANMAR': 'MM',
89
+ 'THAILAND': 'TH',
90
+ 'LAOS': 'LA',
91
+ 'VIETNAM': 'VN',
92
+ 'CAMBODIA': 'KH',
93
+ 'MALAYSIA': 'MY',
94
+ 'SINGAPORE': 'SG',
95
+ 'INDONESIA': 'ID',
96
+ 'PHILIPPINES': 'PH',
97
+ 'BRUNEI': 'BN',
98
+ 'TIMOR-LESTE': 'TL',
99
+ 'SOUTH KOREA': 'KR',
100
+ 'NORTH KOREA': 'KP',
101
+ 'MONGOLIA': 'MN',
102
+ 'TAIWAN': 'TW',
103
+ 'HONG KONG': 'HK',
104
+ 'MACAU': 'MO',
105
+ 'ISRAEL': 'IL',
106
+ 'PALESTINE': 'PS',
107
+ 'JORDAN': 'JO',
108
+ 'LEBANON': 'LB',
109
+ 'SYRIA': 'SY',
110
+ 'IRAQ': 'IQ',
111
+ 'IRAN': 'IR',
112
+ 'KUWAIT': 'KW',
113
+ 'SAUDI ARABIA': 'SA',
114
+ 'BAHRAIN': 'BH',
115
+ 'QATAR': 'QA',
116
+ 'UAE': 'AE',
117
+ 'UNITED ARAB EMIRATES': 'AE',
118
+ 'OMAN': 'OM',
119
+ 'YEMEN': 'YE',
120
+ 'EGYPT': 'EG',
121
+ 'LIBYA': 'LY',
122
+ 'TUNISIA': 'TN',
123
+ 'ALGERIA': 'DZ',
124
+ 'MOROCCO': 'MA',
125
+ 'SUDAN': 'SD',
126
+ 'SOUTH SUDAN': 'SS',
127
+ 'ETHIOPIA': 'ET',
128
+ 'ERITREA': 'ER',
129
+ 'DJIBOUTI': 'DJ',
130
+ 'SOMALIA': 'SO',
131
+ 'KENYA': 'KE',
132
+ 'UGANDA': 'UG',
133
+ 'TANZANIA': 'TZ',
134
+ 'RWANDA': 'RW',
135
+ 'BURUNDI': 'BI',
136
+ 'DEMOCRATIC REPUBLIC OF THE CONGO': 'CD',
137
+ 'CONGO': 'CG',
138
+ 'CENTRAL AFRICAN REPUBLIC': 'CF',
139
+ 'CHAD': 'TD',
140
+ 'CAMEROON': 'CM',
141
+ 'EQUATORIAL GUINEA': 'GQ',
142
+ 'GABON': 'GA',
143
+ 'SAO TOME AND PRINCIPE': 'ST',
144
+ 'NIGERIA': 'NG',
145
+ 'NIGER': 'NE',
146
+ 'MALI': 'ML',
147
+ 'BURKINA FASO': 'BF',
148
+ 'SENEGAL': 'SN',
149
+ 'GAMBIA': 'GM',
150
+ 'GUINEA-BISSAU': 'GW',
151
+ 'GUINEA': 'GN',
152
+ 'SIERRA LEONE': 'SL',
153
+ 'LIBERIA': 'LR',
154
+ 'IVORY COAST': 'CI',
155
+ 'GHANA': 'GH',
156
+ 'TOGO': 'TG',
157
+ 'BENIN': 'BJ',
158
+ 'MAURITANIA': 'MR',
159
+ 'WESTERN SAHARA': 'EH',
160
+ 'CAPE VERDE': 'CV',
161
+ 'MADAGASCAR': 'MG',
162
+ 'MAURITIUS': 'MU',
163
+ 'SEYCHELLES': 'SC',
164
+ 'COMOROS': 'KM',
165
+ 'MAYOTTE': 'YT',
166
+ 'REUNION': 'RE',
167
+ 'BOTSWANA': 'BW',
168
+ 'NAMIBIA': 'NA',
169
+ 'ZAMBIA': 'ZM',
170
+ 'ZIMBABWE': 'ZW',
171
+ 'MALAWI': 'MW',
172
+ 'MOZAMBIQUE': 'MZ',
173
+ 'SWAZILAND': 'SZ',
174
+ 'LESOTHO': 'LS',
175
+ 'ANGOLA': 'AO',
176
+ };
177
+
178
+ const validateCountry = countryInput => {
179
+ // Validate and format country code - ensure it's a valid ISO 3166-1 alpha-2 code
180
+ let validatedCountry = 'US'; // Default fallback
181
+
182
+ if (countryInput && typeof countryInput === 'string') {
183
+ const cleanInput = countryInput.trim().toUpperCase();
184
+
185
+ // First, check if it's already a valid 2-letter ISO code
186
+ if (cleanInput.length === 2 && /^[A-Z]{2}$/.test(cleanInput)) {
187
+ validatedCountry = cleanInput;
188
+ } else if (countryNameToCode[cleanInput]) {
189
+ // Then, try to convert country name to ISO code
190
+ validatedCountry = countryNameToCode[cleanInput];
191
+ } else {
192
+ // If it looks like a partial match, try to find it
193
+ const matchedEntry = Object.entries(countryNameToCode).find(([name]) =>
194
+ name.includes(cleanInput) || cleanInput.includes(name));
195
+ if (matchedEntry) {
196
+ const [, countryCode] = matchedEntry;
197
+ validatedCountry = countryCode;
198
+ }
199
+ }
200
+ }
201
+ return validatedCountry;
202
+ };
203
+
204
+ module.exports = {
205
+ validateCountry,
206
+ };