ti2-tourplan 1.0.41 → 1.0.43

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.
Files changed (2) hide show
  1. package/index.js +74 -63
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -142,12 +142,14 @@ class Plugin {
142
142
  if (!replyObj) {
143
143
  // in case of error /xmlproxy, fallback to call tourplan directly
144
144
  // and then use pyfilematch xml2json to parse the xml
145
- const reply = R.path(['data'], await axios({
145
+ const axiospayload = {
146
146
  method: 'post',
147
147
  url: endpoint,
148
148
  data,
149
149
  headers: getHeaders({ length: data.length }),
150
- }));
150
+ };
151
+ // console.log(axiospayload)
152
+ const reply = R.path(['data'], await axios(axiospayload));
151
153
  if (this.xmlProxyUrl) {
152
154
  try {
153
155
  // using raw axios to avoid logging the large xml request
@@ -170,13 +172,14 @@ class Plugin {
170
172
  }
171
173
  const requestType = R.keys(model)[0];
172
174
  if (!replyObj) throw new Error(`${requestType} failed: ${errorStr || 'no reply object'}`);
173
- const error = replyObj.error || R.path(['Reply', 'ErrorReply', 'Error'], replyObj);
175
+ let error = replyObj.error || R.path(['Reply', 'ErrorReply', 'Error'], replyObj);
174
176
  if (error) {
175
- if (error.indexOf('2050 SCN Request denied for TEST connecting from') > -1
176
- && requestType === 'OptionInfoRequest'
177
- && endpoint.indexOf('actour') > -1
178
- ) {
179
- return 'useFixture';
177
+ if (error.includes('DateFrom in the past')) {
178
+ error = '1002 - Date is in the past';
179
+ } else if (error.includes('1052 SCN')) {
180
+ error = '1052 - OptionId not found(Check if it is Internet Enabled)';
181
+ } else if (error.includes('SCN Server overloaded')) {
182
+ error = "2051 - The Tourplan server is unavailable. Please wait a minute and try again. If you keep getting this error, please contact your team's Tourplan administrator or Tourplan support."
180
183
  }
181
184
  throw new Error(`${requestType} failed: ${error}`);
182
185
  }
@@ -212,15 +215,15 @@ class Plugin {
212
215
  if (passengers && passengers.length && !noPaxList) {
213
216
  RoomConfig.PaxList = passengers.map(p => {
214
217
  const PaxDetails = {
215
- Forename: p.firstName,
216
- Surname: p.lastName,
218
+ Forename: this.escapeInvalidXmlChars(p.firstName),
219
+ Surname: this.escapeInvalidXmlChars(p.lastName),
217
220
  PaxType: {
218
221
  Adult: 'A',
219
222
  Child: 'C',
220
223
  Infant: 'I',
221
224
  }[p.passengerType] || 'A',
222
225
  };
223
- if (p.salutation) PaxDetails.Title = p.salutation;
226
+ if (p.salutation) PaxDetails.Title = this.escapeInvalidXmlChars(p.salutation);
224
227
  if (p.dob) PaxDetails.DateOfBirth = p.dob;
225
228
  if (!R.isNil(p.age) && !isNaN(p.age)) {
226
229
  if (!(p.passengerType === 'Adult' && p.age === 0)) {
@@ -236,7 +239,10 @@ class Plugin {
236
239
  });
237
240
  this.escapeInvalidXmlChars = str => {
238
241
  if (!str) return '';
239
- return str.replace(/[^\x00-\x7F]+/g, '')
242
+ const convertAccentedChars = s => {
243
+ return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
244
+ }
245
+ return convertAccentedChars(str)
240
246
  .replace(/’/g, "'")
241
247
  .replace(/‘/g, "'")
242
248
  .replace(/“/g, '"')
@@ -442,50 +448,57 @@ class Plugin {
442
448
  xmlOptions: hostConnectXmlOptions,
443
449
  };
444
450
  // use cache if we are getting the full list
445
- const replyObj = await this.callTourplan(payload);
446
- let products = [];
447
- if (replyObj === 'useFixture') {
448
- products = require('./__fixtures__/fullacoptionlist.json');
449
- } else {
450
- products = R.call(R.compose(
451
- R.map(optionsGroupedBySupplierId => {
452
- const OptGeneral = R.pathOr({}, [0, 'OptGeneral'], optionsGroupedBySupplierId);
453
- const supplierData = {
454
- supplierId: R.path(['SupplierId'], OptGeneral),
455
- supplierName: R.path(['SupplierName'], OptGeneral),
456
- supplierAddress: `${R.pathOr('', ['Address1'], OptGeneral)}, ${R.pathOr('', ['Address2'], OptGeneral)}, ${R.pathOr('', ['Address3'], OptGeneral)}, ${R.pathOr('', ['Address4'], OptGeneral)}, ${R.pathOr('', ['Address5'], OptGeneral)}`,
457
- serviceTypes: R.uniq(optionsGroupedBySupplierId.map(R.path(['OptGeneral', 'ButtonName']))),
458
- };
459
- return translateTPOption({
460
- supplierData,
461
- optionsGroupedBySupplierId,
462
- typeDefs: productTypeDefs,
463
- query: productQuery,
464
- });
465
- }),
466
- R.values,
467
- R.groupBy(R.path(['OptGeneral', 'SupplierId'])),
468
- root => {
469
- if (!searchInput) return root;
470
- const getFullSearchStr = o => {
471
- const fullPptionName = `${R.path(['OptGeneral', 'Description'], o) || ''}-${R.path(['OptGeneral', 'Comment'], o) || ''}`;
472
- return `${R.path(['OptGeneral', 'SupplierName'], o) || ''} ${fullPptionName} ${R.path(['Opt'], o)} ${R.path(['OptGeneral', 'SupplierId'], o) || ''}`;
473
- };
474
- const inputValueLower = searchInput.trim().toLowerCase();
475
- const parts = inputValueLower.split(' ').filter(Boolean); // Filter out any empty strings just in case
476
- return root.filter(option => {
477
- const fullSearchStr = getFullSearchStr(option).toLowerCase();
478
- return parts.every(part => fullSearchStr.includes(part));
479
- });
480
- },
481
- root => {
482
- const options = R.pathOr([], ['OptionInfoReply', 'Option'], root);
483
- // due to the new parser, single option will be returned as an object
484
- // instead of an array
485
- if (Array.isArray(options)) return options;
486
- return [options];
487
- },
488
- ), replyObj);
451
+ // for example: for searchInput (backend search), we shouldn't get the full list from
452
+ // tourplan each time user search
453
+ const replyObj = optionId
454
+ ? await this.callTourplan(payload)
455
+ : await this.cache.getOrExec({
456
+ fnParams: [model],
457
+ fn: () => this.callTourplan(payload),
458
+ ttl: 60 * 60 * 2, // 2 hours
459
+ forceRefresh: Boolean(forceRefresh),
460
+ });
461
+ const products = R.call(R.compose(
462
+ R.map(optionsGroupedBySupplierId => {
463
+ const OptGeneral = R.pathOr({}, [0, 'OptGeneral'], optionsGroupedBySupplierId);
464
+ const supplierData = {
465
+ supplierId: R.path(['SupplierId'], OptGeneral),
466
+ supplierName: R.path(['SupplierName'], OptGeneral),
467
+ supplierAddress: `${R.pathOr('', ['Address1'], OptGeneral)}, ${R.pathOr('', ['Address2'], OptGeneral)}, ${R.pathOr('', ['Address3'], OptGeneral)}, ${R.pathOr('', ['Address4'], OptGeneral)}, ${R.pathOr('', ['Address5'], OptGeneral)}`,
468
+ serviceTypes: R.uniq(optionsGroupedBySupplierId.map(R.path(['OptGeneral', 'ButtonName']))),
469
+ };
470
+ return translateTPOption({
471
+ supplierData,
472
+ optionsGroupedBySupplierId,
473
+ typeDefs: productTypeDefs,
474
+ query: productQuery,
475
+ });
476
+ }),
477
+ R.values,
478
+ R.groupBy(R.path(['OptGeneral', 'SupplierId'])),
479
+ root => {
480
+ if (!searchInput) return root;
481
+ const getFullSearchStr = o => {
482
+ const fullPptionName = `${R.path(['OptGeneral', 'Description'], o) || ''}-${R.path(['OptGeneral', 'Comment'], o) || ''}`;
483
+ return `${R.path(['OptGeneral', 'SupplierName'], o) || ''} ${fullPptionName} ${R.path(['Opt'], o)} ${R.path(['OptGeneral', 'SupplierId'], o) || ''}`;
484
+ };
485
+ const inputValueLower = searchInput.trim().toLowerCase();
486
+ const parts = inputValueLower.split(' ').filter(Boolean); // Filter out any empty strings just in case
487
+ return root.filter(option => {
488
+ const fullSearchStr = getFullSearchStr(option).toLowerCase();
489
+ return parts.every(part => fullSearchStr.includes(part));
490
+ });
491
+ },
492
+ root => {
493
+ const options = R.pathOr([], ['OptionInfoReply', 'Option'], root);
494
+ // due to the new parser, single option will be returned as an object
495
+ // instead of an array
496
+ if (Array.isArray(options)) return options;
497
+ return [options];
498
+ },
499
+ ), replyObj);
500
+ if (!(products && products.length)) {
501
+ throw new Error('No products found');
489
502
  }
490
503
  return {
491
504
  products,
@@ -673,8 +686,7 @@ class Plugin {
673
686
  ...(puInfo.time && puInfo.time.replace(/\D/g, '') ? {
674
687
  puTime: puInfo.time.replace(/\D/g, ''),
675
688
  } : {}),
676
- puRemark: this.escapeInvalidXmlChars(`${puInfo.time ? `Time: ${puInfo.time || 'NA'},` : ''}
677
- ${puInfo.location ? `Location: ${puInfo.location || 'NA'},` : ''}
689
+ puRemark: this.escapeInvalidXmlChars(`${puInfo.location ? `Location: ${puInfo.location || 'NA'},` : ''}
678
690
  ${puInfo.flightDetails ? `Flight: ${puInfo.flightDetails || 'NA'},` : ''}
679
691
  `),
680
692
  } : {}),
@@ -683,12 +695,11 @@ class Plugin {
683
695
  ...(doInfo.time && doInfo.time.replace(/\D/g, '') ? {
684
696
  doTime: doInfo.time.replace(/\D/g, ''),
685
697
  } : {}),
686
- doRemark: this.escapeInvalidXmlChars(`${doInfo.time ? `Time: ${doInfo.time || 'NA'},` : ''}
687
- ${doInfo.location ? `Location: ${doInfo.location || 'NA'},` : ''}
698
+ doRemark: this.escapeInvalidXmlChars(`${doInfo.location ? `Location: ${doInfo.location || 'NA'},` : ''}
688
699
  ${doInfo.flightDetails ? `Flight: ${doInfo.flightDetails || 'NA'},` : ''}
689
700
  `),
690
701
  } : {}),
691
- Remarks: this.escapeInvalidXmlChars(`${notes || ''} ${extraText ? `\nExtras: ${extraText}` : ''}`).slice(0, 240),
702
+ Remarks: this.escapeInvalidXmlChars(`${notes || ''} ${extraText ? `\nExtras: ${extraText}` : ''}`).slice(0, 220),
692
703
  Opt: optionId,
693
704
  DateFrom: startDate,
694
705
  RateId: 'Default',
@@ -767,10 +778,10 @@ class Plugin {
767
778
  });
768
779
  let searchCriterias = [];
769
780
  if (bookingId) {
770
- searchCriterias = ['BookingId', 'Ref', 'AgentRef'].map(key => ({ [key]: bookingId }));
781
+ searchCriterias = ['BookingId', 'Ref', 'AgentRef'].map(key => ({ [key]: this.escapeInvalidXmlChars(bookingId) }));
771
782
  }
772
783
  if (name) {
773
- searchCriterias.push({ NameContains: name });
784
+ searchCriterias.push({ NameContains: this.escapeInvalidXmlChars(name) });
774
785
  }
775
786
  const allSearches = searchCriterias.length
776
787
  ? searchCriterias.map(async keyObj => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-tourplan",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "description": "Tourplan's TI2 Plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {