ti2-ventrata 1.0.17 → 1.0.19

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,6 +35,8 @@ const getHeaders = ({
201
35
  ...acceptLanguage ? { 'Accept-Language': acceptLanguage } : {},
202
36
  'Content-Type': 'application/json',
203
37
  ...resellerId ? { Referer: resellerId } : {},
38
+ // 'Octo-Capabilities': 'octo/pricing,octo/pickups',
39
+ 'Octo-Capabilities': 'octo/pricing',
204
40
  });
205
41
 
206
42
  class Plugin {
@@ -279,6 +115,10 @@ class Plugin {
279
115
  resellerId,
280
116
  },
281
117
  payload,
118
+ typeDefsAndQueries: {
119
+ productTypeDefs,
120
+ productQuery,
121
+ },
282
122
  }) {
283
123
  let url = `${endpoint || this.endpoint}/products`;
284
124
  if (!isNilOrEmpty(payload)) {
@@ -299,7 +139,14 @@ class Plugin {
299
139
  headers,
300
140
  }));
301
141
  if (!Array.isArray(results)) results = [results];
302
- 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
+ );
303
150
  // dynamic extra filtering
304
151
  if (!isNilOrEmpty(payload)) {
305
152
  const extraFilters = R.omit(['productId'], payload);
@@ -318,50 +165,20 @@ class Plugin {
318
165
  }
319
166
 
320
167
  async searchQuote({
321
- token: {
322
- apiKey,
323
- endpoint,
324
- octoEnv,
325
- acceptLanguage,
326
- resellerId,
327
- },
328
- payload: {
329
- productIds,
330
- optionIds,
331
- occupancies,
332
- },
168
+ // token: {
169
+ // apiKey,
170
+ // endpoint,
171
+ // octoEnv,
172
+ // acceptLanguage,
173
+ // resellerId,
174
+ // },
175
+ // payload: {
176
+ // productIds,
177
+ // optionIds,
178
+ // occupancies,
179
+ // },
333
180
  }) {
334
- assert(occupancies.length > 0, 'there should be at least one occupancy');
335
- assert(productIds.length === optionIds.length, 'mismatched product/option combinations');
336
- assert(productIds.length === occupancies.length, 'mismatched product/occupancies combinations');
337
- assert(productIds.every(Boolean), 'some invalid productId(s)');
338
- assert(optionIds.every(Boolean), 'some invalid optionId(s)');
339
- assert(occupancies.every(Boolean), 'some invalid occupacies(s)');
340
- const quote = occupancies.map(() => productIds.map(productId => ({ productId })));
341
- let productsDetail = await Promise.map(R.uniq(productIds), productId => {
342
- const headers = getHeaders({
343
- apiKey,
344
- endpoint,
345
- octoEnv,
346
- acceptLanguage,
347
- resellerId,
348
- });
349
- const url = `${endpoint || this.endpoint}/products/${productId}`;
350
- return axios({
351
- method: 'get',
352
- url,
353
- headers,
354
- });
355
- }, { concurrency: CONCURRENCY }).map(({ data }) => data);
356
- productsDetail = R.indexBy(R.prop('id'), productsDetail);
357
- // console.log({ optionIds });
358
- productIds.forEach((productId, productIdIx) => {
359
- const optionDetail = productsDetail[productId]
360
- .options.filter(({ id }) => id === optionIds[productIdIx])[0];
361
- quote[productIdIx] =
362
- pickUnit(optionDetail.units, occupancies[productIdIx]).map(e => doMap(e, rateMap));
363
- });
364
- return { quote };
181
+ return { quote: [] };
365
182
  }
366
183
 
367
184
  async searchAvailability({
@@ -372,42 +189,33 @@ class Plugin {
372
189
  acceptLanguage,
373
190
  resellerId,
374
191
  },
375
- token,
376
192
  payload: {
377
193
  productIds,
378
194
  optionIds,
379
- occupancies,
195
+ units,
380
196
  startDate,
381
197
  endDate,
382
198
  dateFormat,
383
199
  currency,
384
200
  },
201
+ typeDefsAndQueries: {
202
+ availTypeDefs,
203
+ availQuery,
204
+ },
385
205
  }) {
386
206
  assert(this.jwtKey, 'JWT secret should be set');
387
- assert(occupancies.length > 0, 'there should be at least one occupancy');
388
207
  assert(
389
208
  productIds.length === optionIds.length,
390
209
  'mismatched productIds/options length',
391
210
  );
392
211
  assert(
393
- optionIds.length === occupancies.length,
394
- 'mismatched options/occupancies length',
212
+ optionIds.length === units.length,
213
+ 'mismatched options/units length',
395
214
  );
396
215
  assert(productIds.every(Boolean), 'some invalid productId(s)');
397
216
  assert(optionIds.every(Boolean), 'some invalid optionId(s)');
398
- assert(occupancies.every(Boolean), 'some invalid occupacies(s)');
399
217
  const localDateStart = moment(startDate, dateFormat).format('YYYY-MM-DD');
400
218
  const localDateEnd = moment(endDate, dateFormat).format('YYYY-MM-DD');
401
- // obtain the rates
402
- const { quote } = await this.searchQuote({
403
- token,
404
- payload: {
405
- productIds,
406
- optionIds,
407
- occupancies,
408
- },
409
- });
410
- const rates = quote.map(q => q.map(({ rateId }) => rateId));
411
219
  const headers = getHeaders({
412
220
  apiKey,
413
221
  endpoint,
@@ -416,43 +224,35 @@ class Plugin {
416
224
  resellerId,
417
225
  });
418
226
  const url = `${endpoint || this.endpoint}/availability`;
419
- let availability = (
420
- await Promise.map(rates, async (rate, rateIx) => {
421
- const qtys = R.countBy(x => x)(rate);
227
+ const availability = (
228
+ await Promise.map(productIds, async (productId, ix) => {
422
229
  const data = {
423
- productId: productIds[rateIx],
424
- optionId: optionIds[rateIx],
230
+ productId,
231
+ optionId: optionIds[ix],
425
232
  localDateStart,
426
233
  localDateEnd,
427
- currency,
428
- units: Object.entries(qtys).map(([id, quantity]) => ({
429
- id, quantity,
430
- })),
234
+ units: units[ix].map(u => ({ id: u.unitId, quantity: u.quantity })),
431
235
  };
432
- return axios({
236
+ if (currency) data.currency = currency;
237
+ const result = R.path(['data'], await axios({
433
238
  method: 'post',
434
239
  url,
435
240
  data,
436
241
  headers,
437
- });
438
- }, { concurrency: CONCURRENCY })
439
- ).map(({ data }) => data);
440
- availability = availability.map(
441
- (avails, availsIx) => (avails.map(
442
- avail => (avail.available ? ({
443
- key: jwt.sign(({
444
- productId: productIds[availsIx],
445
- optionId: optionIds[availsIx],
446
- 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],
447
250
  currency,
448
- unitItems: rates[availsIx].map(rate => ({ unitId: rate })),
449
- }), this.jwtKey),
450
- ...doMap(avail, availabilityMap),
451
- available: true,
452
- }) : ({
453
- available: false,
454
- })),
455
- )),
251
+ unitsWithQuantity: units[ix],
252
+ jwtKey: this.jwtKey,
253
+ },
254
+ }));
255
+ }, { concurrency: CONCURRENCY })
456
256
  );
457
257
  return { availability };
458
258
  }
@@ -465,42 +265,33 @@ class Plugin {
465
265
  acceptLanguage,
466
266
  resellerId,
467
267
  },
468
- token,
469
268
  payload: {
470
269
  productIds,
471
270
  optionIds,
472
- occupancies,
271
+ units,
473
272
  startDate,
474
273
  endDate,
475
274
  currency,
476
275
  dateFormat,
477
276
  },
277
+ typeDefsAndQueries: {
278
+ availTypeDefs,
279
+ availQuery,
280
+ },
478
281
  }) {
479
282
  assert(this.jwtKey, 'JWT secret should be set');
480
- assert(occupancies.length > 0, 'there should be at least one occupancy');
481
283
  assert(
482
284
  productIds.length === optionIds.length,
483
285
  'mismatched productIds/options length',
484
286
  );
485
287
  assert(
486
- optionIds.length === occupancies.length,
487
- 'mismatched options/occupancies length',
288
+ optionIds.length === units.length,
289
+ 'mismatched options/units length',
488
290
  );
489
291
  assert(productIds.every(Boolean), 'some invalid productId(s)');
490
292
  assert(optionIds.every(Boolean), 'some invalid optionId(s)');
491
- assert(occupancies.every(Boolean), 'some invalid occupacies(s)');
492
293
  const localDateStart = moment(startDate, dateFormat).format('YYYY-MM-DD');
493
294
  const localDateEnd = moment(endDate, dateFormat).format('YYYY-MM-DD');
494
- // obtain the rates
495
- const { quote } = await this.searchQuote({
496
- token,
497
- payload: {
498
- productIds,
499
- optionIds,
500
- occupancies,
501
- },
502
- });
503
- const rates = quote.map(q => q.map(({ rateId }) => rateId));
504
295
  const headers = getHeaders({
505
296
  apiKey,
506
297
  endpoint,
@@ -509,31 +300,29 @@ class Plugin {
509
300
  resellerId,
510
301
  });
511
302
  const url = `${endpoint || this.endpoint}/availability/calendar`;
512
- let availability = (
513
- await Promise.map(rates, async (rate, rateIx) => {
514
- const qtys = R.countBy(x => x)(rate);
303
+ const availability = (
304
+ await Promise.map(productIds, async (productId, ix) => {
515
305
  const data = {
516
- productId: productIds[rateIx],
517
- optionId: optionIds[rateIx],
306
+ productId,
307
+ optionId: optionIds[ix],
518
308
  localDateStart,
519
309
  localDateEnd,
520
- currency,
521
- units: Object.entries(qtys).map(([id, quantity]) => ({
522
- id, quantity,
523
- })),
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 })),
524
312
  };
525
- return axios({
313
+ if (currency) data.currency = currency;
314
+ const result = await axios({
526
315
  method: 'post',
527
316
  url,
528
317
  data,
529
318
  headers,
530
319
  });
320
+ return Promise.map(result.data, avail => translateAvailability({
321
+ rootValue: avail,
322
+ typeDefs: availTypeDefs,
323
+ query: availQuery,
324
+ }));
531
325
  }, { concurrency: CONCURRENCY })
532
- ).map(({ data }) => data);
533
- availability = availability.map(
534
- (avails, availsIx) => (avails.map(
535
- avail => doMap(avail, availabilityMap),
536
- )),
537
326
  );
538
327
  return { availability };
539
328
  }
@@ -547,17 +336,21 @@ class Plugin {
547
336
  resellerId,
548
337
  },
549
338
  payload: {
339
+ rebookingId,
550
340
  availabilityKey,
551
341
  holder,
552
342
  notes,
553
343
  reference,
554
344
  settlementMethod,
555
345
  },
346
+ typeDefsAndQueries: {
347
+ bookingTypeDefs,
348
+ bookingQuery,
349
+ },
556
350
  }) {
557
351
  assert(availabilityKey, 'an availability code is required !');
558
- assert(R.path(['name'], holder), 'a holder\' first name is required');
559
- assert(R.path(['surname'], holder), 'a holder\' surname is required');
560
- assert(R.path(['emailAddress'], holder), 'a holder\' email address is required');
352
+ assert(R.path(['name'], holder), 'first name is required');
353
+ assert(R.path(['surname'], holder), 'surname is required');
561
354
  const headers = getHeaders({
562
355
  apiKey,
563
356
  endpoint,
@@ -565,33 +358,45 @@ class Plugin {
565
358
  acceptLanguage,
566
359
  resellerId,
567
360
  });
568
- let url = `${endpoint || this.endpoint}/bookings`;
569
- let data = await jwt.verify(availabilityKey, this.jwtKey);
361
+ const dataForCreateBooking = await jwt.verify(availabilityKey, this.jwtKey);
570
362
  let booking = R.path(['data'], await axios({
571
- method: 'post',
572
- url,
363
+ method: rebookingId ? 'patch' : 'post',
364
+ url: `${endpoint || this.endpoint}/bookings${rebookingId ? `/${rebookingId}` : ''}`,
573
365
  data: {
574
366
  settlementMethod,
575
- ...data,
367
+ ...dataForCreateBooking,
576
368
  notes,
577
369
  },
578
370
  headers,
579
371
  }));
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) });
372
+ // for booking update, we may not need to confirm again
373
+ if (!booking.utcConfirmedAt) {
374
+ const dataForConfirmBooking = {
375
+ contact: {
376
+ fullName: `${holder.name} ${holder.surname}`,
377
+ emailAddress: R.path(['emailAddress'], holder),
378
+ phoneNumber: R.path(['phoneNumber'], holder),
379
+ locales: R.path(['locales'], holder),
380
+ country: R.path(['country'], holder),
381
+ },
382
+ notes,
383
+ resellerReference: reference,
384
+ settlementMethod,
385
+ };
386
+ booking = R.path(['data'], await axios({
387
+ method: 'post',
388
+ url: `${endpoint || this.endpoint}/bookings/${booking.uuid}/confirm`,
389
+ data: dataForConfirmBooking,
390
+ headers,
391
+ }));
392
+ }
393
+ return ({
394
+ booking: await translateBooking({
395
+ rootValue: booking,
396
+ typeDefs: bookingTypeDefs,
397
+ query: bookingQuery,
398
+ }),
399
+ });
595
400
  }
