ti2-ventrata 1.0.37 → 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
  );
@@ -381,18 +429,23 @@ class Plugin {
381
429
  })),
382
430
  R.values,
383
431
  R.groupBy(o => {
384
- const [questionId, unitItemIndex] = o.field.id.split('|');
432
+ const [, unitItemIndex] = o.field.id.split('|');
385
433
  return unitItemIndex;
386
434
  }),
387
435
  ), unitItemCFV);
388
436
  }
389
437
  }
438
+
439
+ const settlementMethod = Plugin.getSettlementMethod(
440
+ reference,
441
+ dataForCreateBooking.settlementMethods,
442
+ );
390
443
  let booking = R.path(['data'], await axios({
391
444
  method: rebookingId ? 'patch' : 'post',
392
445
  url: `${endpoint || this.endpoint}/bookings${rebookingId ? `/${rebookingId}` : ''}`,
393
446
  data: {
394
447
  orderId,
395
- settlementMethod: reference ? 'VOUCHER' : 'DEFERRED',
448
+ settlementMethod,
396
449
  ...dataForCreateBooking,
397
450
  notes,
398
451
  ...(pickupPoint ? { pickupRequested: true, pickupPointId: pickupPoint } : {}),
@@ -401,24 +454,53 @@ class Plugin {
401
454
  }));
402
455
  // for booking update, we may not need to confirm again
403
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
+ }
404
478
  const dataForConfirmBooking = {
405
479
  contact: {
406
- fullName: `${holder.name} ${holder.surname}`,
407
- emailAddress: R.path(['emailAddress'], holder),
408
- phoneNumber: R.path(['phone'], holder),
409
- locales: R.path(['locales'], holder),
410
- 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) || '',
411
486
  },
412
487
  notes,
413
488
  resellerReference: reference,
414
- settlementMethod: reference ? 'VOUCHER' : 'DEFERRED',
489
+ settlementMethod,
490
+ ...(emailAddress ? { emailReceipt: true } : {}), // Send email receipt since we have a valid email address
415
491
  };
416
- booking = R.path(['data'], await axios({
417
- method: 'post',
418
- url: `${endpoint || this.endpoint}/bookings/${booking.uuid}/confirm`,
419
- data: dataForConfirmBooking,
420
- headers,
421
- }));
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
+ }
422
504
  }
423
505
  return ({
424
506
  booking: await translateBooking({
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.37",
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
+ };