ti2-tourplan 1.0.11 → 1.0.13
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/index.js +534 -55
- package/index.test.js +5 -0
- package/package.json +4 -2
- package/resolvers/product.js +123 -0
package/index.js
CHANGED
|
@@ -1,65 +1,46 @@
|
|
|
1
|
+
// const axios = require('axios');
|
|
2
|
+
const Promise = require('bluebird');
|
|
1
3
|
const R = require('ramda');
|
|
2
4
|
const assert = require('assert');
|
|
3
5
|
const moment = require('moment');
|
|
4
6
|
const js2xmlparser = require('js2xmlparser');
|
|
5
7
|
const xml2js = require('xml2js');
|
|
6
|
-
const
|
|
8
|
+
const { translateTPOption } = require('./resolvers/product');
|
|
7
9
|
|
|
8
10
|
const Normalizer = require('./normalizer');
|
|
9
11
|
|
|
10
12
|
const xmlParser = new xml2js.Parser();
|
|
11
13
|
|
|
12
|
-
const
|
|
13
|
-
prettyPrinting: {
|
|
14
|
-
enabled: false,
|
|
15
|
-
},
|
|
14
|
+
const defaultXmlOptions = {
|
|
15
|
+
prettyPrinting: { enabled: false },
|
|
16
16
|
dtd: {
|
|
17
17
|
include: true,
|
|
18
18
|
name: 'tourConnect_4_00_000.dtd',
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
|
+
const hostConnectXmlOptions = {
|
|
22
|
+
prettyPrinting: { enabled: false },
|
|
23
|
+
dtd: {
|
|
24
|
+
include: true,
|
|
25
|
+
name: 'hostConnect_4_06_009.dtd',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
21
28
|
|
|
22
|
-
const getHeaders = ({ length
|
|
29
|
+
const getHeaders = ({ length }) => ({
|
|
23
30
|
Accept: 'application/xml',
|
|
24
31
|
'Content-Type': 'application/xml; charset=utf-8',
|
|
25
32
|
'Content-Length': length,
|
|
26
|
-
...requestId ? { requestId } : {},
|
|
27
33
|
});
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
retVal.request = axiosSafeRequest(retVal.request);
|
|
33
|
-
return retVal;
|
|
34
|
+
const wildcardMatch = (wildcard, str) => {
|
|
35
|
+
const w = wildcard.replace(/[.+^${}()|[\]\\]/g, '\\$&'); // regexp escape
|
|
36
|
+
const re = new RegExp(`^${w.replace(/\*/g, '.*').replace(/\?/g, '.')}$`, 'i');
|
|
37
|
+
return re.test(str); // remove last 'i' above to have case sensitive
|
|
34
38
|
};
|
|
35
|
-
|
|
36
39
|
class Plugin {
|
|
37
40
|
constructor(params = {}) { // we get the env variables from here
|
|
38
41
|
Object.entries(params).forEach(([attr, value]) => {
|
|
39
42
|
this[attr] = value;
|
|
40
43
|
});
|
|
41
|
-
if (this.events) {
|
|
42
|
-
axiosRaw.interceptors.request.use(request => {
|
|
43
|
-
this.events.emit(`${this.name}.axios.request`, axiosSafeRequest(request));
|
|
44
|
-
return request;
|
|
45
|
-
});
|
|
46
|
-
axiosRaw.interceptors.response.use(response => {
|
|
47
|
-
this.events.emit(`${this.name}.axios.response`, axiosSafeResponse(response));
|
|
48
|
-
return response;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
const pluginObj = this;
|
|
52
|
-
this.axios = async (...args) => axiosRaw(...args).catch(err => {
|
|
53
|
-
const errMsg = R.omit(['config'], err.toJSON());
|
|
54
|
-
console.log(`error in ${this.name}`, args[0], errMsg);
|
|
55
|
-
if (pluginObj.events) {
|
|
56
|
-
pluginObj.events.emit(`${this.name}.axios.error`, {
|
|
57
|
-
request: args[0],
|
|
58
|
-
err: errMsg,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
throw err;
|
|
62
|
-
});
|
|
63
44
|
this.tokenTemplate = () => ({
|
|
64
45
|
endpoint: {
|
|
65
46
|
type: 'text',
|
|
@@ -78,37 +59,82 @@ class Plugin {
|
|
|
78
59
|
description: 'The tourplan provided password',
|
|
79
60
|
default: 'en',
|
|
80
61
|
},
|
|
62
|
+
hostConnectEndpoint: {
|
|
63
|
+
type: 'text',
|
|
64
|
+
regExp: /.+/,
|
|
65
|
+
},
|
|
66
|
+
hostConnectAgentID: {
|
|
67
|
+
type: 'text',
|
|
68
|
+
regExp: /.+/,
|
|
69
|
+
},
|
|
70
|
+
hostConnectAgentPassword: {
|
|
71
|
+
type: 'text',
|
|
72
|
+
regExp: /.+/,
|
|
73
|
+
},
|
|
81
74
|
});
|
|
75
|
+
this.callTourplan = async ({
|
|
76
|
+
model,
|
|
77
|
+
endpoint,
|
|
78
|
+
axios,
|
|
79
|
+
xmlOptions,
|
|
80
|
+
}) => {
|
|
81
|
+
let data = Normalizer.stripEnclosingQuotes(
|
|
82
|
+
js2xmlparser.parse('Request', model, xmlOptions),
|
|
83
|
+
);
|
|
84
|
+
data = data.replace(xmlOptions.dtd.name, `Request SYSTEM "${xmlOptions.dtd.name}"`);
|
|
85
|
+
const reply = R.path(['data'], await axios({
|
|
86
|
+
method: 'post',
|
|
87
|
+
url: endpoint,
|
|
88
|
+
data,
|
|
89
|
+
headers: getHeaders({ length: data.length }),
|
|
90
|
+
}));
|
|
91
|
+
const replyObj = await xmlParser.parseStringPromise(reply);
|
|
92
|
+
if (R.path(['Reply', 'ErrorReply', 0, 'Error', 0], replyObj)) {
|
|
93
|
+
throw new Error(R.path(['Reply', 'ErrorReply', 0, 'Error', 0], replyObj));
|
|
94
|
+
}
|
|
95
|
+
return R.path(['Reply'], replyObj);
|
|
96
|
+
};
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
async validateToken({
|
|
100
|
+
axios,
|
|
85
101
|
token: {
|
|
86
102
|
endpoint,
|
|
87
103
|
username,
|
|
88
104
|
password,
|
|
105
|
+
hostConnectEndpoint,
|
|
106
|
+
hostConnectAgentID,
|
|
107
|
+
hostConnectAgentPassword,
|
|
89
108
|
},
|
|
90
|
-
requestId,
|
|
91
109
|
}) {
|
|
92
110
|
try {
|
|
111
|
+
if (hostConnectEndpoint) {
|
|
112
|
+
assert(hostConnectAgentID && hostConnectAgentPassword);
|
|
113
|
+
const model = {
|
|
114
|
+
AgentInfoRequest: {
|
|
115
|
+
AgentID: hostConnectAgentID,
|
|
116
|
+
Password: hostConnectAgentPassword,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const replyObj = await this.callTourplan({
|
|
120
|
+
model,
|
|
121
|
+
endpoint: hostConnectEndpoint,
|
|
122
|
+
axios,
|
|
123
|
+
xmlOptions: hostConnectXmlOptions,
|
|
124
|
+
});
|
|
125
|
+
assert(R.path(['AgentInfoReply', 0, 'Currency'], replyObj));
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
93
128
|
const model = {
|
|
94
129
|
AuthenticationRequest: {
|
|
95
130
|
Login: username,
|
|
96
131
|
Password: password,
|
|
97
132
|
},
|
|
98
133
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const reply = R.path(['data'], await this.axios({
|
|
104
|
-
metod: 'post',
|
|
105
|
-
url: endpoint,
|
|
106
|
-
data,
|
|
107
|
-
headers: getHeaders({ length: data.length }),
|
|
108
|
-
requestId,
|
|
109
|
-
}));
|
|
110
|
-
const replyObj = await xmlParser.parseStringPromise(reply);
|
|
111
|
-
assert(R.path(['Reply', 'AuthenticationReply', 0], replyObj) === '');
|
|
134
|
+
const replyObj = await this.callTourplan({
|
|
135
|
+
model, endpoint, axios, xmlOptions: defaultXmlOptions,
|
|
136
|
+
});
|
|
137
|
+
assert(R.path(['AuthenticationReply', 0], replyObj) === '');
|
|
112
138
|
return true;
|
|
113
139
|
} catch (err) {
|
|
114
140
|
return false;
|
|
@@ -116,6 +142,7 @@ class Plugin {
|
|
|
116
142
|
}
|
|
117
143
|
|
|
118
144
|
async queryAllotment({
|
|
145
|
+
axios,
|
|
119
146
|
token: {
|
|
120
147
|
endpoint = this.endpoint,
|
|
121
148
|
username = this.username,
|
|
@@ -128,7 +155,6 @@ class Plugin {
|
|
|
128
155
|
keyPath,
|
|
129
156
|
appliesTo: appliesToFilter,
|
|
130
157
|
},
|
|
131
|
-
requestId,
|
|
132
158
|
}) {
|
|
133
159
|
const verbose = R.path(['verbose'], this);
|
|
134
160
|
const cleanLog = inputString =>
|
|
@@ -157,15 +183,15 @@ class Plugin {
|
|
|
157
183
|
},
|
|
158
184
|
};
|
|
159
185
|
let data = Normalizer.stripEnclosingQuotes(
|
|
160
|
-
js2xmlparser.parse('Request', model,
|
|
186
|
+
js2xmlparser.parse('Request', model, defaultXmlOptions),
|
|
161
187
|
);
|
|
162
|
-
data = data.replace(
|
|
188
|
+
data = data.replace(defaultXmlOptions.dtd.name, `Request SYSTEM "${defaultXmlOptions.dtd.name}"`);
|
|
163
189
|
if (verbose) console.log('request', cleanLog(data));
|
|
164
|
-
const reply = R.path(['data'], await
|
|
190
|
+
const reply = R.path(['data'], await axios({
|
|
165
191
|
metod: 'post',
|
|
166
192
|
url: endpoint,
|
|
167
193
|
data,
|
|
168
|
-
headers: getHeaders({ length: data.length
|
|
194
|
+
headers: getHeaders({ length: data.length }),
|
|
169
195
|
}));
|
|
170
196
|
if (verbose) console.log('reply', cleanLog(reply));
|
|
171
197
|
const returnObj = await xmlParser.parseStringPromise(reply);
|
|
@@ -225,6 +251,459 @@ class Plugin {
|
|
|
225
251
|
})(),
|
|
226
252
|
};
|
|
227
253
|
}
|
|
254
|
+
|
|
255
|
+
async searchProducts({
|
|
256
|
+
axios,
|
|
257
|
+
typeDefsAndQueries: {
|
|
258
|
+
productTypeDefs,
|
|
259
|
+
productQuery,
|
|
260
|
+
},
|
|
261
|
+
token: {
|
|
262
|
+
hostConnectEndpoint,
|
|
263
|
+
hostConnectAgentID,
|
|
264
|
+
hostConnectAgentPassword,
|
|
265
|
+
},
|
|
266
|
+
payload: { optionId, productName },
|
|
267
|
+
}) {
|
|
268
|
+
const model = {
|
|
269
|
+
OptionInfoRequest: {
|
|
270
|
+
Opt: optionId || '?????????????????',
|
|
271
|
+
Info: 'G',
|
|
272
|
+
AgentID: hostConnectAgentID,
|
|
273
|
+
Password: hostConnectAgentPassword,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
const payload = {
|
|
277
|
+
model,
|
|
278
|
+
endpoint: hostConnectEndpoint,
|
|
279
|
+
axios,
|
|
280
|
+
xmlOptions: hostConnectXmlOptions,
|
|
281
|
+
};
|
|
282
|
+
// use cache if we are getting the full list
|
|
283
|
+
const replyObj = optionId
|
|
284
|
+
? await this.callTourplan(payload)
|
|
285
|
+
: await this.cache.getOrExec({
|
|
286
|
+
fnParams: [model],
|
|
287
|
+
fn: () => this.callTourplan(payload),
|
|
288
|
+
ttl: 60 * 60 * 12, // 12 hours
|
|
289
|
+
forceRefresh: false,
|
|
290
|
+
});
|
|
291
|
+
const productFields = [{
|
|
292
|
+
id: 'productId',
|
|
293
|
+
title: 'Supplier',
|
|
294
|
+
type: 'extended-option',
|
|
295
|
+
}, {
|
|
296
|
+
id: 'optionId',
|
|
297
|
+
title: 'Service',
|
|
298
|
+
type: 'extended-option',
|
|
299
|
+
}, {
|
|
300
|
+
id: 'startDate',
|
|
301
|
+
title: 'Date',
|
|
302
|
+
type: 'date',
|
|
303
|
+
}, {
|
|
304
|
+
id: 'paxConfigs',
|
|
305
|
+
title: 'Pax',
|
|
306
|
+
type: 'list_of_fields',
|
|
307
|
+
fields: [{
|
|
308
|
+
id: 'adults',
|
|
309
|
+
title: 'Adults',
|
|
310
|
+
type: 'count',
|
|
311
|
+
}, {
|
|
312
|
+
id: 'children',
|
|
313
|
+
title: 'Children',
|
|
314
|
+
type: 'count',
|
|
315
|
+
}, {
|
|
316
|
+
id: 'infants',
|
|
317
|
+
title: 'Infants',
|
|
318
|
+
type: 'count',
|
|
319
|
+
}, {
|
|
320
|
+
id: 'roomType',
|
|
321
|
+
title: 'Room Type',
|
|
322
|
+
type: 'extended-option',
|
|
323
|
+
options: [{ value: 'SG', label: 'Single' }, { value: 'DB', label: 'Double' }, { value: 'TW', label: 'Twin' }, { value: 'QD', label: 'Quad' }],
|
|
324
|
+
}],
|
|
325
|
+
requiredForAvailability: true,
|
|
326
|
+
requiredForCalendar: true,
|
|
327
|
+
requiredForBooking: true,
|
|
328
|
+
}, {
|
|
329
|
+
id: 'chargeUnitQuanity', // secondary charge unit (SCU) quantity
|
|
330
|
+
description: 'number of nights or days or hours depending on charge unit',
|
|
331
|
+
title: 'Quantity',
|
|
332
|
+
type: 'count',
|
|
333
|
+
}];
|
|
334
|
+
const products = R.call(R.compose(
|
|
335
|
+
R.map(optionsGroupedBySupplierId => {
|
|
336
|
+
const supplierData = {
|
|
337
|
+
supplierId: R.path([0, 'OptGeneral', 0, 'SupplierId', 0], optionsGroupedBySupplierId),
|
|
338
|
+
supplierName: R.path([0, 'OptGeneral', 0, 'SupplierName', 0], optionsGroupedBySupplierId),
|
|
339
|
+
};
|
|
340
|
+
return translateTPOption({
|
|
341
|
+
supplierData,
|
|
342
|
+
optionsGroupedBySupplierId,
|
|
343
|
+
typeDefs: productTypeDefs,
|
|
344
|
+
query: productQuery,
|
|
345
|
+
});
|
|
346
|
+
}),
|
|
347
|
+
R.values,
|
|
348
|
+
R.groupBy(R.path(['OptGeneral', 0, 'SupplierId', 0])),
|
|
349
|
+
productName ? R.filter(o => {
|
|
350
|
+
const str = `${
|
|
351
|
+
R.path(['OptGeneral', 0, 'Description', 0], o)
|
|
352
|
+
} ${
|
|
353
|
+
R.path(['Opt', 0], o)
|
|
354
|
+
} ${
|
|
355
|
+
R.path(['OptGeneral', 0, 'SupplierId', 0], o)
|
|
356
|
+
} ${
|
|
357
|
+
R.path(['OptGeneral', 0, 'SupplierName', 0], o)
|
|
358
|
+
}`;
|
|
359
|
+
return wildcardMatch(productName, str);
|
|
360
|
+
}) : R.identity,
|
|
361
|
+
R.pathOr([], ['OptionInfoReply', 0, 'Option']),
|
|
362
|
+
), replyObj);
|
|
363
|
+
return {
|
|
364
|
+
products,
|
|
365
|
+
productFields,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async getProductPackages() {
|
|
370
|
+
return [
|
|
371
|
+
{
|
|
372
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8fea-0242ac120002',
|
|
373
|
+
packageName: "Afternoon at St Paul's Cathedral and Kensington Palace (half day)",
|
|
374
|
+
items: [],
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8feb-0242ac120002',
|
|
378
|
+
packageName: 'Beefeater Day Out',
|
|
379
|
+
items: [],
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8fec-0242ac120002',
|
|
383
|
+
packageName: 'Brighton and Hove tour',
|
|
384
|
+
items: [],
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8fed-0242ac120002',
|
|
388
|
+
packageName: 'Buckingham Palace & Kensington Palace (full day)',
|
|
389
|
+
items: [],
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8fee-0242ac120002',
|
|
393
|
+
packageName: 'Buckingham Palace and Hampton Court Palace (full day)',
|
|
394
|
+
items: [],
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8fef-0242ac120002',
|
|
398
|
+
packageName: 'Buckingham Palace and Windsor Castle (full day)',
|
|
399
|
+
items: [],
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff0-0242ac120002',
|
|
403
|
+
packageName: 'Canterbury & Dover day trip',
|
|
404
|
+
items: [],
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff1-0242ac120002',
|
|
408
|
+
packageName: 'Canterbury day trip',
|
|
409
|
+
items: [],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff2-0242ac120002',
|
|
413
|
+
packageName: 'Churchill War Rooms and the Imperial War Museum',
|
|
414
|
+
items: [],
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff3-0242ac120002',
|
|
418
|
+
packageName: 'Churchill War Rooms, the Changing of the Guards & the Tower of London',
|
|
419
|
+
items: [],
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff4-0242ac120002',
|
|
423
|
+
packageName: "City walking tour, St Paul's Cathedral and theatre performance at the Globe",
|
|
424
|
+
items: [],
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff5-0242ac120002',
|
|
428
|
+
packageName: 'Downton Abbey day trip',
|
|
429
|
+
items: [],
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff6-0242ac120002',
|
|
433
|
+
packageName: "Guided tour of St Paul's Cathedral",
|
|
434
|
+
items: [],
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff7-0242ac120002',
|
|
438
|
+
packageName: 'Hampton Court Palace (half day)',
|
|
439
|
+
items: [],
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff8-0242ac120002',
|
|
443
|
+
packageName: 'Hampton Court Palace and Windsor Castle (full day)',
|
|
444
|
+
items: [],
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ff9-0242ac120002',
|
|
448
|
+
packageName: 'Kensington Palace and the Tower of London (full day)',
|
|
449
|
+
items: [
|
|
450
|
+
{
|
|
451
|
+
itemId: 'LONGUACLUXELONFDG',
|
|
452
|
+
itemType: 'option',
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
itemId: 'LONEFKENPALFIKENI',
|
|
456
|
+
itemType: 'option',
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
itemId: 'LONEFTOWLONFITOWL',
|
|
460
|
+
itemType: 'option',
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ffa-0242ac120002',
|
|
466
|
+
packageName: 'Kensington Palace and Westminster Abbey (half day)',
|
|
467
|
+
items: [],
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ffb-0242ac120002',
|
|
471
|
+
packageName: "Late afternoon visit to St Paul's Cathedral",
|
|
472
|
+
items: [],
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ffc-0242ac120002',
|
|
476
|
+
packageName: "St Paul's Cathedral & Kensington Palace (half day)",
|
|
477
|
+
items: [],
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ffd-0242ac120002',
|
|
481
|
+
packageName: "St Paul's Cathedral and the Churchill War Rooms (half day)",
|
|
482
|
+
items: [],
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8ffe-0242ac120002',
|
|
486
|
+
packageName: "St Paul's Cathedral and Westminster Abbey (half day)",
|
|
487
|
+
items: [
|
|
488
|
+
{
|
|
489
|
+
itemId: 'LONGUACLUXELONHDG',
|
|
490
|
+
itemType: 'option',
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
itemId: 'LONEFSTPCATFITSPC',
|
|
494
|
+
itemType: 'option',
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
itemId: 'LONSSGOLTOUGTWAP',
|
|
498
|
+
itemType: 'option',
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
packageId: 'a1b2c3d4-e8e8-11ec-8fff-0242ac120002',
|
|
504
|
+
packageName: "St Paul's Cathedral, Changing of the Guard and Tower of London (full day)",
|
|
505
|
+
items: [
|
|
506
|
+
{
|
|
507
|
+
itemId: 'LONGUACLUXELONFDG',
|
|
508
|
+
itemType: 'option',
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
itemId: 'LONEFTOWLONFITOWL',
|
|
512
|
+
itemType: 'option',
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
itemId: 'LONEFGOLTOUGT105N',
|
|
516
|
+
itemType: 'option',
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9000-0242ac120002',
|
|
522
|
+
packageName: "St Paul's Cathedral, Kensington Palace and Westminster Abbey (full day)",
|
|
523
|
+
items: [],
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9001-0242ac120002',
|
|
527
|
+
packageName: 'St Pauls and Tower of London',
|
|
528
|
+
items: [],
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9002-0242ac120002',
|
|
532
|
+
packageName: 'Stonehenge (Full Day from London)',
|
|
533
|
+
items: [],
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9003-0242ac120002',
|
|
537
|
+
packageName: 'Stonehenge, Lacock and Avebury (full day)',
|
|
538
|
+
items: [],
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9004-0242ac120002',
|
|
542
|
+
packageName: 'The White Cliffs of Dover, Dover Castle, & military tunnels (full day)',
|
|
543
|
+
items: [],
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9005-0242ac120002',
|
|
547
|
+
packageName: 'Tower of London by boat',
|
|
548
|
+
items: [
|
|
549
|
+
{
|
|
550
|
+
itemId: 'LONGUACLUXELONHDG',
|
|
551
|
+
itemType: 'option',
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
itemId: 'LONSSTHACLISINLON',
|
|
555
|
+
itemType: 'option',
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
itemId: 'LONEFTOWLONFITOWL',
|
|
559
|
+
itemType: 'option',
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9006-0242ac120002',
|
|
565
|
+
packageName: 'Warner Bros.- The Making of Harry Potter (non refundable)',
|
|
566
|
+
items: [],
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9007-0242ac120002',
|
|
570
|
+
packageName: 'Westminster Abbey and Buckingham Palace (half day)',
|
|
571
|
+
items: [],
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9008-0242ac120002',
|
|
575
|
+
packageName: 'Westminster Abbey and Changing of the Guard',
|
|
576
|
+
items: [
|
|
577
|
+
{
|
|
578
|
+
itemId: 'LONGUACLUXELONHDG',
|
|
579
|
+
itemType: 'option',
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
itemId: 'LONEFWESABBWESABB',
|
|
583
|
+
itemType: 'option',
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
packageId: 'a1b2c3d4-e8e8-11ec-9009-0242ac120002',
|
|
589
|
+
packageName: 'Westminster Abbey and the Tower by boat (full day)',
|
|
590
|
+
items: [],
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
packageId: 'a1b2c3d4-e8e8-11ec-900a-0242ac120002',
|
|
594
|
+
packageName: 'Westminster Abbey, Changing of the Guard & Kensington Palace (full day)',
|
|
595
|
+
items: [],
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
packageId: 'a1b2c3d4-e8e8-11ec-900b-0242ac120002',
|
|
599
|
+
packageName: 'Westminster Abbey, Changing of the Guard and Buckingham Palace',
|
|
600
|
+
items: [],
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
packageId: 'a1b2c3d4-e8e8-11ec-900c-0242ac120002',
|
|
604
|
+
packageName: 'Westminster and the Tower of London (half day)',
|
|
605
|
+
items: [],
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
packageId: 'a1b2c3d4-e8e8-11ec-900d-0242ac120002',
|
|
609
|
+
packageName: 'Westminster, Changing of the Guard and the Tower of London (full day)',
|
|
610
|
+
items: [],
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
packageId: 'a1b2c3d4-e8e8-11ec-900e-0242ac120002',
|
|
614
|
+
packageName: 'Windsor Castle (half day)',
|
|
615
|
+
items: [],
|
|
616
|
+
},
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async searchQuote({
|
|
621
|
+
axios,
|
|
622
|
+
token: {
|
|
623
|
+
hostConnectEndpoint,
|
|
624
|
+
hostConnectAgentID,
|
|
625
|
+
hostConnectAgentPassword,
|
|
626
|
+
},
|
|
627
|
+
payload: {
|
|
628
|
+
quoteName,
|
|
629
|
+
quoteId,
|
|
630
|
+
optionId,
|
|
631
|
+
startDate,
|
|
632
|
+
reference,
|
|
633
|
+
/*
|
|
634
|
+
paxConfigs: [{ roomType: 'DB', adults: 2 }, { roomType: 'TW', children: 2 }]
|
|
635
|
+
*/
|
|
636
|
+
paxConfigs,
|
|
637
|
+
/*
|
|
638
|
+
The number of second charge units required (second charge units are discussed
|
|
639
|
+
in the OptionInfo section). Should only be specified for options that have SCUs.
|
|
640
|
+
Defaults to 1.
|
|
641
|
+
*/
|
|
642
|
+
chargeUnitQuanity,
|
|
643
|
+
},
|
|
644
|
+
}) {
|
|
645
|
+
const model = {
|
|
646
|
+
AddServiceRequest: {
|
|
647
|
+
AgentID: hostConnectAgentID,
|
|
648
|
+
Password: hostConnectAgentPassword,
|
|
649
|
+
...(quoteId ? {
|
|
650
|
+
ExistingBookingInfo: { BookingId: quoteId },
|
|
651
|
+
} : {
|
|
652
|
+
NewBookingInfo: { Name: quoteName, QB: 'Q' },
|
|
653
|
+
}),
|
|
654
|
+
Opt: optionId,
|
|
655
|
+
DateFrom: startDate,
|
|
656
|
+
RateId: 'Default',
|
|
657
|
+
SCUqty: chargeUnitQuanity || 1,
|
|
658
|
+
AgentRef: reference,
|
|
659
|
+
RoomConfigs: paxConfigs.map(obj => ({
|
|
660
|
+
RoomConfig: {
|
|
661
|
+
Adults: obj.adults || 0,
|
|
662
|
+
Children: obj.children || 0,
|
|
663
|
+
Infants: obj.infants || 0,
|
|
664
|
+
RoomType: ({
|
|
665
|
+
Single: 'SG',
|
|
666
|
+
Double: 'DB',
|
|
667
|
+
Twin: 'TW',
|
|
668
|
+
Triple: 'TR',
|
|
669
|
+
Quad: 'QU',
|
|
670
|
+
})[obj.roomType],
|
|
671
|
+
},
|
|
672
|
+
})),
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
const replyObj = await this.callTourplan({
|
|
676
|
+
model,
|
|
677
|
+
endpoint: hostConnectEndpoint,
|
|
678
|
+
axios,
|
|
679
|
+
xmlOptions: hostConnectXmlOptions,
|
|
680
|
+
});
|
|
681
|
+
console.log('replyObj', replyObj);
|
|
682
|
+
return {
|
|
683
|
+
message: R.path(['AddServiceReply', 0, 'Status', 0], replyObj)
|
|
684
|
+
=== 'NO' ? 'Service cannot be added to quote' : '',
|
|
685
|
+
quote: {
|
|
686
|
+
id: R.path(['AddServiceReply', 0, 'BookingId', 0], replyObj),
|
|
687
|
+
reference: R.path(['AddServiceReply', 0, 'Ref', 0], replyObj),
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// eslint-disable-next-line class-methods-use-this
|
|
693
|
+
async getCreateBookingFields({
|
|
694
|
+
// token: {
|
|
695
|
+
// hostConnectAgentID,
|
|
696
|
+
// hostConnectAgentPassword,
|
|
697
|
+
// hostConnectEndpoint,
|
|
698
|
+
// },
|
|
699
|
+
// axios,
|
|
700
|
+
}) {
|
|
701
|
+
const customFields = [];
|
|
702
|
+
return {
|
|
703
|
+
fields: [],
|
|
704
|
+
customFields,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
228
707
|
}
|
|
229
708
|
|
|
230
709
|
module.exports = Plugin;
|
package/index.test.js
CHANGED
|
@@ -44,6 +44,7 @@ describe('search tests', () => {
|
|
|
44
44
|
it('valid token', async () => {
|
|
45
45
|
axios.mockImplementation(getFixture);
|
|
46
46
|
const retVal = await app.validateToken({
|
|
47
|
+
axios,
|
|
47
48
|
token,
|
|
48
49
|
});
|
|
49
50
|
expect(retVal).toBeTruthy();
|
|
@@ -51,6 +52,7 @@ describe('search tests', () => {
|
|
|
51
52
|
it('invalid token', async () => {
|
|
52
53
|
axios.mockImplementation(getFixture);
|
|
53
54
|
const retVal = await app.validateToken({
|
|
55
|
+
axios,
|
|
54
56
|
token: { ...token, username: 'somerandom' },
|
|
55
57
|
});
|
|
56
58
|
expect(retVal).toBeFalsy();
|
|
@@ -85,6 +87,7 @@ describe('search tests', () => {
|
|
|
85
87
|
it('read allotment empty', async () => {
|
|
86
88
|
const request = axios.mockImplementation(getFixture);
|
|
87
89
|
const retVal = await app.queryAllotment({
|
|
90
|
+
axios,
|
|
88
91
|
token,
|
|
89
92
|
payload: {
|
|
90
93
|
dateFormat,
|
|
@@ -107,6 +110,7 @@ describe('search tests', () => {
|
|
|
107
110
|
it('read allotment not empty', async () => {
|
|
108
111
|
const request = axios.mockImplementation(getFixture);
|
|
109
112
|
const retVal = await app.queryAllotment({
|
|
113
|
+
axios,
|
|
110
114
|
token,
|
|
111
115
|
payload: {
|
|
112
116
|
dateFormat,
|
|
@@ -142,6 +146,7 @@ describe('search tests', () => {
|
|
|
142
146
|
it('read allotment that applies to more products', async () => {
|
|
143
147
|
const request = axios.mockImplementation(getFixture);
|
|
144
148
|
const retVal = await app.queryAllotment({
|
|
149
|
+
axios,
|
|
145
150
|
token,
|
|
146
151
|
payload: {
|
|
147
152
|
dateFormat,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ti2-tourplan",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Tourplan's TI2 Plugin",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"testTimeout": 10000
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"
|
|
30
|
+
"@graphql-tools/schema": "^8.5.1",
|
|
31
|
+
"graphql": "^16.5.0",
|
|
31
32
|
"bluebird": "^3.7.2",
|
|
32
33
|
"js2xmlparser": "^4.0.2",
|
|
33
34
|
"jsonwebtoken": "^8.5.1",
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"xml2js": "^0.4.23"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
41
|
+
"axios": "^0.27.2",
|
|
40
42
|
"eslint": "^8.4.1",
|
|
41
43
|
"eslint-config-airbnb": "^19.0.2",
|
|
42
44
|
"eslint-plugin-import": "^2.25.4",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/* eslint-disable arrow-body-style */
|
|
2
|
+
const { makeExecutableSchema } = require('@graphql-tools/schema');
|
|
3
|
+
const R = require('ramda');
|
|
4
|
+
const { graphql } = require('graphql');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const translateTCMLCollection = async ({ rootValue, typeDefs, query }) => {
|
|
8
|
+
const schema = makeExecutableSchema({
|
|
9
|
+
typeDefs,
|
|
10
|
+
resolvers: {
|
|
11
|
+
Query: {
|
|
12
|
+
productId: R.path(['elementId']),
|
|
13
|
+
productName: R.path(['elementName']),
|
|
14
|
+
availableCurrencies: () => [],
|
|
15
|
+
defaultCurrency: () => '',
|
|
16
|
+
options: R.pathOr([], ['children']),
|
|
17
|
+
},
|
|
18
|
+
Option: {
|
|
19
|
+
optionId: R.path(['elementId']),
|
|
20
|
+
optionName: R.path(['elementName']),
|
|
21
|
+
units: R.pathOr([], ['children']),
|
|
22
|
+
},
|
|
23
|
+
Unit: {
|
|
24
|
+
unitId: R.path(['elementId']),
|
|
25
|
+
unitName: R.path(['elementName']),
|
|
26
|
+
subtitle: () => '',
|
|
27
|
+
pricing: () => [],
|
|
28
|
+
restrictions: () => ({}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const retVal = await graphql({ schema, rootValue, source: query });
|
|
33
|
+
if (retVal.errors) throw new Error(retVal.errors);
|
|
34
|
+
return retVal.data;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const translateTPOption = ({ optionsGroupedBySupplierId, supplierData, typeDefs, query }) => {
|
|
38
|
+
return {
|
|
39
|
+
productId: supplierData.supplierId,
|
|
40
|
+
productName: supplierData.supplierName,
|
|
41
|
+
options: optionsGroupedBySupplierId.map(option => {
|
|
42
|
+
const keyData = {
|
|
43
|
+
optionId: R.path(['Opt', 0], option),
|
|
44
|
+
optionName: R.path(['OptGeneral', 0, 'Description', 0], option),
|
|
45
|
+
};
|
|
46
|
+
/*
|
|
47
|
+
SType: One character that specifies the service type of the
|
|
48
|
+
option. One of: Y (accommodation), A (apartment), P
|
|
49
|
+
(package), N (non-accommodation). If the service
|
|
50
|
+
type is Y or P then the option is room-based (pricing
|
|
51
|
+
is room based, when a service line is added to a
|
|
52
|
+
booking for this option then a room type must be
|
|
53
|
+
supplied). The difference between Y and P is that
|
|
54
|
+
packages are fixed length (hence a number of
|
|
55
|
+
nights is not specified).
|
|
56
|
+
*/
|
|
57
|
+
const units = (() => {
|
|
58
|
+
if (R.path(['OptGeneral', 0, 'SType', 0], option) === 'N') {
|
|
59
|
+
return [{
|
|
60
|
+
unitId: 'Adults',
|
|
61
|
+
unitName: 'Adults',
|
|
62
|
+
pricing: [],
|
|
63
|
+
restrictions: {
|
|
64
|
+
paxCount: R.path(['OptGeneral', 0, 'MPFCU', 0], option),
|
|
65
|
+
minAge: R.path(['OptGeneral', 0, 'Adult_From', 0], option),
|
|
66
|
+
maxAge: R.path(['OptGeneral', 0, 'Adult_To', 0], option),
|
|
67
|
+
},
|
|
68
|
+
}, R.path(['OptGeneral', 0, 'ChildrenAllowed', 0], option) === 'Y' ? {
|
|
69
|
+
unitId: 'Children',
|
|
70
|
+
unitName: 'Children',
|
|
71
|
+
pricing: [],
|
|
72
|
+
restrictions: {
|
|
73
|
+
minAge: R.path(['OptGeneral', 0, 'Child_From', 0], option),
|
|
74
|
+
maxAge: R.path(['OptGeneral', 0, 'Child_To', 0], option),
|
|
75
|
+
},
|
|
76
|
+
} : null, R.path(['OptGeneral', 0, 'InfantsAllowed', 0], option) === 'Y' ? {
|
|
77
|
+
unitId: 'Infants',
|
|
78
|
+
unitName: 'Infants',
|
|
79
|
+
pricing: [],
|
|
80
|
+
restrictions: {
|
|
81
|
+
minAge: R.path(['OptGeneral', 0, 'Infant_From', 0], option),
|
|
82
|
+
maxAge: R.path(['OptGeneral', 0, 'Infant_To', 0], option),
|
|
83
|
+
},
|
|
84
|
+
} : null].filter(Boolean);
|
|
85
|
+
}
|
|
86
|
+
if (R.path(['OptGeneral', 0, 'SType', 0], option) === 'Y') {
|
|
87
|
+
return [['SG', 'Single'], ['TW', 'Twin'], ['DB', 'Double'], ['QD', 'Quad']]
|
|
88
|
+
.filter(([_, unitName]) => R.path(['OptGeneral', 0, `${unitName}_Avail`, 0], option) === 'Y')
|
|
89
|
+
.map(([unitId, unitName]) => ({
|
|
90
|
+
unitId,
|
|
91
|
+
unitName,
|
|
92
|
+
pricing: [],
|
|
93
|
+
restrictions: {
|
|
94
|
+
paxCount: R.path(['OptGeneral', 0, `${unitName}_Max`, 0], option)
|
|
95
|
+
|| R.path(['OptGeneral', 0, `${unitName}_Ad_Max`, 0], option),
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
return [{
|
|
100
|
+
unitId: keyData.optionId, unitName: keyData.optionName, pricing: [], restrictions: {},
|
|
101
|
+
}];
|
|
102
|
+
})();
|
|
103
|
+
return {
|
|
104
|
+
optionId: keyData.optionId,
|
|
105
|
+
optionName: keyData.optionName,
|
|
106
|
+
units,
|
|
107
|
+
};
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
// const schema = makeExecutableSchema({
|
|
111
|
+
// typeDefs,
|
|
112
|
+
// resolvers: {},
|
|
113
|
+
// });
|
|
114
|
+
// const retVal = await graphql({ schema, rootValue, source: query });
|
|
115
|
+
// if (retVal.errors) throw new Error(retVal.errors);
|
|
116
|
+
// console.log(retVal.data, rootValue);
|
|
117
|
+
// return retVal.data;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
translateTCMLCollection,
|
|
122
|
+
translateTPOption,
|
|
123
|
+
};
|