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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(gh pr diff:*)",
5
+ "Bash(gh pr view:*)"
6
+ ],
7
+ "deny": [],
8
+ "ask": []
9
+ }
10
+ }
@@ -56,6 +56,7 @@
56
56
  </RoomConfigs>
57
57
  <CostedInBooking>Y</CostedInBooking>
58
58
  <LinePrice>187795</LinePrice>
59
+ <AgentPrice>159000</AgentPrice>
59
60
  <SCU>night</SCU>
60
61
  <SCUqty>4</SCUqty>
61
62
  <Remarks>Passenger Notes: NA.
@@ -56,6 +56,7 @@
56
56
  </RoomConfigs>
57
57
  <CostedInBooking>Y</CostedInBooking>
58
58
  <LinePrice>187795</LinePrice>
59
+ <AgentPrice>159000</AgentPrice>
59
60
  <SCU>night</SCU>
60
61
  <SCUqty>4</SCUqty>
61
62
  <Remarks>Passenger Notes: NA.
@@ -56,6 +56,7 @@
56
56
  </RoomConfigs>
57
57
  <CostedInBooking>Y</CostedInBooking>
58
58
  <LinePrice>187795</LinePrice>
59
+ <AgentPrice>159000</AgentPrice>
59
60
  <SCU>night</SCU>
60
61
  <SCUqty>4</SCUqty>
61
62
  <Remarks>Passenger Notes: NA.
@@ -281,6 +281,7 @@ Object {
281
281
  "ref": "ALFI393706",
282
282
  "serviceLines": Array [
283
283
  Object {
284
+ "agentPrice": "159000",
284
285
  "doInfo": Object {
285
286
  "date": "2025-08-17",
286
287
  "remarks": "Hotel main lobby",
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');
@@ -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, 220),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-tourplan",
3
- "version": "1.0.136",
3
+ "version": "1.0.138",
4
4
  "description": "Tourplan's TI2 Plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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({ typeDefs, resolvers });
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
+ });