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.
@@ -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: {
@@ -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');
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-tourplan",
3
- "version": "1.0.136",
3
+ "version": "1.0.137",
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
+ });