ti2-tourplan 1.0.114 → 1.0.115

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.
@@ -1034,6 +1034,7 @@ const getAvailabilityConfig = async ({
1034
1034
  endDate,
1035
1035
  message,
1036
1036
  dateRanges,
1037
+ duration,
1037
1038
  maxPaxPerCharge,
1038
1039
  };
1039
1040
  };
@@ -119,6 +119,7 @@ const searchAvailabilityForItinerary = async ({
119
119
  Defaults to 1.
120
120
  */
121
121
  chargeUnitQuantity,
122
+ roomTypeRequired = true,
122
123
  },
123
124
  callTourplan,
124
125
  cache,
@@ -206,8 +207,20 @@ const searchAvailabilityForItinerary = async ({
206
207
  message,
207
208
  dateRanges,
208
209
  maxPaxPerCharge,
210
+ duration,
209
211
  } = availabilityConfig;
210
212
 
213
+ let isDateRangeValidationRequired = true;
214
+ // Skip date range validation for packages & non-accomodation products
215
+ // This is required because:
216
+ // 1. non-accomodation products that are not packages do not have date ranges
217
+ // and can be booked even on the last date of valid date range
218
+ // 2. packages (including multi-day packages) are allowed to be booked even on
219
+ // the last date of valid date range. (A package is when duration is > 1).
220
+ if ((duration && duration > 1) || !roomTypeRequired) {
221
+ isDateRangeValidationRequired = false;
222
+ }
223
+
211
224
  // Validate max pax per charge
212
225
  const maxPaxPerChargeError = validateMaxPaxPerCharge({
213
226
  roomConfigs,
@@ -218,9 +231,9 @@ const searchAvailabilityForItinerary = async ({
218
231
  }
219
232
 
220
233
  let noOfDaysRatesAvailable = 0;
221
- let allDatesHaveRatesAvailable = false;
222
234
 
223
235
  if (dateRanges.length > 0) {
236
+ let allDatesHaveRatesAvailable = true;
224
237
  const startDateIsInvalid = validateStartDay({
225
238
  dateRanges,
226
239
  startDate,
@@ -237,20 +250,24 @@ const searchAvailabilityForItinerary = async ({
237
250
 
238
251
  const lastDateRangeEndDate = moment(dateRanges[dateRanges.length - 1].endDate);
239
252
  noOfDaysRatesAvailable = lastDateRangeEndDate.diff(moment(startDate), 'days') + 1;
240
- // eslint-disable-next-line max-len
241
- allDatesHaveRatesAvailable = doAllDatesHaveRatesAvailable(lastDateRangeEndDate, startDate, chargeUnitQuantity);
253
+ if (isDateRangeValidationRequired) {
254
+ // eslint-disable-next-line max-len
255
+ allDatesHaveRatesAvailable = doAllDatesHaveRatesAvailable(lastDateRangeEndDate, startDate, chargeUnitQuantity);
256
+ }
242
257
 
243
258
  if (allDatesHaveRatesAvailable) {
244
259
  // all dates have rates available, get stay rates for the given dates
245
260
  // Validate date ranges and room configurations
246
- const dateRangesError = validateDateRanges({
247
- dateRanges,
248
- startDate,
249
- chargeUnitQuantity,
250
- });
261
+ if (isDateRangeValidationRequired) {
262
+ const dateRangesError = validateDateRanges({
263
+ dateRanges,
264
+ startDate,
265
+ chargeUnitQuantity,
266
+ });
251
267
 
252
- if (dateRangesError) {
253
- return dateRangesError;
268
+ if (dateRangesError) {
269
+ return dateRangesError;
270
+ }
254
271
  }
255
272
 
256
273
  // get stay rates for the given dates
@@ -353,13 +370,16 @@ const searchAvailabilityForItinerary = async ({
353
370
  const customPeriodInfoMsg = useLastYearRate ? CUSTOM_PERIOD_LAST_YEAR_INFO_MSG : CUSTOM_PERIOD_LAST_AVAILABLE_INFO_MSG;
354
371
 
355
372
  let sWarningMsg = '';
373
+ let dateRangesError = null;
356
374
 
357
- // Validate date ranges
358
- const dateRangesError = validateDateRanges({
359
- dateRanges: [dateRangeToUse],
360
- startDate: dateRangeToUse.startDate,
361
- chargeUnitQuantity,
362
- });
375
+ if (isDateRangeValidationRequired) {
376
+ // Validate date ranges
377
+ dateRangesError = validateDateRanges({
378
+ dateRanges: [dateRangeToUse],
379
+ startDate: dateRangeToUse.startDate,
380
+ chargeUnitQuantity,
381
+ });
382
+ }
363
383
 
364
384
  let minStayRequired = 0;
365
385
  if (dateRangesError) {
@@ -0,0 +1,149 @@
1
+ /* globals describe, it, expect, jest, beforeEach */
2
+
3
+ jest.mock('./itinerary-availability-utils', () => ({
4
+ validateMaxPaxPerCharge: jest.fn(() => null),
5
+ validateDateRanges: jest.fn(() => null),
6
+ validateStartDay: jest.fn(() => null),
7
+ getMatchingRateSet: jest.fn(() => ({ matchingRateSet: null })),
8
+ }));
9
+
10
+ jest.mock('./product-connect/itinerary-pc-api-validation-helper', () => ({
11
+ validateProductConnect: jest.fn(async () => false),
12
+ }));
13
+
14
+ jest.mock('./itinerary-availability-helper', () => ({
15
+ getAgentCurrencyCode: jest.fn(async () => 'USD'),
16
+ getConversionRate: jest.fn(async () => ({ conversionRate: 1 })),
17
+ findNextValidDate: jest.fn(() => null),
18
+ getAvailabilityConfig: jest.fn(async () => ({
19
+ roomConfigs: [{ Adults: 2 }],
20
+ endDate: null,
21
+ message: null,
22
+ dateRanges: [{ startDate: '2025-04-01', endDate: '2025-04-30', rateSets: [] }],
23
+ duration: null,
24
+ maxPaxPerCharge: null,
25
+ })),
26
+ getNoRatesAvailableError: jest.fn(async () => 'No rates available'),
27
+ getStayResults: jest.fn(async () => ([{
28
+ RateId: 'R1',
29
+ Currency: 'USD',
30
+ TotalPrice: '10000',
31
+ AgentPrice: '9000',
32
+ }])),
33
+ getCustomRateDateRange: jest.fn(async () => ({ dateRangeToUse: null, errorMsg: 'No custom rate' })),
34
+ getRatesObjectArray: jest.fn(() => ([{ rateId: 'R1', totalPrice: 10000, agentPrice: 9000 }])),
35
+ getEmptyRateObject: jest.fn(() => ([{ rateId: 'EMPTY' }])),
36
+ MIN_MARKUP_PERCENTAGE: 0,
37
+ MAX_MARKUP_PERCENTAGE: 100,
38
+ MIN_EXTENDED_BOOKING_YEARS: 1,
39
+ MAX_EXTENDED_BOOKING_YEARS: 10,
40
+ GENERIC_AVALABILITY_CHK_ERROR_MESSAGE: 'Generic availability error',
41
+ }));
42
+
43
+ jest.mock('./product-connect/itinerary-pc-rates-helper', () => ({
44
+ getCostFromProductConnect: jest.fn(async () => ({
45
+ success: false,
46
+ costPriceIncludingTax: 0,
47
+ taxRate: 0,
48
+ })),
49
+ }));
50
+
51
+ jest.mock('./product-connect/itinerary-pc-option-helper', () => ({
52
+ getOptionFromProductConnect: jest.fn(async () => null),
53
+ CROSS_SEASON_NOT_ALLOWED: 'N',
54
+ CROSS_SEASON_CAL_SPLIT_RATE: 'S',
55
+ CROSS_SEASON_CAL_USING_RATE_OF_FIRST_RATE_PERIOD: 'F',
56
+ }));
57
+
58
+ const itineraryAvailabilityUtils = require('./itinerary-availability-utils');
59
+ const itineraryAvailabilityHelper = require('./itinerary-availability-helper');
60
+ const { searchAvailabilityForItinerary } = require('./itinerary-availability');
61
+
62
+ describe('searchAvailabilityForItinerary validation flags', () => {
63
+ const baseToken = {
64
+ hostConnectEndpoint: 'https://test-host-connect.com',
65
+ hostConnectAgentID: 'test-agent-id',
66
+ hostConnectAgentPassword: 'test-agent-password',
67
+ };
68
+
69
+ const basePayload = {
70
+ optionId: 'OPTION_1',
71
+ startDate: '2025-04-01',
72
+ chargeUnitQuantity: 2,
73
+ paxConfigs: [{ roomType: 'DB', adults: 2 }],
74
+ };
75
+
76
+ beforeEach(() => {
77
+ jest.clearAllMocks();
78
+ });
79
+
80
+ it('runs date range validation when duration is not fixed and room type is required', async () => {
81
+ itineraryAvailabilityHelper.getAvailabilityConfig.mockResolvedValueOnce({
82
+ roomConfigs: [{ Adults: 2 }],
83
+ endDate: null,
84
+ message: null,
85
+ dateRanges: [{ startDate: '2025-04-01', endDate: '2025-04-30', rateSets: [] }],
86
+ duration: null,
87
+ maxPaxPerCharge: null,
88
+ });
89
+
90
+ const result = await searchAvailabilityForItinerary({
91
+ axios: jest.fn(),
92
+ token: baseToken,
93
+ payload: basePayload,
94
+ callTourplan: jest.fn(),
95
+ cache: { getOrExec: async ({ fn, fnParams }) => fn(...fnParams) },
96
+ });
97
+
98
+ expect(itineraryAvailabilityUtils.validateDateRanges).toHaveBeenCalled();
99
+ expect(result.bookable).toBe(true);
100
+ expect(result.type).toBe('inventory');
101
+ });
102
+
103
+ it('skips date range validation when roomTypeRequired is false', async () => {
104
+ itineraryAvailabilityHelper.getAvailabilityConfig.mockResolvedValueOnce({
105
+ roomConfigs: [{ Adults: 2 }],
106
+ endDate: null,
107
+ message: null,
108
+ dateRanges: [{ startDate: '2025-04-01', endDate: '2025-04-30', rateSets: [] }],
109
+ duration: null,
110
+ maxPaxPerCharge: null,
111
+ });
112
+
113
+ const result = await searchAvailabilityForItinerary({
114
+ axios: jest.fn(),
115
+ token: baseToken,
116
+ payload: { ...basePayload, roomTypeRequired: false },
117
+ callTourplan: jest.fn(),
118
+ cache: { getOrExec: async ({ fn, fnParams }) => fn(...fnParams) },
119
+ });
120
+
121
+ expect(itineraryAvailabilityUtils.validateDateRanges).not.toHaveBeenCalled();
122
+ expect(result.bookable).toBe(true);
123
+ expect(result.type).toBe('inventory');
124
+ });
125
+
126
+ it('skips date range validation when duration is greater than 1', async () => {
127
+ itineraryAvailabilityHelper.getAvailabilityConfig.mockResolvedValueOnce({
128
+ roomConfigs: [{ Adults: 2 }],
129
+ endDate: '2025-04-02',
130
+ message: 'Duration is fixed',
131
+ dateRanges: [{ startDate: '2025-04-01', endDate: '2025-04-30', rateSets: [] }],
132
+ duration: 2,
133
+ maxPaxPerCharge: null,
134
+ });
135
+
136
+ const result = await searchAvailabilityForItinerary({
137
+ axios: jest.fn(),
138
+ token: baseToken,
139
+ payload: basePayload,
140
+ callTourplan: jest.fn(),
141
+ cache: { getOrExec: async ({ fn, fnParams }) => fn(...fnParams) },
142
+ });
143
+
144
+ expect(itineraryAvailabilityUtils.validateDateRanges).not.toHaveBeenCalled();
145
+ expect(result.bookable).toBe(true);
146
+ expect(result.type).toBe('inventory');
147
+ expect(result.endDate).toBe('2025-04-02');
148
+ });
149
+ });
package/index.test.js CHANGED
@@ -442,6 +442,27 @@ describe('search tests', () => {
442
442
  expect(retVal.type).toBe('inventory');
443
443
  });
444
444
 
445
+ it('searchAvailabilityForItinerary - default roomTypeRequired matches explicit true', async () => {
446
+ axios.mockImplementation(getFixture);
447
+ const payload = {
448
+ optionId: 'AKLACAKLSOFDYNAMC',
449
+ startDate: '2025-04-01',
450
+ chargeUnitQuantity: 2,
451
+ paxConfigs: [{ roomType: 'DB', adults: 1 }],
452
+ };
453
+ const defaultBehaviourResponse = await app.searchAvailabilityForItinerary({
454
+ axios,
455
+ token,
456
+ payload,
457
+ });
458
+ const explicitTrueResponse = await app.searchAvailabilityForItinerary({
459
+ axios,
460
+ token,
461
+ payload: { ...payload, roomTypeRequired: true },
462
+ });
463
+ expect(defaultBehaviourResponse).toEqual(explicitTrueResponse);
464
+ });
465
+
445
466
  // Skip this test because we aren't using A check anymore
446
467
  it.skip('searchAvailabilityForItinerary - bookable - on request', async () => {
447
468
  axios.mockImplementation(getFixture);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-tourplan",
3
- "version": "1.0.114",
3
+ "version": "1.0.115",
4
4
  "description": "Tourplan's TI2 Plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -25,7 +25,8 @@ const resolvers = {
25
25
  const lastUpdateISO = R.path(['OptGeneral', 'LastUpdate'], option);
26
26
  return lastUpdateISO ? new Date(lastUpdateISO).getTime() / 1000 : null;
27
27
  },
28
- // Guides, Accommodation, Transfers, Entrance Fees, Meals, Rail, Sightseeing Other
28
+ // Guides, Accommodation, Transfers, Entrance Fees, Meals, Rail, Sightseeing,
29
+ // Rental Cars, Apartments, Blank Web Services, Farm Stays, Packages
29
30
  serviceType: option => {
30
31
  const st = R.pathOr('', ['OptGeneral', 'ButtonName'], option);
31
32
  return typeof st === 'string' ? st : '';