ti2-ventrata 1.0.18 → 1.0.20

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.
@@ -9,6 +9,9 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - uses: actions/checkout@v2
12
+ - run: sed -i 's/"file:.*"/"latest"/' package.json
13
+ - name: install dependencies
14
+ run: npm i
12
15
  - uses: actions/setup-node@v1
13
16
  with:
14
17
  node-version: 12
@@ -38,6 +41,9 @@ jobs:
38
41
  runs-on: ubuntu-latest
39
42
  steps:
40
43
  - uses: actions/checkout@v1
44
+ - run: sed -i 's/"file:.*"/"latest"/' package.json
45
+ - name: install dependencies
46
+ run: npm i
41
47
  - uses: JS-DevTools/npm-publish@v1
42
48
  with:
43
49
  token: ${{ secrets.NPM_TOKEN }}
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const axios = require('axios');
1
+ const axiosRaw = require('axios');
2
2
  const curlirize = require('axios-curlirize');
3
3
  const R = require('ramda');
4
4
  const Promise = require('bluebird');
@@ -6,189 +6,23 @@ const assert = require('assert');
6
6
  const moment = require('moment');
7
7
  const jwt = require('jsonwebtoken');
8
8
  const wildcardMatch = require('./utils/wildcardMatch');
9
+ const { translateProduct } = require('./resolvers/product');
10
+ const { translateAvailability } = require('./resolvers/availability');
11
+ const { translateBooking } = require('./resolvers/booking');
9
12
 
10
13
  const CONCURRENCY = 3; // is this ok ?
11
14
  if (process.env.debug) {
12
- curlirize(axios);
15
+ curlirize(axiosRaw);
13
16
  }
14
17
 