596
401
 
597
402
  async cancelBooking({
@@ -607,6 +412,10 @@ class Plugin {
607
412
  id,
608
413
  reason,
609
414
  },
415
+ typeDefsAndQueries: {
416
+ bookingTypeDefs,
417
+ bookingQuery,
418
+ },
610
419
  }) {
611
420
  assert(!isNilOrEmpty(bookingId) || !isNilOrEmpty(id), 'Invalid booking id');
612
421
  const headers = getHeaders({
@@ -623,7 +432,13 @@ class Plugin {
623
432
  data: { reason },
624
433
  headers,
625
434
  }));
626
- return ({ cancellation: doMap(booking, bookingMap) });
435
+ return ({
436
+ cancellation: await translateBooking({
437
+ rootValue: booking,
438
+ typeDefs: bookingTypeDefs,
439
+ query: bookingQuery,
440
+ }),
441
+ });
627
442
  }
628
443
 
629
444
  async searchBooking({
@@ -642,6 +457,10 @@ class Plugin {
642
457
  travelDateEnd,
643
458
  dateFormat,
644
459
  },
460
+ typeDefsAndQueries: {
461
+ bookingTypeDefs,
462
+ bookingQuery,
463
+ },
645
464
  }) {
646
465
  assert(
647
466
  !isNilOrEmpty(bookingId)
@@ -707,7 +526,13 @@ class Plugin {
707
526
  }
708
527
  return [];
709
528
  })();
