ti2-tourplan 1.0.136 → 1.0.138
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/.claude/settings.local.json +10 -0
- package/__fixtures__/GetBookingRequest_34a34449e57b0cf0147f1d0f3ad52f6702d1d7b6.txt +1 -0
- package/__fixtures__/GetBookingRequest_3586125e1ae2acc1697b9bf145c3234bd14c62aa.txt +1 -0
- package/__fixtures__/GetBookingRequest_ec2774e67986fab0fab72b61b2630a7f38befd5b.txt +1 -0
- package/__snapshots__/index.test.js.snap +1 -0
- package/index.test.js +65 -0
- package/itinerary-add-service.js +3 -1
- package/package.json +1 -1
- package/resolvers/itinerary.js +42 -2
- package/resolvers/itinerary.test.js +85 -0
package/index.test.js
CHANGED
|
@@ -140,6 +140,40 @@ describe('search tests', () => {
|
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
142
|
describe('addServiceToItinerary', () => {
|
|
143
|
+
it('returns the gross line price and net agent price from Tourplan', async () => {
|
|
144
|
+
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
145
|
+
AddServiceReply: {
|
|
146
|
+
BookingId: '12345',
|
|
147
|
+
Ref: 'TESTREF',
|
|
148
|
+
Services: {
|
|
149
|
+
Service: {
|
|
150
|
+
LinePrice: '10000',
|
|
151
|
+
AgentPrice: '8500',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
ServiceLineId: '10',
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
const result = await app.addServiceToItinerary({
|
|
159
|
+
axios,
|
|
160
|
+
token,
|
|
161
|
+
payload: {
|
|
162
|
+
quoteName: 'Pricing response test',
|
|
163
|
+
optionId: 'ABC123',
|
|
164
|
+
startDate: '2026-07-03',
|
|
165
|
+
reference: 'TESTREF',
|
|
166
|
+
paxConfigs: [{ roomType: 'Double', adults: 2 }],
|
|
167
|
+
notes: '',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(result.booking).toMatchObject({
|
|
172
|
+
linePrice: '10000',
|
|
173
|
+
agentPrice: '8500',
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
143
177
|
it('limits new booking names after escaping XML characters', async () => {
|
|
144
178
|
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
145
179
|
AddServiceReply: {
|
|
@@ -278,6 +312,33 @@ describe('search tests', () => {
|
|
|
278
312
|
expect(newBookingInfo.Name).toBe(`${'Ae'.repeat(29)}A`);
|
|
279
313
|
expect(newBookingInfo.HeaderField).toBe('kept');
|
|
280
314
|
});
|
|
315
|
+
|
|
316
|
+
it('limits service remarks to the Tourplan field length', async () => {
|
|
317
|
+
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
318
|
+
AddServiceReply: {
|
|
319
|
+
BookingId: '12345',
|
|
320
|
+
Ref: 'TESTREF',
|
|
321
|
+
ServiceLineId: '10',
|
|
322
|
+
},
|
|
323
|
+
}));
|
|
324
|
+
|
|
325
|
+
await app.addServiceToItinerary({
|
|
326
|
+
axios,
|
|
327
|
+
token,
|
|
328
|
+
payload: {
|
|
329
|
+
quoteName: 'Passenger notes test',
|
|
330
|
+
optionId: 'ABC123',
|
|
331
|
+
startDate: '2026-07-03',
|
|
332
|
+
reference: 'TESTREF',
|
|
333
|
+
paxConfigs: [{ roomType: 'Double', adults: 2 }],
|
|
334
|
+
notes: `Passenger Notes: ${'A'.repeat(80)} Service Notes: late arrival`,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const request = mockCallTourplan.mock.calls[0][0].model.AddServiceRequest;
|
|
339
|
+
expect(request.Remarks).toBe(`Passenger Notes: ${'A'.repeat(43)}`);
|
|
340
|
+
expect(request.Remarks).toHaveLength(60);
|
|
341
|
+
});
|
|
281
342
|
});
|
|
282
343
|
|
|
283
344
|
describe('cancelBooking', () => {
|
|
@@ -1733,6 +1794,10 @@ describe('search tests', () => {
|
|
|
1733
1794
|
expect(retVal.bookings[0].serviceLines[0].paxList[0].firstName).toBe('Sean');
|
|
1734
1795
|
expect(retVal.bookings[0].serviceLines[0].paxList[0]).toHaveProperty('lastName');
|
|
1735
1796
|
expect(retVal.bookings[0].serviceLines[0].paxList[0].lastName).toBe('Conta');
|
|
1797
|
+
expect(retVal.bookings[0].serviceLines[0]).toHaveProperty('linePrice');
|
|
1798
|
+
expect(retVal.bookings[0].serviceLines[0].linePrice).toBe('187795');
|
|
1799
|
+
expect(retVal.bookings[0].serviceLines[0]).toHaveProperty('agentPrice');
|
|
1800
|
+
expect(retVal.bookings[0].serviceLines[0].agentPrice).toBe('159000');
|
|
1736
1801
|
expect(retVal.bookings[0].serviceLines[0]).toHaveProperty('quantity');
|
|
1737
1802
|
expect(retVal.bookings[0].serviceLines[0].quantity).toBe(4);
|
|
1738
1803
|
expect(retVal.bookings[0].serviceLines[0]).toHaveProperty('status');
|
package/itinerary-add-service.js
CHANGED
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
|
|
10
10
|
const DEFAULT_TOURPLAN_SERVICE_STATUS = 'IR';
|
|
11
11
|
const MAX_BOOKING_NAME_LENGTH = 59;
|
|
12
|
+
const MAX_SERVICE_REMARKS_LENGTH = 60;
|
|
12
13
|
const SERVICE_CANNOT_BE_ADDED_ERROR_MESSAGE = 'Service cannot be added to quote for the requested date/stay. (e.g. no rates, block out period, on request, minimum stay etc.)';
|
|
13
14
|
|
|
14
15
|
const getBookingName = quoteName => escapeInvalidXmlChars(quoteName)
|
|
@@ -166,7 +167,7 @@ const addServiceToItinerary = async ({
|
|
|
166
167
|
})),
|
|
167
168
|
},
|
|
168
169
|
} : {}),
|
|
169
|
-
Remarks: escapeInvalidXmlChars(notes).slice(0,
|
|
170
|
+
Remarks: escapeInvalidXmlChars(notes).slice(0, MAX_SERVICE_REMARKS_LENGTH),
|
|
170
171
|
Opt: optionId,
|
|
171
172
|
DateFrom: startDate,
|
|
172
173
|
RateId: rateId || 'Default',
|
|
@@ -196,6 +197,7 @@ const addServiceToItinerary = async ({
|
|
|
196
197
|
id: R.path(['AddServiceReply', 'BookingId'], replyObj) || quoteId,
|
|
197
198
|
reference: R.path(['AddServiceReply', 'Ref'], replyObj),
|
|
198
199
|
linePrice: R.path(['AddServiceReply', 'Services', 'Service', 'LinePrice'], replyObj),
|
|
200
|
+
agentPrice: R.path(['AddServiceReply', 'Services', 'Service', 'AgentPrice'], replyObj),
|
|
199
201
|
lineId: R.path(['AddServiceReply', 'ServiceLineId'], replyObj),
|
|
200
202
|
},
|
|
201
203
|
};
|
package/package.json
CHANGED
package/resolvers/itinerary.js
CHANGED
|
@@ -70,6 +70,7 @@ const resolvers = {
|
|
|
70
70
|
paxList: R.path(['paxList']),
|
|
71
71
|
paxConfigs: R.path(['paxConfigs']),
|
|
72
72
|
linePrice: R.path(['LinePrice']),
|
|
73
|
+
agentPrice: R.path(['AgentPrice']),
|
|
73
74
|
quantity: R.path(['SCUqty']),
|
|
74
75
|
status: R.path(['Status']),
|
|
75
76
|
puInfo: sl => puDoInfoOrNull({
|
|
@@ -133,6 +134,42 @@ const resolvers = {
|
|
|
133
134
|
*/
|
|
134
135
|
const schemaCache = new Map();
|
|
135
136
|
|
|
137
|
+
const typeDefinesField = (typeDefs, typeName, fieldName) => {
|
|
138
|
+
const typeMatch = String(typeDefs || '').match(new RegExp(`type\\s+${typeName}\\s*{([\\s\\S]*?)}`));
|
|
139
|
+
if (!typeMatch) return false;
|
|
140
|
+
return new RegExp(`\\b${fieldName}\\s*:`).test(typeMatch[1]);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* ti2's itinerary-booking SDL may not yet declare ServiceLine.agentPrice.
|
|
145
|
+
* Extend the incoming schema so search/read can return the Tourplan net price
|
|
146
|
+
* without waiting on a ti2 release. No-op when the field is already present.
|
|
147
|
+
*
|
|
148
|
+
* @param {string|DocumentNode} typeDefs
|
|
149
|
+
* @returns {string|DocumentNode}
|
|
150
|
+
*/
|
|
151
|
+
const ensureAgentPriceOnServiceLineTypeDefs = typeDefs => {
|
|
152
|
+
if (typeof typeDefs !== 'string') return typeDefs;
|
|
153
|
+
if (typeDefinesField(typeDefs, 'ServiceLine', 'agentPrice')) return typeDefs;
|
|
154
|
+
return typeDefs.replace(
|
|
155
|
+
/(type\s+ServiceLine\s*{)/,
|
|
156
|
+
'$1\n agentPrice: String',
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Request agentPrice next to linePrice when the caller query omits it.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} query
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
const ensureAgentPriceInQuery = query => {
|
|
167
|
+
if (typeof query !== 'string') return query;
|
|
168
|
+
if (/\bagentPrice\b/.test(query)) return query;
|
|
169
|
+
if (!/\blinePrice\b/.test(query)) return query;
|
|
170
|
+
return query.replace(/\blinePrice\b/, 'linePrice\n agentPrice');
|
|
171
|
+
};
|
|
172
|
+
|
|
136
173
|
/**
|
|
137
174
|
* Return a compiled GraphQL schema for the given typeDefs, building and caching
|
|
138
175
|
* it on the first call and returning the cached instance on every subsequent call.
|
|
@@ -142,7 +179,10 @@ const schemaCache = new Map();
|
|
|
142
179
|
*/
|
|
143
180
|
const getSchema = typeDefs => {
|
|
144
181
|
if (schemaCache.has(typeDefs)) return schemaCache.get(typeDefs);
|
|
145
|
-
const schema = makeExecutableSchema({
|
|
182
|
+
const schema = makeExecutableSchema({
|
|
183
|
+
typeDefs: ensureAgentPriceOnServiceLineTypeDefs(typeDefs),
|
|
184
|
+
resolvers,
|
|
185
|
+
});
|
|
146
186
|
schemaCache.set(typeDefs, schema);
|
|
147
187
|
return schema;
|
|
148
188
|
};
|
|
@@ -163,7 +203,7 @@ const translateItineraryBooking = async ({ typeDefs, query, rootValue }) => {
|
|
|
163
203
|
const retVal = await graphql({
|
|
164
204
|
schema,
|
|
165
205
|
rootValue,
|
|
166
|
-
source: query,
|
|
206
|
+
source: ensureAgentPriceInQuery(query),
|
|
167
207
|
});
|
|
168
208
|
if (retVal.errors) {
|
|
169
209
|
throw new Error(retVal.errors.map(e => e.message).join('; '));
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* globals describe, it, expect */
|
|
2
|
+
const { typeDefs: itineraryBookingTypeDefs, query: itineraryBookingQuery } = require('../node_modules/ti2/controllers/graphql-schemas/itinerary-booking');
|
|
3
|
+
const { translateItineraryBooking } = require('./itinerary');
|
|
4
|
+
|
|
5
|
+
const typeDefsWithAgentPrice = itineraryBookingTypeDefs.replace(
|
|
6
|
+
/(type\s+ServiceLine\s*{)/,
|
|
7
|
+
'$1\n agentPrice: String',
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const queryWithAgentPrice = itineraryBookingQuery.replace(
|
|
11
|
+
/\blinePrice\b/,
|
|
12
|
+
'linePrice\n agentPrice',
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const bookingRoot = {
|
|
16
|
+
BookingId: '316559',
|
|
17
|
+
Name: 'Test booking',
|
|
18
|
+
Ref: 'ALFI393706',
|
|
19
|
+
TotalPrice: '187795',
|
|
20
|
+
Currency: 'GBP',
|
|
21
|
+
TravelDate: '2025-08-13',
|
|
22
|
+
EnteredDate: '2024-09-12',
|
|
23
|
+
ReadOnly: 'N',
|
|
24
|
+
CanAddServices: 'Y',
|
|
25
|
+
Services: {
|
|
26
|
+
Service: {
|
|
27
|
+
ServiceLineId: '745684',
|
|
28
|
+
Opt: 'LONHOSANLONBFBDLX',
|
|
29
|
+
Description: 'Bed and Full Buffet Breakfast',
|
|
30
|
+
LinePrice: '187795',
|
|
31
|
+
AgentPrice: '159000',
|
|
32
|
+
Date: '2025-08-13',
|
|
33
|
+
Status: 'OK',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe('translateItineraryBooking agentPrice', () => {
|
|
39
|
+
it('maps Tourplan AgentPrice on service lines when the caller schema omits the field', async () => {
|
|
40
|
+
const retVal = await translateItineraryBooking({
|
|
41
|
+
typeDefs: itineraryBookingTypeDefs,
|
|
42
|
+
query: itineraryBookingQuery,
|
|
43
|
+
rootValue: bookingRoot,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(retVal.serviceLines[0]).toMatchObject({
|
|
47
|
+
serviceLineId: '745684',
|
|
48
|
+
linePrice: '187795',
|
|
49
|
+
agentPrice: '159000',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('maps Tourplan AgentPrice when the caller schema already declares the field', async () => {
|
|
54
|
+
const retVal = await translateItineraryBooking({
|
|
55
|
+
typeDefs: typeDefsWithAgentPrice,
|
|
56
|
+
query: queryWithAgentPrice,
|
|
57
|
+
rootValue: bookingRoot,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(retVal.serviceLines[0]).toMatchObject({
|
|
61
|
+
serviceLineId: '745684',
|
|
62
|
+
linePrice: '187795',
|
|
63
|
+
agentPrice: '159000',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns null agentPrice when Tourplan omits AgentPrice', async () => {
|
|
68
|
+
const { Services, ...rest } = bookingRoot;
|
|
69
|
+
const retVal = await translateItineraryBooking({
|
|
70
|
+
typeDefs: typeDefsWithAgentPrice,
|
|
71
|
+
query: queryWithAgentPrice,
|
|
72
|
+
rootValue: {
|
|
73
|
+
...rest,
|
|
74
|
+
Services: {
|
|
75
|
+
Service: {
|
|
76
|
+
...Services.Service,
|
|
77
|
+
AgentPrice: undefined,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(retVal.serviceLines[0].agentPrice).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
});
|