ti2-ventrata 1.0.3

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.
@@ -0,0 +1,181 @@
1
+ module.exports = [{
2
+ id: 'adult',
3
+ internalName: 'Adult',
4
+ reference: 'adult',
5
+ type: 'ADULT',
6
+ requiredContactFields: [],
7
+ restrictions: {
8
+ minAge: 16,
9
+ maxAge: 59,
10
+ idRequired: false,
11
+ minQuantity: null,
12
+ maxQuantity: null,
13
+ paxCount: 1,
14
+ accompaniedBy: []
15
+ },
16
+ title: 'Adult',
17
+ titlePlural: 'Adults',
18
+ subtitle: 'Aged 16 - 59',
19
+ pricingFrom: [
20
+ {
21
+ original: 3600,
22
+ retail: 2430,
23
+ net: 2160,
24
+ currency: 'USD',
25
+ currencyPrecision: 2,
26
+ includedTaxes: []
27
+ },
28
+ {
29
+ original: 1996,
30
+ retail: 1796,
31
+ net: 1597,
32
+ currency: 'GBP',
33
+ currencyPrecision: 2,
34
+ includedTaxes: []
35
+ },
36
+ {
37
+ original: 2900,
38
+ retail: 2160,
39
+ net: 1920,
40
+ currency: 'EUR',
41
+ currencyPrecision: 2,
42
+ includedTaxes: []
43
+ }
44
+ ]
45
+ },
46
+ {
47
+ id: 'child',
48
+ internalName: 'Child',
49
+ reference: 'child',
50
+ type: 'CHILD',
51
+ requiredContactFields: [],
52
+ restrictions: {
53
+ minAge: 3,
54
+ maxAge: 16,
55
+ idRequired: false,
56
+ minQuantity: null,
57
+ maxQuantity: null,
58
+ paxCount: 1,
59
+ accompaniedBy: []
60
+ },
61
+ title: 'Child',
62
+ titlePlural: 'Children',
63
+ subtitle: 'Aged 3-16',
64
+ pricingFrom: [
65
+ {
66
+ original: 1900,
67
+ retail: 1260,
68
+ net: 1120,
69
+ currency: 'USD',
70
+ currencyPrecision: 2,
71
+ includedTaxes: []
72
+ },
73
+ {
74
+ original: 998,
75
+ retail: 898,
76
+ net: 798,
77
+ currency: 'GBP',
78
+ currencyPrecision: 2,
79
+ includedTaxes: []
80
+ },
81
+ {
82
+ original: 1500,
83
+ retail: 1080,
84
+ net: 960,
85
+ currency: 'EUR',
86
+ currencyPrecision: 2,
87
+ includedTaxes: []
88
+ }
89
+ ]
90
+ },
91
+ {
92
+ id: 'senior',
93
+ internalName: 'Senior',
94
+ reference: 'senior',
95
+ type: 'SENIOR',
96
+ requiredContactFields: [],
97
+ restrictions: {
98
+ minAge: 60,
99
+ maxAge: 100,
100
+ idRequired: false,
101
+ minQuantity: null,
102
+ maxQuantity: null,
103
+ paxCount: 1,
104
+ accompaniedBy: []
105
+ },
106
+ title: 'Senior',
107
+ titlePlural: 'Seniors',
108
+ subtitle: 'Aged over 60',
109
+ pricingFrom: [
110
+ {
111
+ original: 3400,
112
+ retail: 2250,
113
+ net: 2000,
114
+ currency: 'USD',
115
+ currencyPrecision: 2,
116
+ includedTaxes: []
117
+ },
118
+ {
119
+ original: 1871,
120
+ retail: 1684,
121
+ net: 1497,
122
+ currency: 'GBP',
123
+ currencyPrecision: 2,
124
+ includedTaxes: []
125
+ },
126
+ {
127
+ original: 2600,
128
+ retail: 1980,
129
+ net: 1760,
130
+ currency: 'EUR',
131
+ currencyPrecision: 2,
132
+ includedTaxes: []
133
+ }
134
+ ]
135
+ },
136
+ {
137
+ id: 'family',
138
+ internalName: 'Family',
139
+ reference: 'family',
140
+ type: 'FAMILY',
141
+ requiredContactFields: [],
142
+ restrictions: {
143
+ minAge: 0,
144
+ maxAge: 100,
145
+ idRequired: false,
146
+ minQuantity: null,
147
+ maxQuantity: null,
148
+ paxCount: 4,
149
+ accompaniedBy: []
150
+ },
151
+ title: 'Family',
152
+ titlePlural: 'Families',
153
+ subtitle: '0+',
154
+ pricingFrom: [
155
+ {
156
+ original: 10000,
157
+ retail: 6750,
158
+ net: 6000,
159
+ currency: 'USD',
160
+ currencyPrecision: 2,
161
+ includedTaxes: []
162
+ },
163
+ {
164
+ original: 5599,
165
+ retail: 5039,
166
+ net: 4479,
167
+ currency: 'GBP',
168
+ currencyPrecision: 2,
169
+ includedTaxes: []
170
+ },
171
+ {
172
+ original: 7800,
173
+ retail: 5940,
174
+ net: 5280,
175
+ currency: 'EUR',
176
+ currencyPrecision: 2,
177
+ includedTaxes: []
178
+ }
179
+ ]
180
+ }];
181
+
package/index.js ADDED
@@ -0,0 +1,535 @@
1
+ const axios = require('axios');
2
+ const R = require('ramda');
3
+ const Promise = require('bluebird');
4
+ const assert = require('assert');
5
+ const moment = require('moment');
6
+ const jwt = require('jsonwebtoken');
7
+ const wildcardMatch = require('./utils/wildcardMatch');
8
+
9
+ const isNilOrEmpty = R.either(R.isNil, R.isEmpty);
10
+ const isNilOrEmptyArray = el => {
11
+ if (!Array.isArray(el)) return true;
12
+ return R.isNil(el) || R.isEmpty(el);
13
+ };
14
+
15
+ const doMap = (obj, map) => {
16
+ const retVal = {};
17
+ Object.entries(map).forEach(([attribute, fn]) => {
18
+ const newVal = fn(obj);
19
+ if (newVal !== undefined) {
20
+ retVal[attribute] = newVal;
21
+ }
22
+ });
23
+ return retVal;
24
+ };
25
+
26
+ const doMapCurry = mapObj => item => doMap(item, mapObj);
27
+
28
+ const productMapIn = {
29
+ productId: R.path(['id']),
30
+ productName: R.path(['title']),
31
+ options: e => e.options.map(option => ({
32
+ optionId: R.path(['id'], option),
33
+ optionName: R.path(['title'], option),
34
+ })),
35
+ };
36
+
37
+ const rateMap = {
38
+ rateId: R.path(['id']),
39
+ rateName: R.path(['internalName']),
40
+ pricing: R.path(['pricingFrom']),
41
+ };
42
+
43
+ const availabilityMap = {
44
+ dateTimeStart: R.path(['localDateTimeStart']),
45
+ dateTimeEnd: R.path(['localDateTimeEnd']),
46
+ allDay: R.path(['allDay']),
47
+ pricing: R.path(['pricing']),
48
+ offer: avail => (avail.offerCode ? doMap(avail, {
49
+ offerId: R.path(['offerCode']),
50
+ title: R.pathOr(undefined, ['offerTitle']),
51
+ description: R.pathOr(undefined, ['offer', 'description']),
52
+ }) : undefined),
53
+ meetingPoint: avail => (avail.meetingPoint ? doMap(avail, {
54
+ description: R.pathOr(undefined, ['meetingPoint']),
55
+ dateTime: R.pathOr(undefined, ['meetingLocalDateTime']),
56
+ coordinates: () => ((avail.meetingPointLatitude
57
+ && avail.meetingPointLongitude) ? doMap(avail, {
58
+ lat: R.path(['meetingPointLatitude']),
59
+ long: R.path(['meetingPointLongitude']),
60
+ }) : undefined),
61
+ }) : undefined),
62
+ };
63
+ const capitalize = sParam => {
64
+ if (typeof sParam !== 'string') return '';
65
+ const s = sParam.replace(/_/g, ' ');
66
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
67
+ };
68
+
69
+ // const optionMap = {
70
+ // optionId: R.path(['id']),
71
+ // optionName: R.path(['title']),
72
+ // units: option => R.pathOr(undefined, ['units'], option).map(unit => doMap(unit, unitMap)),
73
+ // };
74
+
75
+ const unitMap = {
76
+ unitId: R.path(['id']),
77
+ unitName: R.path(['title']),
78
+ restrictions: R.path(['restrictions']),
79
+ pricing: R.path(['pricing']),
80
+ };
81
+
82
+ const itineraryMap = {
83
+ name: R.path(['name']),
84
+ type: R.path(['type']),
85
+ address: R.path(['address']),
86
+ coordinates: itinerary => ((itinerary.latitude
87
+ && Boolean(itinerary.longitude)) ? doMap(itinerary, {
88
+ lat: R.path(['latitude']),
89
+ long: R.path(['longitude']),
90
+ }) : undefined),
91
+ duration: ({ duration }) => (isNilOrEmpty(duration) ? undefined : doMapCurry({
92
+ durationName: R.path(['duration']),
93
+ amount: R.path(['durationAmount']),
94
+ unit: R.path(['durationUnit']),
95
+ })),
96
+ };
97
+
98
+ const unitItemMap = {
99
+ unitItemId: R.path(['uuid']),
100
+ supplierId: R.path(['supplierReference']),
101
+ status: e => capitalize(R.path(['status'], e)),
102
+ contact: R.path(['contact']),
103
+ pricing: R.path(['pricing']),
104
+ unit: unit => doMap(unit, unitMap),
105
+ };
106
+
107
+ const contactMapOut = {
108
+ country: R.path(['country']),
109
+ emailAddress: R.path(['emailAddress']),
110
+ name: R.path(['firstName']),
111
+ surname: R.path(['lastName']),
112
+ fullName: R.path(['fullName']),
113
+ locales: R.path(['locales']),
114
+ notes: R.path(['notes']),
115
+ phoneNumber: R.path(['phoneNumber']),
116
+ };
117
+
118
+ const bookingMap = {
119
+ id: R.path(['id']),
120
+ supplierId: R.path(['supplierReference']),
121
+ status: e => capitalize(R.path(['status'], e)),
122
+ productId: R.path(['product', 'id']),
123
+ productName: R.path(['product', 'title']),
124
+ optionId: R.path(['option', 'id']),
125
+ itinerary: ({ option: { itinerary } = {} }) =>
126
+ (isNilOrEmptyArray(itinerary) ? undefined : itinerary.map(doMapCurry(itineraryMap))),
127
+ duration: booking => doMap(booking, {
128
+ durationName: R.path(['duration']),
129
+ amount: R.path(['durationAmount']),
130
+ unit: R.path(['durationUnit']),
131
+ }),
132
+ cancellable: R.path(['cancellable']),
133
+ unitItems: ({ unitItems }) =>
134
+ (isNilOrEmptyArray(unitItems) ? undefined : unitItems.map(doMapCurry(unitItemMap))),
135
+ start: R.path(['availability', 'localDateTimeStart']),
136
+ end: R.path(['availability', 'localDateTimeEnd']),
137
+ allDay: R.path(['availability', 'allDay']),
138
+ bookingDate: R.path(['hotel', 'utcCreatedAt']),
139
+ holder: booking => doMap(R.path(['contact'], booking), contactMapOut),
140
+ telephone: R.pathOr(undefined, ['contact', 'phoneNumber']),
141
+ notes: R.pathOr(undefined, ['notes']),
142
+ price: R.path(['pricing']),
143
+ offer: booking => (booking.offerCode ? doMap(booking, {
144
+ offerId: R.path(['offerCode']),
145
+ title: R.pathOr(undefined, ['offerTitle']),
146
+ description: R.pathOr(undefined, ['offer', 'description']),
147
+ }) : undefined),
148
+ 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 ?
149
+ };
150
+
151
+ const contactMap = {
152
+ fullName: holder => `${holder.name} ${holder.surname}`,
153
+ emailAddress: R.path(['emailAddress']),
154
+ phoneNumber: R.path(['phoneNumber']),
155
+ locales: R.path(['locales']),
156
+ country: R.path(['country']),
157
+ };
158
+
159
+ const getHeaders = ({ apiKey, acceptLanguage, octoEnv }) => ({
160
+ Authorization: `Bearer ${apiKey}`,
161
+ 'Octo-Env': octoEnv,
162
+ ...acceptLanguage ? { 'Accept-Language': acceptLanguage } : {},
163
+ 'Content-Type': 'application/json',
164
+ });
165
+
166
+ class Plugin {
167
+ constructor(params) { // we get the env variables from here
168
+ Object.entries(params).forEach(([attr, value]) => {
169
+ this[attr] = value;
170
+ });
171
+ }
172
+
173
+ async searchProducts({
174
+ token: {
175
+ apiKey = this.apiKey,
176
+ endpoint = this.endpoint,
177
+ octoEnv = this.octoEnv,
178
+ acceptLanguage = this.acceptLanguage,
179
+ },
180
+ payload,
181
+ }) {
182
+ let url = `${endpoint || this.endpoint}/products`;
183
+ if (!isNilOrEmpty(payload)) {
184
+ if (payload.productId) {
185
+ url = `${url}/${payload.productId}`;
186
+ }
187
+ }
188
+ const headers = getHeaders({
189
+ apiKey,
190
+ endpoint,
191
+ octoEnv,
192
+ acceptLanguage,
193
+ });
194
+ let results = R.pathOr([], ['data'], await axios({
195
+ method: 'get',
196
+ url,
197
+ headers,
198
+ }));
199
+ if (!Array.isArray(results)) results = [results];
200
+ let products = results.map(e => doMap(e, productMapIn));
201
+ // dynamic extra filtering
202
+ if (!isNilOrEmpty(payload)) {
203
+ const extraFilters = R.omit(['productId'], payload);
204
+ if (Object.keys(extraFilters).length > 0) {
205
+ products = products.filter(
206
+ product => Object.entries(extraFilters).every(
207
+ ([key, value]) => wildcardMatch(value, product[key]),
208
+ ),
209
+ );
210
+ }
211
+ }
212
+ return ({ products });
213
+ }
214
+
215
+ async searchQuote({
216
+ token: {
217
+ apiKey = this.apiKey,
218
+ endpoint = this.endpoint,
219
+ octoEnv = this.octoEnv,
220
+ acceptLanguage = this.acceptLanguage,
221
+ },
222
+ payload: {
223
+ productIds,
224
+ optionIds,
225
+ occupancies,
226
+ },
227
+ }) {
228
+ assert(occupancies.length > 0, 'there should be at least one occupancy');
229
+ assert(productIds.length === optionIds.length, 'mismatched product/option combinations');
230
+ assert(productIds.length === occupancies.length, 'mismatched product/occupancies combinations');
231
+ assert(productIds.every(Boolean), 'some invalid productId(s)');
232
+ assert(optionIds.every(Boolean), 'some invalid optionId(s)');
233
+ assert(occupancies.every(Boolean), 'some invalid occupacies(s)');
234
+ const quote = occupancies.map(() => productIds.map(productId => ({ productId })));
235
+ let productsDetail = await Promise.map(R.uniq(productIds), productId => {
236
+ const headers = getHeaders({
237
+ apiKey,
238
+ endpoint,
239
+ octoEnv,
240
+ acceptLanguage,
241
+ });
242
+ const url = `${endpoint || this.endpoint}/products/${productId}`;
243
+ return axios({
244
+ method: 'get',
245
+ url,
246
+ headers,
247
+ });
248
+ }, { concurrency: 3 /* is this ok ? */ }).map(({ data }) => data);
249
+ productsDetail = R.indexBy(R.prop('id'), productsDetail);
250
+ // console.log({ optionIds });
251
+ productIds.forEach((productId, productIdIx) => {
252
+ const optionDetail = productsDetail[productId]
253
+ .options.filter(({ id }) => id === optionIds[productIdIx])[0];
254
+ quote[productIdIx] =
255
+ pickUnit(optionDetail.units, occupancies[productIdIx]).map(e => doMap(e, rateMap));
256
+ });
257
+ return { quote };
258
+ }
259
+
260
+ async searchAvailability({
261
+ token: {
262
+ apiKey = this.apiKey,
263
+ endpoint = this.endpoint,
264
+ octoEnv = this.octoEnv,
265
+ acceptLanguage = this.acceptLanguage,
266
+ },
267
+ token,
268
+ payload: {
269
+ productIds,
270
+ optionIds,
271
+ occupancies,
272
+ startDate,
273
+ // endDate,
274
+ dateFormat,
275
+ },
276
+ }) {
277
+ assert(this.jwtKey, 'JWT secret should be set');
278
+ assert(occupancies.length > 0, 'there should be at least one occupancy');
279
+ assert(
280
+ productIds.length === optionIds.length,
281
+ 'mismatched productIds/options length',
282
+ );
283
+ assert(
284
+ optionIds.length === occupancies.length,
285
+ 'mismatched options/occupancies length',
286
+ );
287
+ assert(productIds.every(Boolean), 'some invalid productId(s)');
288
+ assert(optionIds.every(Boolean), 'some invalid optionId(s)');
289
+ assert(occupancies.every(Boolean), 'some invalid occupacies(s)');
290
+ const localDateStart = moment(startDate, dateFormat).format('YYYY-MM-DD');
291
+ const localDateEnd = moment(startDate, dateFormat).format('YYYY-MM-DD');
292
+ // obtain the rates
293
+ const { quote } = await this.searchQuote({
294
+ token,
295
+ payload: {
296
+ productIds,
297
+ optionIds,
298
+ occupancies,
299
+ },
300
+ });
301
+ const rates = quote.map(q => q.map(({ rateId }) => rateId));
302
+ const headers = getHeaders({
303
+ apiKey,
304
+ endpoint,
305
+ octoEnv,
306
+ acceptLanguage,
307
+ });
308
+ const url = `${endpoint || this.endpoint}/availability`;
309
+ let availability = (
310
+ await Promise.map(rates, async (rate, rateIx) => {
311
+ const qtys = R.countBy(x => x)(rate);
312
+ const data = {
313
+ productId: productIds[rateIx],
314
+ optionId: optionIds[rateIx],
315
+ localDateStart,
316
+ localDateEnd,
317
+ units: Object.entries(qtys).map(([id, quantity]) => ({
318
+ id, quantity,
319
+ })),
320
+ };
321
+ return axios({
322
+ method: 'post',
323
+ url,
324
+ data,
325
+ headers,
326
+ });
327
+ }, { concurrency: 3 }) // is this ok ?
328
+ ).map(({ data }) => data);
329
+ availability = availability.map(
330
+ (avails, availsIx) => (avails.map(
331
+ avail => (avail.available ? ({
332
+ key: jwt.sign(({
333
+ productId: productIds[availsIx],
334
+ optionId: optionIds[availsIx],
335
+ availabilityId: avail.id,
336
+ unitItems: rates[availsIx].map(rate => ({ unitId: rate })),
337
+ }), this.jwtKey),
338
+ ...doMap(avail, availabilityMap),
339
+ available: true,
340
+ }) : ({
341
+ available: false,
342
+ })),
343
+ )),
344
+ );
345
+ return { availability };
346
+ }
347
+
348
+ async createBooking({
349
+ token: {
350
+ apiKey = this.apiKey,
351
+ endpoint = this.endpoint,
352
+ octoEnv = this.octoEnv,
353
+ acceptLanguage = this.acceptLanguage,
354
+ },
355
+ payload: {
356
+ availabilityKey,
357
+ notes,
358
+ reference,
359
+ holder,
360
+ },
361
+ }) {
362
+ assert(availabilityKey, 'an availability code is required !');
363
+ assert(R.path(['name'], holder), 'a holder\' first name is required');
364
+ assert(R.path(['surname'], holder), 'a holder\' surname is required');
365
+ assert(R.path(['emailAddress'], holder), 'a holder\' email address is required');
366
+ const headers = getHeaders({
367
+ apiKey,
368
+ endpoint,
369
+ octoEnv,
370
+ acceptLanguage,
371
+ });
372
+ let url = `${endpoint || this.endpoint}/bookings`;
373
+ let data = await jwt.verify(availabilityKey, this.jwtKey);
374
+ let booking = R.path(['data'], await axios({
375
+ method: 'post',
376
+ url,
377
+ data: { ...data, notes },
378
+ headers,
379
+ }));
380
+ url = `${endpoint || this.endpoint}/bookings/${booking.uuid}/confirm`;
381
+ const contact = doMap(holder, contactMap);
382
+ data = {
383
+ notes,
384
+ contact,
385
+ resellerReference: reference,
386
+ };
387
+ booking = R.path(['data'], await axios({
388
+ method: 'post',
389
+ url,
390
+ data,
391
+ headers,
392
+ }));
393
+ return ({ booking: doMap(booking, bookingMap) });
394
+ }
395
+
396
+ async cancelBooking({
397
+ token: {
398
+ apiKey = this.apiKey,
399
+ endpoint = this.endpoint,
400
+ octoEnv = this.octoEnv,
401
+ acceptLanguage = this.acceptLanguage,
402
+ },
403
+ payload: {
404
+ bookingId,
405
+ id,
406
+ reason,
407
+ },
408
+ }) {
409
+ assert(!isNilOrEmpty(bookingId) || !isNilOrEmpty(id), 'Invalid booking id');
410
+ const headers = getHeaders({
411
+ apiKey,
412
+ endpoint,
413
+ octoEnv,
414
+ acceptLanguage,
415
+ });
416
+ const url = `${endpoint || this.endpoint}/bookings/${bookingId || id}`;
417
+ const booking = R.path(['data'], await axios({
418
+ method: 'delete',
419
+ url,
420
+ data: { reason },
421
+ headers,
422
+ }));
423
+ return ({ cancellation: doMap(booking, bookingMap) });
424
+ }
425
+
426
+ async searchBooking({
427
+ token: {
428
+ apiKey = this.apiKey,
429
+ endpoint = this.endpoint,
430
+ octoEnv = this.octoEnv,
431
+ acceptLanguage = this.acceptLanguage,
432
+ },
433
+ payload: {
434
+ bookingId,
435
+ resellerReference,
436
+ supplierId,
437
+ travelDateStart,
438
+ travelDateEnd,
439
+ dateFormat,
440
+ },
441
+ }) {
442
+ assert(
443
+ !isNilOrEmpty(bookingId)
444
+ || !isNilOrEmpty(resellerReference)
445
+ || !isNilOrEmpty(supplierId)
446
+ || !(
447
+ isNilOrEmpty(travelDateStart) && isNilOrEmpty(travelDateEnd) && isNilOrEmpty(dateFormat)
448
+ ),
449
+ 'at least one parameter is required',
450
+ );
451
+ const headers = getHeaders({
452
+ apiKey,
453
+ endpoint,
454
+ octoEnv,
455
+ acceptLanguage,
456
+ });
457
+ const searchByUrl = async url => {
458
+ try {
459
+ return R.path(['data'], await axios({
460
+ method: 'get',
461
+ url,
462
+ headers,
463
+ }));
464
+ } catch (err) {
465
+ return [];
466
+ }
467
+ };
468
+ const bookings = await (async () => {
469
+ let url;
470
+ if (!isNilOrEmpty(bookingId)) {
471
+ return Promise.all([
472
+ searchByUrl(`${endpoint || this.endpoint}/bookings/${bookingId}`),
473
+ searchByUrl(`${endpoint || this.endpoint}/bookings?resellerReference=${bookingId}`),
474
+ searchByUrl(`${endpoint || this.endpoint}/bookings?supplierReference=${bookingId}`),
475
+ ]);
476
+ }
477
+ if (!isNilOrEmpty(resellerReference)) {
478
+ url = `${endpoint || this.endpoint}/bookings?resellerReference=${resellerReference}`;
479
+ return R.path(['data'], await axios({
480
+ method: 'get',
481
+ url,
482
+ headers,
483
+ }));
484
+ }
485
+ if (!isNilOrEmpty(supplierId)) {
486
+ url = `${endpoint || this.endpoint}/bookings?supplierReference=${supplierId}`;
487
+ return R.path(['data'], await axios({
488
+ method: 'get',
489
+ url,
490
+ headers,
491
+ }));
492
+ }
493
+ if (!isNilOrEmpty(travelDateStart)) {
494
+ const localDateStart = moment(travelDateStart, dateFormat).format();
495
+ const localDateEnd = moment(travelDateEnd, dateFormat).format();
496
+ url = `${endpoint || this.endpoint}/bookings?localDateStart=${encodeURIComponent(localDateStart)}&localDateEnd=${encodeURIComponent(localDateEnd)}`;
497
+ return R.path(['data'], await axios({
498
+ method: 'get',
499
+ url,
500
+ headers,
501
+ }));
502
+ }
503
+ return [];
504
+ })();
505
+ return ({ bookings: R.unnest(bookings).map(doMapCurry(bookingMap)) });
506
+ }
507
+ }
508
+
509
+ const pickUnit = (units, paxs) => {
510
+ const evalOne = (unit, pax) => {
511
+ if (pax.age < R.path(['restrictions', 'minAge'], unit)) {
512
+ return false;
513
+ }
514
+ if (pax.age > R.path(['restrictions', 'maxAge'], unit)) {
515
+ return false;
516
+ }
517
+ return true;
518
+ };
519
+ if (paxs.length > 1) { // find group units
520
+ const group = units.filter(({ restrictions }) => Boolean(restrictions)).find(unit => {
521
+ if (
522
+ R.path(['restrictions', 'paxCount'], unit) === paxs.length
523
+ && paxs.every(pax => evalOne(unit, pax))
524
+ ) return true;
525
+ return false;
526
+ });
527
+ if (group) return [group];
528
+ }
529
+ return paxs.map(pax => units
530
+ .filter(unit => R.path(['restrictions', 'paxCount'], unit) === 1)
531
+ .find(unit => evalOne(unit, pax))); // individual units
532
+ };
533
+
534
+ module.exports = Plugin;
535
+ module.exports.pickUnit = pickUnit;