ti2-tourplan 1.0.136 → 1.0.137
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/__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 +38 -0
- package/itinerary-add-service.js +1 -0
- 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: {
|
|
@@ -1733,6 +1767,10 @@ describe('search tests', () => {
|
|
|
1733
1767
|
expect(retVal.bookings[0].serviceLines[0].paxList[0].firstName).toBe('Sean');
|
|
1734
1768
|
expect(retVal.bookings[0].serviceLines[0].paxList[0]).toHaveProperty('lastName');
|
|
1735
1769
|
expect(retVal.bookings[0].serviceLines[0].paxList[0].lastName).toBe('Conta');
|
|
1770
|
+
expect(retVal.bookings[0].serviceLines[0]).toHaveProperty('linePrice');
|
|
1771
|
+
expect(retVal.bookings[0].serviceLines[0].linePrice).toBe('187795');
|
|
1772
|
+
expect(retVal.bookings[0].serviceLines[0]).toHaveProperty('agentPrice');
|
|
1773
|
+
expect(retVal.bookings[0].serviceLines[0].agentPrice).toBe('159000');
|
|
1736
1774
|
expect(retVal.bookings[0].serviceLines[0]).toHaveProperty('quantity');
|
|
1737
1775
|
expect(retVal.bookings[0].serviceLines[0].quantity).toBe(4);
|
|
1738
1776
|
expect(retVal.bookings[0].serviceLines[0]).toHaveProperty('status');
|
package/itinerary-add-service.js
CHANGED
|
@@ -196,6 +196,7 @@ const addServiceToItinerary = async ({
|
|
|
196
196
|
id: R.path(['AddServiceReply', 'BookingId'], replyObj) || quoteId,
|
|
197
197
|
reference: R.path(['AddServiceReply', 'Ref'], replyObj),
|
|
198
198
|
linePrice: R.path(['AddServiceReply', 'Services', 'Service', 'LinePrice'], replyObj),
|
|
199
|
+
agentPrice: R.path(['AddServiceReply', 'Services', 'Service', 'AgentPrice'], replyObj),
|
|
199
200
|
lineId: R.path(['AddServiceReply', 'ServiceLineId'], replyObj),
|
|
200
201
|
},
|
|
201
202
|
};
|
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
|
+
});
|