710
- return ({ bookings: R.unnest(bookings).map(doMapCurry(bookingMap)) });
529
+ return ({
530
+ bookings: await Promise.map(R.unnest(bookings), booking => translateBooking({
531
+ rootValue: booking,
532
+ typeDefs: bookingTypeDefs,
533
+ query: bookingQuery,
534
+ })),
535
+ });
711
536
  }
712
537
  }
713
538
 
package/index.test.js CHANGED
@@ -6,16 +6,31 @@ 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 = {
18
- productName: 'Pub Crawl Tour',
33
+ productName: 'Edinburgh Pub Crawl Tour',
19
34
  };
20
35
  const token = {
21
36
  apiKey: process.env.ti2_ventrata_apiKey,
@@ -103,9 +118,11 @@ 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));
125
+ // console.log(retVal.products.map(p => p.productName))
109
126
  expect(retVal.products).toContainObject([{
110
127
  productName: testProduct.productName,
111
128
  }]);
@@ -120,6 +137,7 @@ describe('search tests', () => {
120
137
  payload: {
121
138
  productId: testProduct.productId,
122
139
  },
140
+ typeDefsAndQueries,
123
141
  });
124
142
  expect(Array.isArray(retVal.products)).toBeTruthy();
125
143
  expect(retVal.products).toHaveLength(1);
