ti2-tourplan 1.0.120 → 1.0.122

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.
@@ -140,6 +140,54 @@ const getDaysInYear = dateString => {
140
140
  const year = moment(dateString).year();
141
141
  return moment([year]).isLeapYear() ? 366 : 365;
142
142
  };
143
+
144
+ /*
145
+ Option Availability Integer List (OptAvail)
146
+ This is a list of integers that represent the availability of the option for
147
+ each day in the requested date range.
148
+ The integers are to be interpreted as follows:
149
+ Greater than 0 means that inventory is available, with the integer specifying
150
+ the number of units available.
151
+ For options with a service type of Y , the inventory is in units of rooms.
152
+ For other service types, the inventory is in units of pax.
153
+ -1 Not available.
154
+ -2 Available on free sell.
155
+ -3 Available on request.
156
+ Note: A return value of 0 or something less than -3 is impossible.
157
+ */
158
+ const getAvailabilityOnly = async ({
159
+ optionId,
160
+ hostConnectEndpoint,
161
+ hostConnectAgentID,
162
+ hostConnectAgentPassword,
163
+ axios,
164
+ startDate,
165
+ requestedEndDate,
166
+ callTourplan,
167
+ }) => {
168
+ const isValidRequestedEndDate = moment(requestedEndDate, 'YYYY-MM-DD', true).isValid();
169
+ const dateTo = isValidRequestedEndDate
170
+ ? requestedEndDate
171
+ : moment(startDate).add(1, 'year').subtract(1, 'day').format('YYYY-MM-DD');
172
+ const model = {
173
+ OptionInfoRequest: {
174
+ Opt: optionId,
175
+ Info: 'AD',
176
+ AgentID: hostConnectAgentID,
177
+ Password: hostConnectAgentPassword,
178
+ DateFrom: startDate,
179
+ DateTo: dateTo,
180
+ ACache: 'N',
181
+ },
182
+ };
183
+ const replyObj = await callTourplan({
184
+ model,
185
+ endpoint: hostConnectEndpoint,
186
+ axios,
187
+ xmlOptions: hostConnectXmlOptions,
188
+ });
189
+ return R.pathOr([], ['OptionInfoReply', 'Option', 'OptAvail'], replyObj);
190
+ };
143
191
  /*
144
192
  Get general option information from Tourplan API.
145
193
 
@@ -1327,6 +1375,7 @@ const findNextValidDate = (startDate, dateRanges) => {
1327
1375
 
1328
1376
  module.exports = {
1329
1377
  getAgentCurrencyCode,
1378
+ getAvailabilityOnly,
1330
1379
  getAvailabilityConfig,
1331
1380
  getNoRatesAvailableError,
1332
1381
  getStayResults,
@@ -18,6 +18,7 @@ const {
18
18
  } = require('./itinerary-availability-helper');
19
19
 
20
20
  const {
21
+ getAvailabilityOnly,
21
22
  getAvailabilityConfig,
22
23
  getNoRatesAvailableError,
23
24
  getStayResults,
@@ -86,6 +87,28 @@ const returnEmptyRatesOrError = (allowSendingServicesWithoutARate, agentCurrency
86
87
  };
87
88
  };
88
89
 
90
+ const getOptAvailValues = optAvail => {
91
+ if (Array.isArray(optAvail)) {
92
+ return optAvail
93
+ .map(value => Number(value))
94
+ .filter(value => !Number.isNaN(value));
95
+ }
96
+
97
+ if (typeof optAvail === 'string') {
98
+ return optAvail
99
+ .trim()
100
+ .split(/\s+/)
101
+ .map(value => Number(value))
102
+ .filter(value => !Number.isNaN(value));
103
+ }
104
+
105
+ return [];
106
+ };
107
+
108
+ const isOptAvailBookable = optAvailValues => (
109
+ optAvailValues.some(value => value > 0 || value === -2 || value === -3)
110
+ );
111
+
89
112
  const searchAvailabilityForItinerary = async ({
90
113
  axios,
91
114
  token: {
@@ -108,6 +131,7 @@ const searchAvailabilityForItinerary = async ({
108
131
  payload: {
109
132
  optionId,
110
133
  startDate,
134
+ endDate: requestedEndDate,
111
135
  /*
112
136
  paxConfigs: [{ roomType: 'DB', adults: 2 }, { roomType: 'TW', children: 2 }]
113
137
  */
