ti2-tourplan 1.0.138 → 1.0.140
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/__snapshots__/index.test.js.snap +1 -0
- package/index.test.js +199 -0
- package/itinerary-add-service.js +26 -3
- package/itinerary-products-search.js +41 -13
- package/itinerary-search.js +121 -63
- package/itinerary-search.test.js +105 -6
- package/package.json +1 -1
- package/resolvers/product.js +188 -23
- package/resolvers/product.test.js +198 -0
- package/tp-helpers/suppliers.js +72 -0
- package/tp-helpers/suppliers.test.js +53 -0
- package/utils.js +12 -0
- package/utils.test.js +16 -0
- package/.claude/settings.local.json +0 -10
package/index.test.js
CHANGED
|
@@ -36,6 +36,11 @@ jest.mock('axios');
|
|
|
36
36
|
|
|
37
37
|
// Default callTourplan mock: resolve HostConnect replies from __fixtures__.
|
|
38
38
|
const defaultCallTourplanImplementation = async ({ model, endpoint }) => {
|
|
39
|
+
// Supplier notes are optional. Product search must not depend on a fixture
|
|
40
|
+
// for the unfiltered SupplierInfo request.
|
|
41
|
+
if (model.SupplierInfoRequest) {
|
|
42
|
+
return { SupplierInfoReply: { Suppliers: { Supplier: [] } } };
|
|
43
|
+
}
|
|
39
44
|
// Create a mock request object similar to what axios would receive
|
|
40
45
|
const requestType = Object.keys(model)[0]; // Gets 'OptionInfoRequest'
|
|
41
46
|
|
|
@@ -339,6 +344,100 @@ describe('search tests', () => {
|
|
|
339
344
|
expect(request.Remarks).toBe(`Passenger Notes: ${'A'.repeat(43)}`);
|
|
340
345
|
expect(request.Remarks).toHaveLength(60);
|
|
341
346
|
});
|
|
347
|
+
|
|
348
|
+
it.each([
|
|
349
|
+
['an exact boundary reference', { reference: 'A'.repeat(60) }, 'A'.repeat(60)],
|
|
350
|
+
['an overlong reference', { reference: 'B'.repeat(61) }, 'B'.repeat(60)],
|
|
351
|
+
['a long itinerary description', {
|
|
352
|
+
reference: 'A detailed anniversary itinerary with transfers, hotels, touring, and extra arrangements',
|
|
353
|
+
}, 'A detailed anniversary itinerary with transfers, hotels, tou'],
|
|
354
|
+
['the fallback reference', { reference: ' FALLBACK-REFERENCE ' }, 'FALLBACK-REFERENCE'],
|
|
355
|
+
['the preferred agentRef', {
|
|
356
|
+
agentRef: ' AGENT-PICKED ',
|
|
357
|
+
reference: 'REFERENCE-IGNORED',
|
|
358
|
+
}, 'AGENT-PICKED'],
|
|
359
|
+
['a value that expands during sanitization', { agentRef: 'Ä'.repeat(40) }, 'Ae'.repeat(30)],
|
|
360
|
+
['invalid XML characters', { agentRef: ' REF\u0000WITH\u0001CONTROL ' }, 'REFWITHCONTROL'],
|
|
361
|
+
])('normalizes AgentRef from %s without changing the payload', async (
|
|
362
|
+
description,
|
|
363
|
+
sourceFields,
|
|
364
|
+
expected,
|
|
365
|
+
) => {
|
|
366
|
+
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
367
|
+
AddServiceReply: { BookingId: '12345', Ref: 'TESTREF', ServiceLineId: '10' },
|
|
368
|
+
}));
|
|
369
|
+
const payload = {
|
|
370
|
+
quoteName: `Agent reference test: ${description}`,
|
|
371
|
+
optionId: 'ABC123',
|
|
372
|
+
startDate: '2026-07-03',
|
|
373
|
+
paxConfigs: [{ roomType: 'Double', adults: 2 }],
|
|
374
|
+
notes: '',
|
|
375
|
+
...sourceFields,
|
|
376
|
+
};
|
|
377
|
+
const originalPayload = { ...payload };
|
|
378
|
+
|
|
379
|
+
await app.addServiceToItinerary({ axios, token, payload });
|
|
380
|
+
|
|
381
|
+
const agentReference = mockCallTourplan.mock.calls[0][0].model.AddServiceRequest.AgentRef;
|
|
382
|
+
expect(agentReference).toBe(expected);
|
|
383
|
+
expect(Array.from(agentReference).length).toBeLessThanOrEqual(60);
|
|
384
|
+
expect(payload).toEqual(originalPayload);
|
|
385
|
+
|
|
386
|
+
const requestXml = js2xmlparser.parse(
|
|
387
|
+
'Request',
|
|
388
|
+
mockCallTourplan.mock.calls[0][0].model,
|
|
389
|
+
hostConnectXmlOptions,
|
|
390
|
+
);
|
|
391
|
+
expect(requestXml.indexOf('<SCUqty>')).toBeLessThan(requestXml.indexOf('<AgentRef>'));
|
|
392
|
+
expect(requestXml.indexOf('<AgentRef>')).toBeLessThan(requestXml.indexOf('<RoomConfigs>'));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('omits AgentRef when the selected source sanitizes to empty', async () => {
|
|
396
|
+
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
397
|
+
AddServiceReply: { BookingId: '12345', Ref: 'TESTREF', ServiceLineId: '10' },
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
await app.addServiceToItinerary({
|
|
401
|
+
axios,
|
|
402
|
+
token,
|
|
403
|
+
payload: {
|
|
404
|
+
quoteName: 'Empty agent reference',
|
|
405
|
+
optionId: 'ABC123',
|
|
406
|
+
startDate: '2026-07-03',
|
|
407
|
+
agentRef: ' \u0001 ',
|
|
408
|
+
reference: 'REFERENCE-IGNORED',
|
|
409
|
+
paxConfigs: [{ roomType: 'Double', adults: 2 }],
|
|
410
|
+
notes: '',
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(mockCallTourplan.mock.calls[0][0].model.AddServiceRequest.AgentRef).toBeUndefined();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('normalizes a direct-line AgentRef override without changing XML element order', async () => {
|
|
418
|
+
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
419
|
+
AddServiceReply: { BookingId: '12345', Ref: 'TESTREF', ServiceLineId: '10' },
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
await app.addServiceToItinerary({
|
|
423
|
+
axios,
|
|
424
|
+
token,
|
|
425
|
+
payload: {
|
|
426
|
+
quoteName: 'Direct override agent reference',
|
|
427
|
+
optionId: 'ABC123',
|
|
428
|
+
startDate: '2026-07-03',
|
|
429
|
+
reference: 'REFERENCE-IGNORED',
|
|
430
|
+
paxConfigs: [{ roomType: 'Double', adults: 2 }],
|
|
431
|
+
notes: '',
|
|
432
|
+
directLinePayload: { AgentRef: 'D'.repeat(61) },
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const model = mockCallTourplan.mock.calls[0][0].model;
|
|
437
|
+
expect(model.AddServiceRequest.AgentRef).toBe('D'.repeat(60));
|
|
438
|
+
const requestXml = js2xmlparser.parse('Request', model, hostConnectXmlOptions);
|
|
439
|
+
expect(requestXml.indexOf('<AgentRef>')).toBeLessThan(requestXml.indexOf('<RoomConfigs>'));
|
|
440
|
+
});
|
|
342
441
|
});
|
|
343
442
|
|
|
344
443
|
describe('cancelBooking', () => {
|
|
@@ -932,6 +1031,91 @@ describe('search tests', () => {
|
|
|
932
1031
|
expect(retVal).toMatchSnapshot();
|
|
933
1032
|
});
|
|
934
1033
|
|
|
1034
|
+
it('keeps class, charge unit, Other, infant cap, and supplier note on the stock schema', async () => {
|
|
1035
|
+
mockCallTourplan.mockImplementation(async ({ model }) => {
|
|
1036
|
+
if (model.AgentInfoRequest) {
|
|
1037
|
+
return { AgentInfoReply: { Currency: 'GBP' } };
|
|
1038
|
+
}
|
|
1039
|
+
if (model.GetLocationsRequest) {
|
|
1040
|
+
return { GetLocationsReply: { Locations: { Location: [] } } };
|
|
1041
|
+
}
|
|
1042
|
+
if (model.GetServicesRequest) {
|
|
1043
|
+
return { GetServicesReply: { TPLServices: { TPLService: [] } } };
|
|
1044
|
+
}
|
|
1045
|
+
if (model.GetSystemSettingsRequest) {
|
|
1046
|
+
return { GetSystemSettingsReply: { Countries: { Country: [] } } };
|
|
1047
|
+
}
|
|
1048
|
+
if (model.SupplierInfoRequest) {
|
|
1049
|
+
return {
|
|
1050
|
+
SupplierInfoReply: {
|
|
1051
|
+
Suppliers: {
|
|
1052
|
+
Supplier: {
|
|
1053
|
+
SupplierId: '7318',
|
|
1054
|
+
Name: 'Example Hotel',
|
|
1055
|
+
SupplierNotes: {
|
|
1056
|
+
SupplierNote: { NoteText: ' Family owned hotel ' },
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
if (model.OptionInfoRequest) {
|
|
1064
|
+
return {
|
|
1065
|
+
OptionInfoReply: {
|
|
1066
|
+
Option: {
|
|
1067
|
+
Opt: 'LONHOEXAMPLESTD',
|
|
1068
|
+
OptGeneral: {
|
|
1069
|
+
SupplierId: '7318',
|
|
1070
|
+
SupplierName: 'Example Hotel',
|
|
1071
|
+
Description: 'Standard Room',
|
|
1072
|
+
Class: '3*',
|
|
1073
|
+
ClassDescription: '3 Star',
|
|
1074
|
+
SCU: 'Night',
|
|
1075
|
+
SType: 'Y',
|
|
1076
|
+
ButtonName: 'Accommodation',
|
|
1077
|
+
Single_Avail: 'Y',
|
|
1078
|
+
Single_Max: '0',
|
|
1079
|
+
Single_Ad_Max: '2',
|
|
1080
|
+
Single_Max_With_Infants: '1',
|
|
1081
|
+
Other_Avail: 'Y',
|
|
1082
|
+
Other_Max: '4',
|
|
1083
|
+
Other_Ad_Max: '3',
|
|
1084
|
+
Other_Max_With_Infants: '5',
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
return {};
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
const retVal = await app.searchProductsForItinerary({
|
|
1094
|
+
axios,
|
|
1095
|
+
token,
|
|
1096
|
+
typeDefsAndQueries,
|
|
1097
|
+
payload: { optionId: 'LONHOEXAMPLESTD' },
|
|
1098
|
+
});
|
|
1099
|
+
const option = retVal.products[0].options[0];
|
|
1100
|
+
expect(retVal.products[0].description).toBe('Family owned hotel');
|
|
1101
|
+
expect(option.optionClass).toBe('3 Star');
|
|
1102
|
+
expect(option.chargeUnit).toBe('night');
|
|
1103
|
+
expect(option.restrictions.Single.maxPax).toBe(0);
|
|
1104
|
+
expect(option.restrictions.Single.maxPaxWithInfants).toBe(1);
|
|
1105
|
+
expect(option.restrictions.Other).toEqual({
|
|
1106
|
+
allowed: true,
|
|
1107
|
+
maxPax: 4,
|
|
1108
|
+
maxAdults: 3,
|
|
1109
|
+
maxPaxWithInfants: 5,
|
|
1110
|
+
});
|
|
1111
|
+
expect(option.units.map(unit => unit.unitId)).toEqual([
|
|
1112
|
+
'Single', 'Twin', 'Double', 'Triple', 'Quad', 'Other',
|
|
1113
|
+
]);
|
|
1114
|
+
const single = option.units.find(unit => unit.unitId === 'Single');
|
|
1115
|
+
expect(single.restrictions.maxPax).toBe(0);
|
|
1116
|
+
expect(single.restrictions.maxPaxWithInfants).toBe(1);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
935
1119
|
describe('searchProductsForItinerary pickupPoints', () => {
|
|
936
1120
|
const singlePickupPoint = {
|
|
937
1121
|
Point_ID: '504',
|
|
@@ -956,6 +1140,9 @@ describe('search tests', () => {
|
|
|
956
1140
|
if (model.GetSystemSettingsRequest) {
|
|
957
1141
|
return { GetSystemSettingsReply: { Countries: { Country: [] } } };
|
|
958
1142
|
}
|
|
1143
|
+
if (model.SupplierInfoRequest) {
|
|
1144
|
+
return { SupplierInfoReply: { Suppliers: { Supplier: [] } } };
|
|
1145
|
+
}
|
|
959
1146
|
if (model.OptionInfoRequest) {
|
|
960
1147
|
return {
|
|
961
1148
|
OptionInfoReply: {
|
|
@@ -1209,6 +1396,9 @@ describe('search tests', () => {
|
|
|
1209
1396
|
const mockFullCatalog = ({ throwOnOpt } = {}) => {
|
|
1210
1397
|
mockCallTourplan.mockImplementation(async ({ model }) => {
|
|
1211
1398
|
if (model.GetServicesRequest) return getServicesReply;
|
|
1399
|
+
if (model.SupplierInfoRequest) {
|
|
1400
|
+
return { SupplierInfoReply: { Suppliers: { Supplier: [] } } };
|
|
1401
|
+
}
|
|
1212
1402
|
const opt = model.OptionInfoRequest && model.OptionInfoRequest.Opt;
|
|
1213
1403
|
if (throwOnOpt && throwOnOpt.includes(opt)) {
|
|
1214
1404
|
throw new Error(`${opt} OptionInfo should have been skipped`);
|
|
@@ -1278,6 +1468,9 @@ describe('search tests', () => {
|
|
|
1278
1468
|
if (model.GetSystemSettingsRequest) {
|
|
1279
1469
|
return { GetSystemSettingsReply: { Countries: { Country: [] } } };
|
|
1280
1470
|
}
|
|
1471
|
+
if (model.SupplierInfoRequest) {
|
|
1472
|
+
return { SupplierInfoReply: { Suppliers: { Supplier: [] } } };
|
|
1473
|
+
}
|
|
1281
1474
|
if (model.GetServicesRequest) return getServicesReply;
|
|
1282
1475
|
const opt = model.OptionInfoRequest && model.OptionInfoRequest.Opt;
|
|
1283
1476
|
if (optionsByOpt[opt]) {
|
|
@@ -1523,6 +1716,9 @@ describe('search tests', () => {
|
|
|
1523
1716
|
},
|
|
1524
1717
|
};
|
|
1525
1718
|
}
|
|
1719
|
+
if (model.SupplierInfoRequest) {
|
|
1720
|
+
return { SupplierInfoReply: { Suppliers: { Supplier: [] } } };
|
|
1721
|
+
}
|
|
1526
1722
|
const opt = model.OptionInfoRequest && model.OptionInfoRequest.Opt;
|
|
1527
1723
|
if (opt === '???SM????????????') {
|
|
1528
1724
|
return {
|
|
@@ -1605,6 +1801,9 @@ describe('search tests', () => {
|
|
|
1605
1801
|
},
|
|
1606
1802
|
};
|
|
1607
1803
|
}
|
|
1804
|
+
if (model.SupplierInfoRequest) {
|
|
1805
|
+
return { SupplierInfoReply: { Suppliers: { Supplier: [] } } };
|
|
1806
|
+
}
|
|
1608
1807
|
if (model.OptionInfoRequest) {
|
|
1609
1808
|
return {
|
|
1610
1809
|
OptionInfoReply: {
|
package/itinerary-add-service.js
CHANGED
|
@@ -2,6 +2,7 @@ const R = require('ramda');
|
|
|
2
2
|
const {
|
|
3
3
|
getRoomConfigs,
|
|
4
4
|
escapeInvalidXmlChars,
|
|
5
|
+
normalizeAgentReference,
|
|
5
6
|
CUSTOM_RATE_ID_NAME,
|
|
6
7
|
CUSTOM_NO_RATE_NAME,
|
|
7
8
|
hostConnectXmlOptions,
|
|
@@ -15,6 +16,13 @@ const SERVICE_CANNOT_BE_ADDED_ERROR_MESSAGE = 'Service cannot be added to quote
|
|
|
15
16
|
const getBookingName = quoteName => escapeInvalidXmlChars(quoteName)
|
|
16
17
|
.substring(0, MAX_BOOKING_NAME_LENGTH);
|
|
17
18
|
|
|
19
|
+
const buildAgentReference = ({ agentRef, reference }) => {
|
|
20
|
+
const source = typeof agentRef === 'string' && agentRef.trim() !== ''
|
|
21
|
+
? agentRef
|
|
22
|
+
: reference;
|
|
23
|
+
return normalizeAgentReference(source);
|
|
24
|
+
};
|
|
25
|
+
|
|
18
26
|
const addServiceToItinerary = async ({
|
|
19
27
|
axios,
|
|
20
28
|
token: {
|
|
@@ -30,6 +38,7 @@ const addServiceToItinerary = async ({
|
|
|
30
38
|
quoteId,
|
|
31
39
|
optionId,
|
|
32
40
|
startDate,
|
|
41
|
+
agentRef,
|
|
33
42
|
reference,
|
|
34
43
|
/*
|
|
35
44
|
paxConfigs: [{ roomType: 'DB', adults: 2 }, { roomType: 'TW', children: 2 }]
|
|
@@ -66,6 +75,20 @@ const addServiceToItinerary = async ({
|
|
|
66
75
|
return acc;
|
|
67
76
|
}, {});
|
|
68
77
|
|
|
78
|
+
const directLinePayloadHasAgentReference = directLinePayload &&
|
|
79
|
+
Object.prototype.hasOwnProperty.call(directLinePayload, 'AgentRef');
|
|
80
|
+
const customFieldsHaveAgentReference = Object.prototype.hasOwnProperty.call(
|
|
81
|
+
cfvPerService,
|
|
82
|
+
'AgentRef',
|
|
83
|
+
);
|
|
84
|
+
const payloadAgentReference = buildAgentReference({ agentRef, reference });
|
|
85
|
+
let agentReferenceSource = payloadAgentReference;
|
|
86
|
+
if (directLinePayloadHasAgentReference) agentReferenceSource = directLinePayload.AgentRef;
|
|
87
|
+
if (customFieldsHaveAgentReference) agentReferenceSource = cfvPerService.AgentRef;
|
|
88
|
+
const agentReference = normalizeAgentReference(agentReferenceSource);
|
|
89
|
+
const directLinePayloadWithoutAgentReference = R.omit(['AgentRef'], directLinePayload || {});
|
|
90
|
+
const customFieldsWithoutAgentReference = R.omit(['AgentRef'], cfvPerService);
|
|
91
|
+
|
|
69
92
|
const directHeaderPayloadHasName = directHeaderPayload &&
|
|
70
93
|
Object.prototype.hasOwnProperty.call(directHeaderPayload, 'Name');
|
|
71
94
|
const bookingNameSource = directHeaderPayloadHasName ? directHeaderPayload.Name : quoteName;
|
|
@@ -178,10 +201,10 @@ const addServiceToItinerary = async ({
|
|
|
178
201
|
if (Number.isNaN(num) || num < 1) return 1;
|
|
179
202
|
return num;
|
|
180
203
|
})(),
|
|
181
|
-
AgentRef:
|
|
204
|
+
...(agentReference ? { AgentRef: agentReference } : {}),
|
|
182
205
|
RoomConfigs: getRoomConfigs(paxConfigs),
|
|
183
|
-
...
|
|
184
|
-
...
|
|
206
|
+
...directLinePayloadWithoutAgentReference,
|
|
207
|
+
...customFieldsWithoutAgentReference,
|
|
185
208
|
},
|
|
186
209
|
};
|
|
187
210
|
const replyObj = await callTourplan({
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const R = require('ramda');
|
|
2
2
|
const Promise = require('bluebird');
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
attachOptionCatalogFields,
|
|
5
|
+
translateTPOption,
|
|
6
|
+
getOptionCurrency,
|
|
7
|
+
} = require('./resolvers/product');
|
|
4
8
|
const { getAgentCurrencyCode } = require('./availability/itinerary-availability-helper');
|
|
5
9
|
const { hostConnectXmlOptions } = require('./utils');
|
|
6
10
|
const { getCachedLocations } = require('./tp-helpers/locations');
|
|
7
11
|
const { getCachedServices } = require('./tp-helpers/services');
|
|
8
12
|
const { getCachedDestinationCountries } = require('./tp-helpers/system-settings');
|
|
13
|
+
const { getCachedSuppliers } = require('./tp-helpers/suppliers');
|
|
9
14
|
const { enrichOptionWithCodeTables } = require('./tp-helpers/option-enrichment');
|
|
10
15
|
const { addStructuredOptionMetadata } = require('./option-metadata');
|
|
11
16
|
const {
|
|
@@ -167,7 +172,12 @@ const searchProductsForItinerary = async ({
|
|
|
167
172
|
return {};
|
|
168
173
|
}
|
|
169
174
|
};
|
|
170
|
-
const [
|
|
175
|
+
const [
|
|
176
|
+
locationsByCode,
|
|
177
|
+
resolvedServicesByCode,
|
|
178
|
+
countriesByDestination,
|
|
179
|
+
suppliersById,
|
|
180
|
+
] = await Promise.all([
|
|
171
181
|
loadOptionalTable('locations', () => (
|
|
172
182
|
getCachedLocations({
|
|
173
183
|
callTourplan,
|
|
@@ -200,6 +210,16 @@ const searchProductsForItinerary = async ({
|
|
|
200
210
|
hostConnectAgentPassword,
|
|
201
211
|
})
|
|
202
212
|
)),
|
|
213
|
+
loadOptionalTable('suppliers', () => (
|
|
214
|
+
getCachedSuppliers({
|
|
215
|
+
callTourplan,
|
|
216
|
+
cache,
|
|
217
|
+
axios,
|
|
218
|
+
hostConnectEndpoint,
|
|
219
|
+
hostConnectAgentID,
|
|
220
|
+
hostConnectAgentPassword,
|
|
221
|
+
})
|
|
222
|
+
)),
|
|
203
223
|
]);
|
|
204
224
|
servicesByCode = resolvedServicesByCode;
|
|
205
225
|
|
|
@@ -217,14 +237,21 @@ const searchProductsForItinerary = async ({
|
|
|
217
237
|
), enrichedOptions);
|
|
218
238
|
const products = await Promise.map(
|
|
219
239
|
arrayOfOptionsGroupedBySupplierId,
|
|
220
|
-
optionsGroupedBySupplierId =>
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
optionsGroupedBySupplierId => {
|
|
241
|
+
const supplierId = R.path([0, 'OptGeneral', 'SupplierId'], optionsGroupedBySupplierId);
|
|
242
|
+
const supplierKey = supplierId === undefined || supplierId === null
|
|
243
|
+
? ''
|
|
244
|
+
: String(supplierId).trim();
|
|
245
|
+
return translateTPOption({
|
|
246
|
+
rootValue: {
|
|
247
|
+
optionsGroupedBySupplierId,
|
|
248
|
+
supplierRecord: suppliersById[supplierKey] || {},
|
|
249
|
+
},
|
|
250
|
+
agentCurrencyCode,
|
|
251
|
+
typeDefs: itineraryProductTypeDefs,
|
|
252
|
+
query: itineraryProductQuery,
|
|
253
|
+
});
|
|
254
|
+
},
|
|
228
255
|
{
|
|
229
256
|
concurrency: 10,
|
|
230
257
|
},
|
|
@@ -232,7 +259,8 @@ const searchProductsForItinerary = async ({
|
|
|
232
259
|
// Preserve raw OptRates / PickupPoints from Tourplan in product search response
|
|
233
260
|
// when available (camelCase keys: optRates, pickupPoints; Tourplan field casing
|
|
234
261
|
// inside). Re-attach after GraphQL because stock TI2 itinerary-product
|
|
235
|
-
// typeDefs/query omit these fields (same for city/country/currency
|
|
262
|
+
// typeDefs/query omit these fields (same for city/country/currency,
|
|
263
|
+
// optionClass, chargeUnit, Other, and maxPaxWithInfants).
|
|
236
264
|
// Also ensure city and currency are always on the option for product cache.
|
|
237
265
|
const enrichedByOptionId = R.indexBy(R.prop('Opt'), enrichedOptions);
|
|
238
266
|
const optionRatesByOptionId = options.reduce((acc, option) => {
|
|
@@ -261,7 +289,7 @@ const searchProductsForItinerary = async ({
|
|
|
261
289
|
|| R.path(['PickupPoints'], rawOption),
|
|
262
290
|
)
|
|
263
291
|
: undefined;
|
|
264
|
-
return addStructuredOptionMetadata({
|
|
292
|
+
return addStructuredOptionMetadata(attachOptionCatalogFields({
|
|
265
293
|
...R.omit(['city', 'country', 'rateContext'], currentOption),
|
|
266
294
|
...(city ? { city } : {}),
|
|
267
295
|
...(country ? { country } : {}),
|
|
@@ -270,7 +298,7 @@ const searchProductsForItinerary = async ({
|
|
|
270
298
|
...(R.path([currentOption.optionId], optionRatesByOptionId)
|
|
271
299
|
? { optRates: R.path([currentOption.optionId], optionRatesByOptionId) }
|
|
272
300
|
: {}),
|
|
273
|
-
});
|
|
301
|
+
}, rawOption));
|
|
274
302
|
}),
|
|
275
303
|
}));
|
|
276
304
|
return {
|