@@ -131,6 +149,7 @@ describe('search tests', () => {
131
149
  payload: {
132
150
  productName: '*bus*',
133
151
  },
152
+ typeDefsAndQueries,
134
153
  });
135
154
  expect(Array.isArray(retVal.products)).toBeTruthy();
136
155
  expect(retVal.products.length).toBeGreaterThan(0);
@@ -145,62 +164,39 @@ describe('search tests', () => {
145
164
  dateFormat,
146
165
  productIds: [
147
166
  '28ca088b-bc7b-4746-ab06-5971f1ed5a5e',
148
- '5d981651-e204-4549-bfbe-691043dd2515',
167
+ '3465143f-4902-447a-9c1e-8e5598666663',
149
168
  ],
150
- optionIds: ['DEFAULT', 'DEFAULT'],
151
- occupancies: [
152
- [{ age: 30 }, { age: 40 }],
153
- [{ 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 }],
154
173
  ],
155
174
  },
175
+ typeDefsAndQueries,
156
176
  });
157
177
  expect(retVal).toBeTruthy();
158
178
  const { availability } = retVal;
159
179
  expect(availability).toHaveLength(2);
160
180
  expect(availability[0].length).toBeGreaterThan(0);
161
181
  });
162
- it('should be able to get quotes', async () => {
163
- const retVal = await app.searchQuote({
164
- token,
165
- payload: {
166
- startDate: moment().add(6, 'M').format(dateFormat),
167
- endDate: moment().add(6, 'M').add(2, 'd').format(dateFormat),
168
- dateFormat,
169
- productIds: busProducts.map(({ productId }) => productId),
170
- optionIds: busProducts.map(({ options }) =>
171
- faker.random.arrayElement(options).optionId),
172
- occupancies: [
173
- [{ age: 30 }, { age: 40 }],
174
- [{ age: 30 }, { age: 40 }],
175
- ],
176
- },
177
- });
178
- expect(retVal).toBeTruthy();
179
- const { quote } = retVal;
180
- expect(quote.length).toBeGreaterThan(0);
181
- expect(quote[0]).toContainObject([{
182
- rateName: 'adult',
183
- pricing: expect.toContainObject([{
184
- currency: 'USD',
185
- }]),
186
- }]);
187
- });
182
+
188
183
  let availabilityKey;
