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.
- package/.github/workflows/nodejs.yml +6 -0
- package/index.js +147 -322
- package/index.test.js +42 -39
- package/package.json +5 -2
- package/resolvers/availability.js +61 -0
- package/resolvers/booking.js +79 -0
- package/resolvers/product.js +48 -0
- package/resolvers/rate.js +34 -0
|
@@ -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
|
|
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(
|
|
15
|
+
curlirize(axiosRaw);
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
},
|
|
328
|
-
payload: {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
394
|
-
'mismatched options/
|
|
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
|
-
|
|
420
|
-
await Promise.map(
|
|
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
|
|
424
|
-
optionId: optionIds[
|
|
230
|
+
productId,
|
|
231
|
+
optionId: optionIds[ix],
|
|
425
232
|
localDateStart,
|
|
426
233
|
localDateEnd,
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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 ===
|
|
487
|
-
'mismatched options/
|
|
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
|
-
|
|
513
|
-
await Promise.map(
|
|
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
|
|
517
|
-
optionId: optionIds[
|
|
306
|
+
productId,
|
|
307
|
+
optionId: optionIds[ix],
|
|
518
308
|
localDateStart,
|
|
519
309
|
localDateEnd,
|
|
520
|
-
|
|
521
|
-
units:
|
|
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
|
-
|
|
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), '
|
|
559
|
-
assert(R.path(['surname'], holder), '
|
|
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
|
-
|
|
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
|
-
...
|
|
367
|
+
...dataForCreateBooking,
|
|
576
368
|
notes,
|
|
577
369
|
},
|
|
578
370
|
headers,
|
|
579
371
|
}));
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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 ({
|
|
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 ({
|
|
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
|
-
'
|
|
167
|
+
'3465143f-4902-447a-9c1e-8e5598666663',
|
|
149
168
|
],
|
|
150
|
-
optionIds: ['DEFAULT', '
|
|
151
|
-
|
|
152
|
-
[{
|
|
153
|
-
[{
|
|
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
|
-
|
|
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
|
-
'
|
|
194
|
+
'3465143f-4902-447a-9c1e-8e5598666663',
|
|
199
195
|
],
|
|
200
|
-
optionIds: ['DEFAULT', '
|
|
201
|
-
|
|
202
|
-
[{
|
|
203
|
-
[{
|
|
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.
|
|
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
|
+
};
|