@@ -120,10 +144,38 @@ const searchAvailabilityForItinerary = async ({
120
144
  */
121
145
  chargeUnitQuantity,
122
146
  roomTypeRequired = true,
147
+ availabilityOnly = false,
123
148
  },
124
149
  callTourplan,
125
150
  cache,
126
151
  }) => {
152
+ const isAvailabilityOnly = availabilityOnly === true;
153
+
154
+ if (isAvailabilityOnly) {
155
+ const availabilityOnlyResponse = await getAvailabilityOnly({
156
+ optionId,
157
+ hostConnectEndpoint,
158
+ hostConnectAgentID,
159
+ hostConnectAgentPassword,
160
+ axios,
161
+ startDate,
162
+ requestedEndDate,
163
+ callTourplan,
164
+ });
165
+ const optAvailValues = getOptAvailValues(availabilityOnlyResponse);
166
+ const SCheckPass = isOptAvailBookable(optAvailValues);
167
+ return {
168
+ bookable: SCheckPass,
169
+ type: 'availability',
170
+ ...(requestedEndDate && SCheckPass ? { endDate: requestedEndDate } : {}),
171
+ message: SCheckPass
172
+ ? 'Availability checked successfully'
173
+ : 'No availability found for the requested range',
174
+ availability: optAvailValues,
175
+ rates: [],
176
+ };
177
+ }
178
+
127
179
  // Get agent currency code & cache it
128
180
  const agentCurrencyCode = await getAgentCurrencyCode({
129
181
  hostConnectEndpoint,
@@ -385,7 +437,11 @@ const searchAvailabilityForItinerary = async ({
385
437
  if (dateRangesError) {
386
438
  if (dateRangesError.message.includes('minimum stay')) {
387
439
  // If minimum stay error, return availability with a warning message
388
- const { matchingRateSet } = getMatchingRateSet(dateRangeToUse.rateSets, dateRangeToUse.startDate, chargeUnitQuantity);
440
+ const { matchingRateSet } = getMatchingRateSet(
441
+ dateRangeToUse.rateSets,
442
+ dateRangeToUse.startDate,
443
+ chargeUnitQuantity,
444
+ );
389
445
  minStayRequired = matchingRateSet ? matchingRateSet.minSCU : 0;
390
446
  sWarningMsg = MIN_STAY_WARNING_MESSAGE.replace('{minSCU}', minStayRequired);
391
447
  } else if (dateRangesError.message.includes('rates are closed')) {
@@ -24,6 +24,7 @@ jest.mock('./itinerary-availability-helper', () => ({
24
24
  maxPaxPerCharge: null,
25
25
  })),
26
26
  getNoRatesAvailableError: jest.fn(async () => 'No rates available'),
27
+ getAvailabilityOnly: jest.fn(async () => '-1 -1 -1'),
27
28
  getStayResults: jest.fn(async () => ([{
28
29
  RateId: 'R1',
29
30
  Currency: 'USD',
@@ -146,4 +147,44 @@ describe('searchAvailabilityForItinerary validation flags', () => {
146
147
  expect(result.type).toBe('inventory');
147
148
  expect(result.endDate).toBe('2025-04-02');
148
149
  });
150
+
151
+ it('uses availabilityOnly flow and keeps response shape consistent', async () => {
152
+ itineraryAvailabilityHelper.getAvailabilityOnly.mockResolvedValueOnce('-1 -2 -1');
153
+
154
+ const result = await searchAvailabilityForItinerary({
155
+ axios: jest.fn(),
156
+ token: baseToken,
157
+ payload: { ...basePayload, availabilityOnly: true, endDate: '2025-04-05' },
158
+ callTourplan: jest.fn(),
159
+ cache: { getOrExec: async ({ fn, fnParams }) => fn(...fnParams) },
160
+ });
161
+
162
+ expect(itineraryAvailabilityHelper.getAvailabilityOnly).toHaveBeenCalledTimes(1);
163
+ expect(itineraryAvailabilityHelper.getAgentCurrencyCode).not.toHaveBeenCalled();
164
+ expect(result).toEqual({
165
+ bookable: true,
166
+ type: 'availability',
167
+ endDate: '2025-04-05',
168
+ message: 'Availability checked successfully',
169
+ availability: [-1, -2, -1],
170
+ rates: [],
171
+ });
172
+ });
173
+
174
+ it('marks availabilityOnly response as not bookable when all days are unavailable', async () => {
175
+ itineraryAvailabilityHelper.getAvailabilityOnly.mockResolvedValueOnce('-1 -1 -1');
176
+
177
+ const result = await searchAvailabilityForItinerary({
178
+ axios: jest.fn(),
179
+ token: baseToken,
180
+ payload: { ...basePayload, availabilityOnly: true, endDate: '2025-04-05' },
181
+ callTourplan: jest.fn(),
182
+ cache: { getOrExec: async ({ fn, fnParams }) => fn(...fnParams) },
183
+ });
184
+
185
+ expect(result.bookable).toBe(false);
186
+ expect(result.message).toBe('No availability found for the requested range');
187
+ expect(result.availability).toEqual([-1, -1, -1]);
188
+ expect(result.rates).toEqual([]);
189
+ });
149
190
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-tourplan",
3
- "version": "1.0.120",
3
+ "version": "1.0.122",
4
4
  "description": "Tourplan's TI2 Plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/utils.js CHANGED
@@ -62,6 +62,7 @@ const escapeInvalidXmlChars = str => {
62
62
  // because js2xmlparser does that for us. Plus if we use sanitize before calling js2xmlparser
63
63
  // js2xmlparser will escape & to '&' making it invalid XML
64
64
  return convertAccentedChars(asString)
65
+ .replace(/\u00A0/g, ' ')
65
66
  .replace(/’/g, "'")
66
67
  .replace(/‘/g, "'")
67
68
  .replace(/“/g, '"')
package/utils.test.js CHANGED
@@ -22,6 +22,10 @@ describe('escapeInvalidXmlChars', () => {
22
22
  expect(escapeInvalidXmlChars('Say ‘hi’ and “bye” – ok')).toBe('Say \'hi\' and "bye" - ok');
23
23
  });
24
24
 
25
+ it('replaces non-breaking spaces with regular spaces', () => {
26
+ expect(escapeInvalidXmlChars('Borwieck\u00A0Mrs R')).toBe('Borwieck Mrs R');
27
+ });
28
+
25
29
  it('strips disallowed control characters', () => {
26
30
  expect(escapeInvalidXmlChars('A\u0000B\u0008C')).toBe('ABC');
27
31
  });