189
184
  it('should be able to get availability', async () => {
190
185
  const retVal = await app.searchAvailability({
191
186
  token,
187
+ typeDefsAndQueries,
192
188
  payload: {
193
189
  startDate: moment().add(6, 'M').format(dateFormat),
194
190
  endDate: moment().add(6, 'M').add(2, 'd').format(dateFormat),
195
191
  dateFormat,
196
192
  productIds: [
197
193
  '28ca088b-bc7b-4746-ab06-5971f1ed5a5e',
198
- '5d981651-e204-4549-bfbe-691043dd2515',
194
+ '3465143f-4902-447a-9c1e-8e5598666663',
199
195
  ],
200
- optionIds: ['DEFAULT', 'DEFAULT'],
201
- occupancies: [
202
- [{ age: 30 }, { age: 40 }],
203
- [{ 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 }],
204
200
  ],
205
201
  },
206
202
  });
@@ -217,6 +213,7 @@ describe('search tests', () => {
217
213
  const fullName = faker.name.findName().split(' ');
218
214
  const retVal = await app.createBooking({
219
215
  token,
216
+ typeDefsAndQueries,
220
217
  payload: {
221
218
  availabilityKey,
222
219
  notes: faker.lorem.paragraph(),
@@ -243,6 +240,7 @@ describe('search tests', () => {
243
240
  it('should be able to cancel the booking', async () => {
244
241
  const retVal = await app.cancelBooking({
245
242
  token,
243
+ typeDefsAndQueries,
246
244
  payload: {
247
245
  bookingId: booking.id,
248
246
  reason: faker.lorem.paragraph(),
@@ -258,6 +256,7 @@ describe('search tests', () => {
258
256
  it('it should be able to search bookings by id', async () => {
259
257
  const retVal = await app.searchBooking({
260
258
  token,
259
+ typeDefsAndQueries,
261
260
  payload: {
262
261
  bookingId: booking.id,
263
262
  },
@@ -269,6 +268,7 @@ describe('search tests', () => {
269
268
  it('it should be able to search bookings by reference', async () => {
270
269
  const retVal = await app.searchBooking({
271
270
  token,
271
+ typeDefsAndQueries,
272
272
  payload: {
273
273
  bookingId: reference,
274
274
  },
@@ -280,6 +280,7 @@ describe('search tests', () => {
280
280
  it('it should be able to search bookings by supplierBookingId', async () => {
281
281
  const retVal = await app.searchBooking({
282
282
  token,
283
+ typeDefsAndQueries,
283
284
  payload: {
284
285
  bookingId: booking.supplierBookingId,
285
286
  },
@@ -291,6 +292,7 @@ describe('search tests', () => {
291
292
  it('it should be able to search bookings by travelDate', async () => {
292
293
  const retVal = await app.searchBooking({
293
294
  token,
295
+ typeDefsAndQueries,
294
296
  payload: {
295
297
  travelDateStart: moment().add(6, 'M').format(dateFormat),
296
298
  travelDateEnd: moment().add(6, 'M').add(2, 'd').format(dateFormat),
@@ -305,6 +307,7 @@ describe('search tests', () => {
305
307
  const fullName = faker.name.findName().split(' ');
306
308
  const retVal = await app.createBooking({
307
309
  token,
310
+ typeDefsAndQueries,
308
311
  payload: {
309
312
  availabilityKey,
310
313
  notes: faker.lorem.paragraph(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-ventrata",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
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: R.prop('pricingFrom'),
34
+ unitPricing: R.prop('unitPricingFrom'),
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(['supplierReference']),
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
+ };