15
- const isNilOrEmpty = R.either(R.isNil, R.isEmpty);
16
- const isNilOrEmptyArray = el => {
17
- if (!Array.isArray(el)) return true;
18
- return R.isNil(el) || R.isEmpty(el);
19
- };
20
-
21
- const doMap = (obj, map, omitParam = []) => {
22
- const retVal = {};
23
- const omit = [
24
- ...omitParam,
25
- ...Object.keys(map),
26
- ];
27
- Object.entries(map).forEach(([attribute, fn]) => {
28
- const newVal = fn(obj);
29
- if (newVal !== undefined) {
30
- retVal[attribute] = newVal;
31
- }
18
+ const axios = async (...args) => axiosRaw(...args)
19
+ .catch(err => {
20
+ const errMsg = R.path(['response', 'data', 'errorMessage'], err);
21
+ console.log('error in ti2-ventrata', args[0], errMsg);
22
+ if (errMsg) throw new Error(errMsg);
23
+ throw err;
32
24
  });
33
- return {
34
- ...retVal,
35
- ...R.omit(omit, obj),
36
- };
37
- };
38
-
39
- const doMapCurry = mapObj => item => doMap(item, mapObj);
40
-
41
- const productMapIn = {
42
- productId: R.path(['id']),
43
- productName: o => R.path(['title'], o) || R.prop('internalName', o),
44
- availableCurrencies: R.path(['availableCurrencies']),
45
- defaultCurrency: R.path(['defaultCurrency']),
46
- options: e => R.pathOr(undefined, ['options'], e).map(option => doMap(option, optionMapIn, ['id', 'title'])),
47
- };
48
-
49
- const optionMapIn = {
50
- optionId: R.path(['id']),
51
- optionName: o => R.path(['title'], o) || R.prop('internalName', o),
52
- units: option => R.pathOr(undefined, ['units'], option).map(unit => doMap(unit, unitMap)),
53
- };
54
-
55
- const rateMap = {
56
- rateId: R.path(['id']),
57
- rateName: rateName => R.toLower(R.path(['internalName'], rateName)),
58
- pricing: R.path(['pricingFrom']),
59
- };
60
-
61
- const unitPricingMap = {
62
- unitId: R.prop('unitId'),
63
- original: R.prop('original'),
64
- retail: R.prop('retail'),
65
- net: R.prop('net'),
66
- currencyPrecision: R.prop('currencyPrecision'),
67
- };
68
-
69
- const availabilityMap = {
70
- dateTimeStart: root => R.path(['localDateTimeStart'], root) || R.path(['localDate'], root),
71
- dateTimeEnd: root => R.path(['localDateTimeEnd'], root) || R.path(['localDate'], root),
72
- allDay: R.path(['allDay']),
73
- pricing: root => R.path(['pricing'], root) || R.path(['pricingFrom'], root),
74
- unitPricing: root => {
75
- let before = [];
76
- if (R.path(['unitPricing'], root)) before = R.path(['unitPricing'], root);
77
- else before = R.path(['unitPricingFrom'], root) || [];
78
- return before.map(item => doMap(item, unitPricingMap));
79
- },
80
- vacancies: R.prop('vacancies'),
81
- offer: avail => (avail.offerCode ? doMap(avail, {
82
- offerId: R.path(['offerCode']),
83
- title: R.pathOr(undefined, ['offerTitle']),
84
- description: R.pathOr(undefined, ['offer', 'description']),
85
- }) : undefined),
86
- meetingPoint: avail => (avail.meetingPoint ? doMap(avail, {
87
- description: R.pathOr(undefined, ['meetingPoint']),
88
- dateTime: R.pathOr(undefined, ['meetingLocalDateTime']),
89
- coordinates: () => ((avail.meetingPointLatitude
90
- && avail.meetingPointLongitude) ? doMap(avail, {
91
- lat: R.path(['meetingPointLatitude']),
92
- long: R.path(['meetingPointLongitude']),
93
- }) : undefined),
94
- }) : undefined),
95
- };
96
- const capitalize = sParam => {
97
- if (typeof sParam !== 'string') return '';
98
- const s = sParam.replace(/_/g, ' ');
99
- return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
100
- };
101
-
102
- const unitMap = {
103
- unitId: R.path(['id']),
104
- unitName: o => R.path(['title'], o) || R.prop('internalName', o),
105
- type: R.path(['type']),
106
- subtitle: R.path(['subtitle']),
107
- restrictions: R.path(['restrictions']),
108
- pricing: root => R.path(['pricing'], root) || R.path(['pricingFrom'], root),
109
- };
110
-
111
- const itineraryMap = {
112
- name: R.path(['name']),
113
- type: R.path(['type']),
114
- address: R.path(['address']),
115
- coordinates: itinerary => ((itinerary.latitude
116
- && Boolean(itinerary.longitude)) ? doMap(itinerary, {
117
- lat: R.path(['latitude']),
118
- long: R.path(['longitude']),
119
- }) : undefined),
120
- duration: ({ duration }) => (isNilOrEmpty(duration) ? undefined : doMapCurry({
121
- durationName: R.path(['duration']),
122
- amount: R.path(['durationAmount']),
123
- unit: R.path(['durationUnit']),
124
- })),
125
- };
126
-
127
- const unitItemMap = {
128
- unitItemId: R.path(['uuid']),
129
- unitId: R.path(['unitId']),
130
- unitName: R.path(['unit', 'title']),
131
- supplierBookingId: R.path(['supplierReference']),
132
- status: e => capitalize(R.path(['status'], e)),
133
- contact: R.path(['contact']),
134
- pricing: R.path(['pricing']),
135
- unit: root => doMap(root.unit, unitMap),
136
- };
137
-
138
- const contactMapOut = {
139
- country: R.path(['country']),
140
- emailAddress: R.path(['emailAddress']),
141
- name: R.path(['firstName']),
142
- surname: R.path(['lastName']),
143
- fullName: R.path(['fullName']),
144
- locales: R.path(['locales']),
145
- notes: R.path(['notes']),
146
- phoneNumber: R.path(['phoneNumber']),
147
- };
148
-
149
- const bookingMap = {
150
- id: R.path(['id']),
151
- orderId: R.path(['orderReference']),
152
- bookingId: R.path(['supplierReference']),
153
- supplierBookingId: R.path(['supplierReference']),
154
- status: e => capitalize(R.path(['status'], e)),
155
- productId: R.path(['product', 'id']),
156
- productName: R.path(['product', 'title']),
157
- optionId: R.path(['option', 'id']),
158
- optionName: R.path(['option', 'title']),
159
- itinerary: ({ option: { itinerary } = {} }) =>
160
- (isNilOrEmptyArray(itinerary) ? undefined : itinerary.map(doMapCurry(itineraryMap))),
161
- duration: booking => doMap(booking, {
162
- durationName: R.path(['duration']),
163
- amount: R.path(['durationAmount']),
164
- unit: R.path(['durationUnit']),
165
- }),
166
- cancellable: R.path(['cancellable']),
167
- unitItems: ({ unitItems }) =>
168
- (isNilOrEmptyArray(unitItems) ? undefined : unitItems.map(doMapCurry(unitItemMap))),
169
- start: R.path(['availability', 'localDateTimeStart']),
170
- end: R.path(['availability', 'localDateTimeEnd']),
171
- allDay: R.path(['availability', 'allDay']),
172
- bookingDate: R.path(['utcCreatedAt']),
173
- holder: booking => doMap(R.path(['contact'], booking), contactMapOut),
174
- telephone: R.pathOr(undefined, ['contact', 'phoneNumber']),
175
- notes: R.pathOr(undefined, ['notes']),
176
- price: R.path(['pricing']),
177
- offer: booking => (booking.offerCode ? doMap(booking, {
178
- offerId: R.path(['offerCode']),
179
- title: R.pathOr(undefined, ['offerTitle']),
180
- description: R.pathOr(undefined, ['offer', 'description']),
181
- }) : undefined),
182
- cancelPolicy: R.pathOr(undefined, ['product', 'cancellationPolicy']), // TODO: Looks like cancellation text on appers on the shortDescription of the product entity .. an NLP extractor would be usefull ?
183
- };
184
-
185
- const contactMap = {
186
- fullName: holder => `${holder.name} ${holder.surname}`,
187
- emailAddress: R.path(['emailAddress']),
188
- phoneNumber: R.path(['phoneNumber']),
189
- locales: R.path(['locales']),
190
- country: R.path(['country']),
191
- };
25
+ const isNilOrEmpty = R.either(R.isNil, R.isEmpty);
192
26
 
193
27
  const getHeaders = ({
194
28
  apiKey,
@@ -201,7 +35,8 @@ const getHeaders = ({
201
35
  ...acceptLanguage ? { 'Accept-Language': acceptLanguage } : {},
202
36
  'Content-Type': 'application/json',
203
37
  ...resellerId ? { Referer: resellerId } : {},
204
- 'Octo-Capabilities': 'octo/pricing',
38
+ 'Octo-Capabilities': 'octo/pricing,octo/pickups',
39
+ // 'Octo-Capabilities': 'octo/pricing',
205
40
  });
206
41
 
207
42
  class Plugin {
@@ -280,6 +115,10 @@ class Plugin {
280
115
  resellerId,
281
116
  },
282
117
  payload,
118
+ typeDefsAndQueries: {
119
+ productTypeDefs,
120
+ productQuery,
121
+ },
283
122
  }) {
284
123
  let url = `${endpoint || this.endpoint}/products`;
285
124
  if (!isNilOrEmpty(payload)) {
@@ -300,7 +139,14 @@ class Plugin {
300
139
  headers,
301
140
  }));
302
141
  if (!Array.isArray(results)) results = [results];
303
- let products = results.map(e => doMap(e, productMapIn, ['id', 'title']));
142
+ let products = await Promise.map(
143
+ results,
144
+ product => translateProduct({
145
+ rootValue: product,
146
+ typeDefs: productTypeDefs,
147
+ query: productQuery,
148
+ }),
149
+ );
304
150
  // dynamic extra filtering
305
151
  if (!isNilOrEmpty(payload)) {
306
152
  const extraFilters = R.omit(['productId'], payload);
@@ -319,50 +165,20 @@ class Plugin {
319
165
  }
320
166
 
321
167
  async searchQuote({
322
- token: {
323
- apiKey,
324
- endpoint,
325
- octoEnv,
326
- acceptLanguage,
327
- resellerId,
328
- },
329
- payload: {
330
- productIds,
331
- optionIds,
332
- occupancies,
333
- },
168
+ // token: {
169
+ // apiKey,
170
+ // endpoint,
171
+ // octoEnv,
172
+ // acceptLanguage,
173
+ // resellerId,
174
+ // },
175
+ // payload: {
176
+ // productIds,
177
+ // optionIds,
178
+ // occupancies,
179
+ // },
334
180
  }) {
335
- assert(occupancies.length > 0, 'there should be at least one occupancy');
336
- assert(productIds.length === optionIds.length, 'mismatched product/option combinations');
337
- assert(productIds.length === occupancies.length, 'mismatched product/occupancies combinations');
338
- assert(productIds.every(Boolean), 'some invalid productId(s)');
339
- assert(optionIds.every(Boolean), 'some invalid optionId(s)');
340
- assert(occupancies.every(Boolean), 'some invalid occupacies(s)');
341
- const quote = occupancies.map(() => productIds.map(productId => ({ productId })));
342
- let productsDetail = await Promise.map(R.uniq(productIds), productId => {
343
- const headers = getHeaders({
344
- apiKey,
345
- endpoint,
346
- octoEnv,
347
- acceptLanguage,
348
- resellerId,
349
- });
350
- const url = `${endpoint || this.endpoint}/products/${productId}`;
351
- return axios({
352
- method: 'get',
353
- url,
354
- headers,
355
- });
356
- }, { concurrency: CONCURRENCY }).map(({ data }) => data);
357
- productsDetail = R.indexBy(R.prop('id'), productsDetail);
358
- // console.log({ optionIds });
359
- productIds.forEach((productId, productIdIx) => {
360
- const optionDetail = productsDetail[productId]
361
- .options.filter(({ id }) => id === optionIds[productIdIx])[0];
362
- quote[productIdIx] =
363
- pickUnit(optionDetail.units, occupancies[productIdIx]).map(e => doMap(e, rateMap));
364
- });
365
- return { quote };
181
+ return { quote: [] };
366
182
  }
367
183
 
368
184
  async searchAvailability({
@@ -373,42 +189,33 @@ class Plugin {
373
189
  acceptLanguage,
374
190
  resellerId,
375
191
  },
376
- token,
377
192
  payload: {
378
193
  productIds,
379
194
  optionIds,
380
- occupancies,
195
+ units,
381
196
  startDate,
382
197
  endDate,
383
198
  dateFormat,
384
199
  currency,
385
200
  },
201
+ typeDefsAndQueries: {
202
+ availTypeDefs,
203
+ availQuery,
204
+ },
386
205
  }) {
387
206
  assert(this.jwtKey, 'JWT secret should be set');
388
- assert(occupancies.length > 0, 'there should be at least one occupancy');
389
207
  assert(
390
208
  productIds.length === optionIds.length,
391
209
  'mismatched productIds/options length',
392
210
  );
393
211
  assert(
394
- optionIds.length === occupancies.length,
395
- 'mismatched options/occupancies length',
212
+ optionIds.length === units.length,
213
+ 'mismatched options/units length',
396
214
  );
397
215
  assert(productIds.every(Boolean), 'some invalid productId(s)');
398
216
  assert(optionIds.every(Boolean), 'some invalid optionId(s)');
399
- assert(occupancies.every(Boolean), 'some invalid occupacies(s)');
400
217
  const localDateStart = moment(startDate, dateFormat).format('YYYY-MM-DD');
401
218
  const localDateEnd = moment(endDate, dateFormat).format('YYYY-MM-DD');
402
- // obtain the rates
403
- const { quote } = await this.searchQuote({
404
- token,
405
- payload: {
406
- productIds,
407
- optionIds,
408
- occupancies,
409
- },
410
- });
411
- const rates = quote.map(q => q.map(({ rateId }) => rateId));
412
219
  const headers = getHeaders({
413
220
  apiKey,
414
221
  endpoint,
@@ -417,43 +224,35 @@ class Plugin {
417
224
  resellerId,
418
225
  });
419
226
  const url = `${endpoint || this.endpoint}/availability`;
420
- let availability = (
421
- await Promise.map(rates, async (rate, rateIx) => {
422
- const qtys = R.countBy(x => x)(rate);
227
+ const availability = (
228
+ await Promise.map(productIds, async (productId, ix) => {
423
229
  const data = {
424
- productId: productIds[rateIx],
425
- optionId: optionIds[rateIx],
230
+ productId,
231
+ optionId: optionIds[ix],
426
232
  localDateStart,
427
233
  localDateEnd,
428
- currency,
429
- units: Object.entries(qtys).map(([id, quantity]) => ({
430
- id, quantity,
431
- })),
234
+ units: units[ix].map(u => ({ id: u.unitId, quantity: u.quantity })),
432
235
  };
433
- return axios({
236
+ if (currency) data.currency = currency;
237
+ const result = R.path(['data'], await axios({
434
238
  method: 'post',
435
239
  url,
436
240
  data,
437
241
  headers,
438
- });
439
- }, { concurrency: CONCURRENCY })
440
- ).map(({ data }) => data);
441
- availability = availability.map(
442
- (avails, availsIx) => (avails.map(
443
- avail => (avail.available ? ({
444
- key: jwt.sign(({
445
- productId: productIds[availsIx],
446
- optionId: optionIds[availsIx],
447
- availabilityId: avail.id,
242
+ }));
243
+ return Promise.map(result, avail => translateAvailability({
244
+ typeDefs: availTypeDefs,
245
+ query: availQuery,
246
+ rootValue: avail,
247
+ variableValues: {
248
+ productId,
249
+ optionId: optionIds[ix],
448
250
  currency,
449
- unitItems: rates[availsIx].map(rate => ({ unitId: rate })),
450
- }), this.jwtKey),
451
- ...doMap(avail, availabilityMap),
452
- available: true,
453
- }) : ({
454
- available: false,
455
- })),
456
- )),
251
+ unitsWithQuantity: units[ix],
252
+ jwtKey: this.jwtKey,
253
+ },
254
+ }));
255
+ }, { concurrency: CONCURRENCY })
457
256
  );
458
257
  return { availability };
459
258
  }
@@ -466,42 +265,33 @@ class Plugin {
466
265
  acceptLanguage,
467
266
  resellerId,
468
267
  },
469
- token,
470
268
  payload: {
471
269
  productIds,
472
270
  optionIds,
473
- occupancies,
271
+ units,
474
272
  startDate,
475
273
  endDate,
476
274
  currency,
477
275
  dateFormat,
478
276
  },
277
+ typeDefsAndQueries: {
278
+ availTypeDefs,
279
+ availQuery,
280
+ },
479
281
  }) {
480
282
  assert(this.jwtKey, 'JWT secret should be set');
481
- assert(occupancies.length > 0, 'there should be at least one occupancy');
482
283
  assert(
483
284
  productIds.length === optionIds.length,
484
285
  'mismatched productIds/options length',
485
286
  );
486
287
  assert(
487
- optionIds.length === occupancies.length,
488
- 'mismatched options/occupancies length',
288
+ optionIds.length === units.length,
289
+ 'mismatched options/units length',
489
290
  );
490
291
  assert(productIds.every(Boolean), 'some invalid productId(s)');
491
292
  assert(optionIds.every(Boolean), 'some invalid optionId(s)');
492
- assert(occupancies.every(Boolean), 'some invalid occupacies(s)');
493
293
  const localDateStart = moment(startDate, dateFormat).format('YYYY-MM-DD');
494
294
  const localDateEnd = moment(endDate, dateFormat).format('YYYY-MM-DD');
495
- // obtain the rates
496
- const { quote } = await this.searchQuote({
497
- token,
498
- payload: {
499
- productIds,
500
- optionIds,
501
- occupancies,
502
- },
503
- });
504
- const rates = quote.map(q => q.map(({ rateId }) => rateId));
505
295
  const headers = getHeaders({
506
296
  apiKey,
507
297
  endpoint,
@@ -510,31 +300,29 @@ class Plugin {
510
300
  resellerId,
511
301
  });
512
302
  const url = `${endpoint || this.endpoint}/availability/calendar`;
513
- let availability = (
514
- await Promise.map(rates, async (rate, rateIx) => {
515
- const qtys = R.countBy(x => x)(rate);
303
+ const availability = (
304
+ await Promise.map(productIds, async (productId, ix) => {
516
305
  const data = {
517
- productId: productIds[rateIx],
518
- optionId: optionIds[rateIx],
306
+ productId,
307
+ optionId: optionIds[ix],
519
308
  localDateStart,
520
309
  localDateEnd,
521
- currency,
522
- units: Object.entries(qtys).map(([id, quantity]) => ({
523
- id, quantity,
524
- })),
310
+ // units is required here to get the total pricing for the calendar
311
+ units: units[ix].map(u => ({ id: u.unitId, quantity: u.quantity })),
525
312
  };
526
- return axios({
313
+ if (currency) data.currency = currency;
314
+ const result = await axios({
527
315
  method: 'post',
528
316
  url,
529
317
  data,
530
318
  headers,
531
319
  });
320
+ return Promise.map(result.data, avail => translateAvailability({
321
+ rootValue: avail,
322
+ typeDefs: availTypeDefs,
323
+ query: availQuery,
324
+ }));
532
325
  }, { concurrency: CONCURRENCY })
533
- ).map(({ data }) => data);
534
- availability = availability.map(
535
- (avails, availsIx) => (avails.map(
536
- avail => doMap(avail, availabilityMap),
537
- )),
538
326
  );
539
327
  return { availability };
540
328
  }
@@ -548,11 +336,17 @@ class Plugin {
548
336
  resellerId,
549
337
  },
550
338
  payload: {
339
+ rebookingId,
551
340
  availabilityKey,
552
341
  holder,
553
342
  notes,
554
343
  reference,
555
344
  settlementMethod,
345
+ pickupPoint,
346
+ },
347
+ typeDefsAndQueries: {
348
+ bookingTypeDefs,
349
+ bookingQuery,
556
350
  },
557
351
  }) {
558
352
  assert(availabilityKey, 'an availability code is required !');
@@ -565,33 +359,46 @@ class Plugin {
565
359
  acceptLanguage,
566
360
  resellerId,
567
361
  });
568
- let url = `${endpoint || this.endpoint}/bookings`;
569
- let data = await jwt.verify(availabilityKey, this.jwtKey);
362
+ const dataForCreateBooking = await jwt.verify(availabilityKey, this.jwtKey);
570
363
  let booking = R.path(['data'], await axios({
571
- method: 'post',
572
- url,
364
+ method: rebookingId ? 'patch' : 'post',
365
+ url: `${endpoint || this.endpoint}/bookings${rebookingId ? `/${rebookingId}` : ''}`,
573
366
  data: {
574
367
  settlementMethod,
575
- ...data,
368
+ ...dataForCreateBooking,
576
369
  notes,
370
+ ...(pickupPoint ? { pickupRequested: true, pickupPointId: pickupPoint } : {}),
577
371
  },
578
372
  headers,
579
373
  }));
580
- url = `${endpoint || this.endpoint}/bookings/${booking.uuid}/confirm`;
581
- const contact = doMap(holder, contactMap);
582
- data = {
583
- contact,
584
- notes,
585
- resellerReference: reference,
586
- settlementMethod,
587
- };
588
- booking = R.path(['data'], await axios({
589
- method: 'post',
590
- url,
591
- data,
592
- headers,
593
- }));
594
- return ({ booking: doMap(booking, bookingMap) });
374
+ // for booking update, we may not need to confirm again
375
+ if (!booking.utcConfirmedAt) {
376
+ const dataForConfirmBooking = {
377
+ contact: {
378
+ fullName: `${holder.name} ${holder.surname}`,
379
+ emailAddress: R.path(['emailAddress'], holder),
380
+ phoneNumber: R.path(['phoneNumber'], holder),
381
+ locales: R.path(['locales'], holder),
382
+ country: R.path(['country'], holder),
383
+ },
384
+ notes,
385
+ resellerReference: reference,
386
+ settlementMethod,
387
+ };
388
+ booking = R.path(['data'], await axios({
389
+ method: 'post',
390
+ url: `${endpoint || this.endpoint}/bookings/${booking.uuid}/confirm`,
391
+ data: dataForConfirmBooking,
392
+ headers,
393
+ }));
394
+ }
395
+ return ({
396
+ booking: await translateBooking({
397
+ rootValue: booking,
398
+ typeDefs: bookingTypeDefs,
399
+ query: bookingQuery,
400
+ }),
401
+ });
595
402
  }
596
403
 
597
404
  async cancelBooking({
@@ -607,6 +414,10 @@ class Plugin {
607
414
  id,
608
415
  reason,
609
416
  },
417
+ typeDefsAndQueries: {
418
+ bookingTypeDefs,
419
+ bookingQuery,
420
+ },
610
421
  }) {
611
422
  assert(!isNilOrEmpty(bookingId) || !isNilOrEmpty(id), 'Invalid booking id');
612
423
  const headers = getHeaders({
@@ -623,7 +434,13 @@ class Plugin {
623
434
  data: { reason },
624
435
  headers,
625
436
  }));
626
- return ({ cancellation: doMap(booking, bookingMap) });
437
+ return ({
438
+ cancellation: await translateBooking({
439
+ rootValue: booking,
440
+ typeDefs: bookingTypeDefs,
441
+ query: bookingQuery,
442
+ }),
443
+ });
627
444
  }
628
445
 
629
446
  async searchBooking({
@@ -642,6 +459,10 @@ class Plugin {
642
459
  travelDateEnd,
643
460
  dateFormat,
644
461
  },
462
+ typeDefsAndQueries: {
463
+ bookingTypeDefs,
464
+ bookingQuery,
465
+ },
645
466
  }) {
646
467
  assert(
647
468
  !isNilOrEmpty(bookingId)
@@ -707,7 +528,13 @@ class Plugin {
707
528
  }
708
529
  return [];
709
530
  })();
710
- return ({ bookings: R.unnest(bookings).map(doMapCurry(bookingMap)) });
531
+ return ({
532
+ bookings: await Promise.map(R.unnest(bookings), booking => translateBooking({
533
+ rootValue: booking,
534
+ typeDefs: bookingTypeDefs,
535
+ query: bookingQuery,
536
+ })),
537
+ });
711
538
  }
712
539
  }
713
540
 
package/index.test.js CHANGED
@@ -6,12 +6,27 @@ const faker = require('faker');
6
6
  const Plugin = require('./index');
7
7
  const fixtureUnits = require('./__fixtures__/units.js');
8
8
 
9
+ const { typeDefs: productTypeDefs, query: productQuery } = require('./node_modules/ti2/controllers/graphql-schemas/product');
10
+ const { typeDefs: availTypeDefs, query: availQuery } = require('./node_modules/ti2/controllers/graphql-schemas/availability');
11
+ const { typeDefs: bookingTypeDefs, query: bookingQuery } = require('./node_modules/ti2/controllers/graphql-schemas/booking');
12
+ const { typeDefs: rateTypeDefs, query: rateQuery } = require('./node_modules/ti2/controllers/graphql-schemas/rate');
13
+
14
+ const typeDefsAndQueries = {
15
+ productTypeDefs,
16
+ productQuery,
17
+ availTypeDefs,
18
+ availQuery,
19
+ bookingTypeDefs,
20
+ bookingQuery,
21
+ rateTypeDefs,
22
+ rateQuery,
23
+ };
24
+
9
25
  const app = new Plugin({
10
26
  jwtKey: process.env.ti2_ventrata_jwtKey,
27
+ endpoint: process.env.ti2_ventrata_endpoint,
11
28
  });
12
29
 
13
- const rnd = arr => arr[Math.floor(Math.random() * arr.length)];
14
-
15
30
  describe('search tests', () => {
16
31
  let products;
17
32
  let testProduct = {
@@ -103,6 +118,7 @@ describe('search tests', () => {
103
118
  it('get for all products, a test product should exist', async () => {
104
119
  const retVal = await app.searchProducts({
105
120
  token,
121
+ typeDefsAndQueries,
106
122
  });
107
123
  expect(Array.isArray(retVal.products)).toBeTruthy();
108
124
  // console.log(retVal.products.filter(({ productName }) => productName === testProduct.productName));
@@ -121,6 +137,7 @@ describe('search tests', () => {
121
137
  payload: {
122
138
  productId: testProduct.productId,
123
139
  },
140
+ typeDefsAndQueries,
124
141
  });
125
142
  expect(Array.isArray(retVal.products)).toBeTruthy();
126
143
  expect(retVal.products).toHaveLength(1);
@@ -132,6 +149,7 @@ describe('search tests', () => {
132
149
  payload: {
133
150
  productName: '*bus*',
134
151
  },
152
+ typeDefsAndQueries,
135
153
  });
136
154
  expect(Array.isArray(retVal.products)).toBeTruthy();
137
155
  expect(retVal.products.length).toBeGreaterThan(0);
@@ -146,62 +164,39 @@ describe('search tests', () => {
146
164
  dateFormat,
147
165
  productIds: [
148
166
  '28ca088b-bc7b-4746-ab06-5971f1ed5a5e',
149
- '5d981651-e204-4549-bfbe-691043dd2515',
167
+ '3465143f-4902-447a-9c1e-8e5598666663',
150
168
  ],
151
- optionIds: ['DEFAULT', 'DEFAULT'],
152
- occupancies: [
153
- [{ age: 30 }, { age: 40 }],
154
- [{ age: 30 }, { age: 40 }],
169
+ optionIds: ['DEFAULT', 'dbe73645-2dd9-4cde-ade0-4faa95668d01'],
170
+ units: [
171
+ [{ unitId: 'unit_c1709f42-297e-4f7e-bd6b-3e77d4622d8a', quantity: 2 }],
172
+ [{ unitId: 'unit_d49f8d25-5b37-4365-b67d-daa0594d021e', quantity: 2 }],
155
173
  ],
156
174
  },
175
+ typeDefsAndQueries,
157
176
  });
158
177
  expect(retVal).toBeTruthy();
159
178
  const { availability } = retVal;
160
179
  expect(availability).toHaveLength(2);
161
180
  expect(availability[0].length).toBeGreaterThan(0);
162
181
  });
163
- it('should be able to get quotes', async () => {
164
- const retVal = await app.searchQuote({
165
- token,
166
- payload: {
167
- startDate: moment().add(6, 'M').format(dateFormat),
168
- endDate: moment().add(6, 'M').add(2, 'd').format(dateFormat),
169
- dateFormat,
170
- productIds: busProducts.map(({ productId }) => productId),
171
- optionIds: busProducts.map(({ options }) =>
172
- faker.random.arrayElement(options).optionId),
173
- occupancies: [
174
- [{ age: 30 }, { age: 40 }],
175
- [{ age: 30 }, { age: 40 }],
176
- ],
177
- },
178
- });
179
- expect(retVal).toBeTruthy();
180
- const { quote } = retVal;
181
- expect(quote.length).toBeGreaterThan(0);
182
- expect(quote[0]).toContainObject([{
183
- rateName: 'adult',
184
- pricing: expect.toContainObject([{
185
- currency: 'USD',
186
- }]),
187
- }]);
188
- });
182
+
189
183
  let availabilityKey;
190
184
  it('should be able to get availability', async () => {
191
185
  const retVal = await app.searchAvailability({
192
186
  token,
187
+ typeDefsAndQueries,
193
188
  payload: {
194
189
  startDate: moment().add(6, 'M').format(dateFormat),
195
190
  endDate: moment().add(6, 'M').add(2, 'd').format(dateFormat),
196
191
  dateFormat,
197
192
  productIds: [
198
193
  '28ca088b-bc7b-4746-ab06-5971f1ed5a5e',
199
- '5d981651-e204-4549-bfbe-691043dd2515',
194
+ '3465143f-4902-447a-9c1e-8e5598666663',
200
195
  ],
201
- optionIds: ['DEFAULT', 'DEFAULT'],
202
- occupancies: [
203
- [{ age: 30 }, { age: 40 }],
204
- [{ age: 30 }, { age: 40 }],
196
+ optionIds: ['DEFAULT', 'dbe73645-2dd9-4cde-ade0-4faa95668d01'],
197
+ units: [
198
+ [{ unitId: 'unit_c1709f42-297e-4f7e-bd6b-3e77d4622d8a', quantity: 2 }],
199
+ [{ unitId: 'unit_d49f8d25-5b37-4365-b67d-daa0594d021e', quantity: 2 }],
205
200
  ],
206
201
  },
207
202
  });
@@ -218,6 +213,7 @@ describe('search tests', () => {
218
213
  const fullName = faker.name.findName().split(' ');
219
214
  const retVal = await app.createBooking({
220
215
  token,
216
+ typeDefsAndQueries,
221
217
  payload: {
222
218
  availabilityKey,
223
219
  notes: faker.lorem.paragraph(),
@@ -244,6 +240,7 @@ describe('search tests', () => {
244
240
  it('should be able to cancel the booking', async () => {
245
241
  const retVal = await app.cancelBooking({
246
242
  token,
243
+ typeDefsAndQueries,
247
244
  payload: {
248
245
  bookingId: booking.id,
249
246
  reason: faker.lorem.paragraph(),
@@ -259,6 +256,7 @@ describe('search tests', () => {
259
256
  it('it should be able to search bookings by id', async () => {
260
257
  const retVal = await app.searchBooking({
261
258
  token,
259
+ typeDefsAndQueries,
262
260
  payload: {
263
261
  bookingId: booking.id,
264
262
  },
@@ -270,6 +268,7 @@ describe('search tests', () => {
270
268
  it('it should be able to search bookings by reference', async () => {
271
269
  const retVal = await app.searchBooking({
272
270
  token,
271
+ typeDefsAndQueries,
273
272
  payload: {
274
273
  bookingId: reference,
275
274
  },
@@ -281,6 +280,7 @@ describe('search tests', () => {
281
280
  it('it should be able to search bookings by supplierBookingId', async () => {
282
281
  const retVal = await app.searchBooking({
283
282
  token,
283
+ typeDefsAndQueries,
284
284
  payload: {
285
285
  bookingId: booking.supplierBookingId,
286
286
  },
@@ -292,6 +292,7 @@ describe('search tests', () => {
292
292
  it('it should be able to search bookings by travelDate', async () => {
293
293
  const retVal = await app.searchBooking({
294
294
  token,
295
+ typeDefsAndQueries,
295
296
  payload: {
296
297
  travelDateStart: moment().add(6, 'M').format(dateFormat),
297
298
  travelDateEnd: moment().add(6, 'M').add(2, 'd').format(dateFormat),
@@ -306,6 +307,7 @@ describe('search tests', () => {
306
307
  const fullName = faker.name.findName().split(' ');
307
308
  const retVal = await app.createBooking({
308
309
  token,
310
+ typeDefsAndQueries,
309
311
  payload: {
310
312
  availabilityKey,
311
313
  notes: faker.lorem.paragraph(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-ventrata",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Ventrata's TI2 Plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -27,9 +27,11 @@
27
27
  "testTimeout": 10000
28
28
  },
29
29
  "dependencies": {
30
+ "@graphql-tools/schema": "^9.0.12",
30
31
  "axios": "^0.24.0",
31
32
  "axios-curlirize": "^1.3.7",
32
33
  "bluebird": "^3.7.2",
34
+ "graphql": "^16.6.0",
33
35
  "jsonwebtoken": "^8.5.1",
34
36
  "moment": "^2.29.4",
35
37
  "ramda": "^0.27.1"
@@ -44,6 +46,7 @@
44
46
  "jest-cli": "^27.4.7",
45
47
  "jest-diff": "^27.4.2",
46
48
  "jest-environment-node": "27.4",
47
- "jest-util": "27.4"
49
+ "jest-util": "27.4",
50
+ "ti2": "latest"
48
51
  }
49
52
  }
@@ -0,0 +1,61 @@
1
+ const { makeExecutableSchema } = require('@graphql-tools/schema');
2
+ const { graphql } = require('graphql');
3
+ const R = require('ramda');
4
+ const jwt = require('jsonwebtoken');
5
+
6
+ const resolvers = {
7
+ Query: {
8
+ key: (root, args) => {
9
+ const {
10
+ productId,
11
+ optionId,
12
+ currency,
13
+ unitsWithQuantity,
14
+ jwtKey,
15
+ } = args;
16
+ if (!jwtKey) return null;
17
+ return jwt.sign({
18
+ productId,
19
+ optionId,
20
+ availabilityId: root.id,
21
+ currency,
22
+ unitItems: R.chain(u => new Array(u.quantity).fill(1).map(() => ({
23
+ unitId: u.unitId,
24
+ })), unitsWithQuantity),
25
+ }, jwtKey);
26
+ },
27
+ dateTimeStart: root => R.path(['localDateTimeStart'], root) || R.path(['localDate'], root),
28
+ dateTimeEnd: root => R.path(['localDateTimeEnd'], root) || R.path(['localDate'], root),
29
+ allDay: R.path(['allDay']),
30
+ vacancies: R.prop('vacancies'),
31
+ available: root => root.status !== 'SOLD_OUT',
32
+ // get the starting price
33
+ pricing: root => R.prop('pricingFrom', root) || R.prop('pricing', root),
34
+ unitPricing: root => R.prop('unitPricingFrom', root) || R.prop('unitPricing', root),
35
+ pickupAvailable: R.prop('pickupAvailable'),
36
+ pickupRequired: R.prop('pickupRequired'),
37
+ pickupPoints: root => R.pathOr([], ['pickupPoints'], root)
38
+ .map(o => ({
39
+ ...o,
40
+ postal: o.postal_code,
41
+ })),
42
+ },
43
+ };
44
+
45
+ const translateAvailability = async ({ rootValue, variableValues, typeDefs, query }) => {
46
+ const schema = makeExecutableSchema({
47
+ typeDefs,
48
+ resolvers,
49
+ });
50
+ const retVal = await graphql({
51
+ schema,
52
+ rootValue,
53
+ source: query,
54
+ variableValues,
55
+ });
56
+ if (retVal.errors) throw new Error(retVal.errors);
57
+ return retVal.data;
58
+ };
59
+ module.exports = {
60
+ translateAvailability,
61
+ };
@@ -0,0 +1,79 @@
1
+ const { makeExecutableSchema } = require('@graphql-tools/schema');
2
+ const R = require('ramda');
3
+ const { graphql } = require('graphql');
4
+
5
+ const capitalize = sParam => {
6
+ if (typeof sParam !== 'string') return '';
7
+ const s = sParam.replace(/_/g, ' ');
8
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
9
+ };
10
+ const resolvers = {
11
+ Query: {
12
+ id: R.path(['id']),
13
+ orderId: R.path(['orderReference']),
14
+ bookingId: R.path(['id']),
15
+ supplierBookingId: R.path(['supplierReference']),
16
+ status: e => capitalize(R.path(['status'], e)),
17
+ productId: R.path(['product', 'id']),
18
+ productName: root => R.path(['product', 'title'], root) || R.path(['product', 'internalName'], root),
19
+ cancellable: R.path(['cancellable']),
20
+ editable: R.path(['cancellable']),
21
+ unitItems: ({ unitItems }) => unitItems.map(unitItem => ({
22
+ unitItemId: unitItem.uuid,
23
+ unitId: unitItem.unitId,
24
+ unitName: unitItem.title || unitItem.internalName,
25
+ })),
26
+ start: R.path(['availability', 'localDateTimeStart']),
27
+ end: R.path(['availability', 'localDateTimeEnd']),
28
+ allDay: R.path(['availability', 'allDay']),
29
+ bookingDate: R.path(['utcCreatedAt']),
30
+ holder: root => ({
31
+ name: R.path(['contact', 'fullName'], root).split(' ')[0],
32
+ surname: R.last(R.path(['contact', 'fullName'], root).split(' ')),
33
+ fullName: R.path(['contact', 'fullName'], root),
34
+ phoneNumber: R.path(['contact', 'phoneNumber'], root),
35
+ emailAddress: R.path(['contact', 'emailAddress'], root),
36
+ }),
37
+ notes: root => root.notes || '',
38
+ price: root => root.pricing,
39
+ cancelPolicy: ({ option }) => {
40
+ if (option.cancellationCutoff) {
41
+ return `Cancel up to ${option.cancellationCutoff} before activity starts`;
42
+ }
43
+ return '';
44
+ },
45
+ optionId: R.path(['option', 'id']),
46
+ optionName: root => R.path(['option', 'title'], root) || R.path(['option', 'internalName'], root),
47
+ resellerReference: R.propOr('', 'resellerReference'),
48
+ publicUrl: () => null,
49
+ privateUrl: () => null,
50
+ pickupRequested: R.prop('pickupRequested'),
51
+ pickupPointId: R.prop('pickupPointId'),
52
+ pickupPoint: root => {
53
+ const pickupPoint = R.path(['pickupPoint'], root);
54
+ if (!pickupPoint) return null;
55
+ return {
56
+ ...pickupPoint,
57
+ postal: pickupPoint.postal_code,
58
+ };
59
+ },
60
+ },
61
+ };
62
+
63
+ const translateBooking = async ({ rootValue, typeDefs, query }) => {
64
+ const schema = makeExecutableSchema({
65
+ typeDefs,
66
+ resolvers,
67
+ });
68
+ const retVal = await graphql({
69
+ schema,
70
+ rootValue,
71
+ source: query,
72
+ });
73
+ if (retVal.errors) throw new Error(retVal.errors);
74
+ return retVal.data;
75
+ };
76
+
77
+ module.exports = {
78
+ translateBooking,
79
+ };
@@ -0,0 +1,48 @@
1
+ const { makeExecutableSchema } = require('@graphql-tools/schema');
2
+ const R = require('ramda');
3
+ const { graphql } = require('graphql');
4
+
5
+ const resolvers = {
6
+ Query: {
7
+ productId: R.path(['id']),
8
+ productName: o => R.path(['title'], o) || R.prop('internalName', o),
9
+ availableCurrencies: R.path(['availableCurrencies']),
10
+ defaultCurrency: R.path(['defaultCurrency']),
11
+ options: R.propOr([], 'options'),
12
+ },
13
+ Option: {
14
+ optionId: R.prop('id'),
15
+ optionName: o => R.path(['title'], o) || R.prop('internalName', o),
16
+ units: R.propOr([], 'units'),
17
+ },
18
+ Unit: {
19
+ unitId: R.path(['id']),
20
+ unitName: o => R.path(['title'], o) || R.prop('internalName', o),
21
+ subtitle: R.path(['subtitle']),
22
+ type: R.prop('type'),
23
+ pricing: root => R.path(['pricing'], root) || R.path(['pricingFrom'], root),
24
+ restrictions: R.prop('restrictions'),
25
+ },
26
+ };
27
+
28
+ const translateProduct = async ({
29
+ rootValue,
30
+ typeDefs,
31
+ query,
32
+ }) => {
33
+ const schema = makeExecutableSchema({
34
+ typeDefs,
35
+ resolvers,
36
+ });
37
+ const retVal = await graphql({
38
+ schema,
39
+ rootValue,
40
+ source: query,
41
+ });
42
+ if (retVal.errors) throw new Error(retVal.errors);
43
+ return retVal.data;
44
+ };
45
+
46
+ module.exports = {
47
+ translateProduct,
48
+ };
@@ -0,0 +1,34 @@
1
+ const { makeExecutableSchema } = require('@graphql-tools/schema');
2
+ const R = require('ramda');
3
+ const { graphql } = require('graphql');
4
+
5
+ const resolvers = {
6
+ Query: {
7
+ rateId: R.path(['unitId']),
8
+ rateName: root => R.toLower(R.path(['unitName'], root)),
9
+ pricing: root => [{
10
+ original: R.path(['total_including_tax'], root),
11
+ retail: R.path(['total_including_tax'], root),
12
+ currencyPrecision: 2,
13
+ currency: R.path(['company', 'currency'], root),
14
+ }],
15
+ },
16
+ };
17
+
18
+ const translateRate = async ({ rootValue, typeDefs, query }) => {
19
+ const schema = makeExecutableSchema({
20
+ typeDefs,
21
+ resolvers,
22
+ });
23
+ const retVal = await graphql({
24
+ schema,
25
+ rootValue,
26
+ source: query,
27
+ });
28
+ if (retVal.errors) throw new Error(retVal.errors);
29
+ return retVal.data;
30
+ };
31
+
32
+ module.exports = {
33
+ translateRate,
34
+ };