ti2-tourplan 1.0.121 → 1.0.123
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/README.md +66 -0
- package/availability/itinerary-availability-helper.js +49 -0
- package/availability/itinerary-availability.js +57 -1
- package/availability/itinerary-availability.validation-flags.test.js +41 -0
- package/index.js +14 -0
- package/index.test.js +143 -0
- package/itinerary-cancel-booking.js +110 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,3 +39,69 @@ TL;DR Here's what the license entails:
|
|
|
39
39
|
7. Any modifications of this code base MUST be distributed with the same license, GPLv3.
|
|
40
40
|
8. This software is provided without warranty.
|
|
41
41
|
9. The software author or license can not be held liable for any damages inflicted by the software.
|
|
42
|
+
|
|
43
|
+
## Cancel Booking (HostConnect)
|
|
44
|
+
|
|
45
|
+
This plugin cancels bookings using `CancelServicesRequest` (not `CancelBookingRequest`).
|
|
46
|
+
|
|
47
|
+
### HostConnect XML Request
|
|
48
|
+
|
|
49
|
+
```xml
|
|
50
|
+
<Request>
|
|
51
|
+
<CancelServicesRequest>
|
|
52
|
+
<AgentID>YOUR_AGENT_ID</AgentID>
|
|
53
|
+
<Password>YOUR_AGENT_PASSWORD</Password>
|
|
54
|
+
<BookingId>14226</BookingId>
|
|
55
|
+
<ReturnBooking>Y</ReturnBooking>
|
|
56
|
+
</CancelServicesRequest>
|
|
57
|
+
</Request>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Equivalent curl:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
curl --location 'https://<HOSTCONNECT_ENDPOINT>/api/hostConnectApi' \
|
|
64
|
+
--header 'Content-Type: application/xml; charset=utf-8' \
|
|
65
|
+
--header 'requestId: <uuid>' \
|
|
66
|
+
--header 'Accept: application/xml' \
|
|
67
|
+
--data '<Request>
|
|
68
|
+
<CancelServicesRequest>
|
|
69
|
+
<AgentID>YOUR_AGENT_ID</AgentID>
|
|
70
|
+
<Password>YOUR_AGENT_PASSWORD</Password>
|
|
71
|
+
<BookingId>14226</BookingId>
|
|
72
|
+
<ReturnBooking>Y</ReturnBooking>
|
|
73
|
+
</CancelServicesRequest>
|
|
74
|
+
</Request>'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### HostConnect XML Response (example)
|
|
78
|
+
|
|
79
|
+
```xml
|
|
80
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
81
|
+
<!DOCTYPE Reply SYSTEM "hostConnect_5_05_010.dtd">
|
|
82
|
+
<Reply>
|
|
83
|
+
<CancelServicesReply>
|
|
84
|
+
<BookingId>14226</BookingId>
|
|
85
|
+
<Ref>A2IN111975</Ref>
|
|
86
|
+
<ServiceStatuses>
|
|
87
|
+
<ServiceStatus>
|
|
88
|
+
<ServiceLineId>61738</ServiceLineId>
|
|
89
|
+
<Date>2026-07-03</Date>
|
|
90
|
+
<SequenceNumber>10</SequenceNumber>
|
|
91
|
+
<Status>XX</Status>
|
|
92
|
+
</ServiceStatus>
|
|
93
|
+
</ServiceStatuses>
|
|
94
|
+
</CancelServicesReply>
|
|
95
|
+
</Reply>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Plugin Return Shape
|
|
99
|
+
|
|
100
|
+
The plugin returns both:
|
|
101
|
+
|
|
102
|
+
- `cancelServicesReply`: raw parsed `CancelServicesReply` object
|
|
103
|
+
- `cancellation`: normalized object
|
|
104
|
+
- `id`: from `BookingId`
|
|
105
|
+
- `status`: aggregate over all `ServiceStatus.Status` values:
|
|
106
|
+
- same status on all lines => that status (e.g. `XX`)
|
|
107
|
+
- mixed line statuses => `MIXED`
|
|
@@ -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(
|
|
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/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const Normalizer = require('./normalizer');
|
|
|
10
10
|
|
|
11
11
|
const { searchAvailabilityForItinerary } = require('./availability/itinerary-availability');
|
|
12
12
|
const { addServiceToItinerary } = require('./itinerary-add-service');
|
|
13
|
+
const { cancelBooking } = require('./itinerary-cancel-booking');
|
|
13
14
|
const { searchProductsForItinerary } = require('./itinerary-products-search');
|
|
14
15
|
const { searchItineraries } = require('./itinerary-search');
|
|
15
16
|
const { hostConnectXmlOptions } = require('./utils');
|
|
@@ -679,6 +680,19 @@ class BuyerPlugin {
|
|
|
679
680
|
});
|
|
680
681
|
}
|
|
681
682
|
|
|
683
|
+
async cancelBooking({
|
|
684
|
+
axios,
|
|
685
|
+
token,
|
|
686
|
+
payload,
|
|
687
|
+
}) {
|
|
688
|
+
return cancelBooking({
|
|
689
|
+
axios,
|
|
690
|
+
token,
|
|
691
|
+
payload,
|
|
692
|
+
callTourplan: this.callTourplan.bind(this),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
682
696
|
async searchItineraries({
|
|
683
697
|
axios,
|
|
684
698
|
typeDefsAndQueries,
|
package/index.test.js
CHANGED
|
@@ -132,6 +132,149 @@ describe('search tests', () => {
|
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
|
+
describe('cancelBooking', () => {
|
|
136
|
+
it('cancels a booking by id', async () => {
|
|
137
|
+
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
138
|
+
CancelServicesReply: {
|
|
139
|
+
BookingId: '316559',
|
|
140
|
+
Ref: 'A2IN111975',
|
|
141
|
+
ServiceStatuses: {
|
|
142
|
+
ServiceStatus: {
|
|
143
|
+
ServiceLineId: '61738',
|
|
144
|
+
Date: '2026-07-03',
|
|
145
|
+
SequenceNumber: '10',
|
|
146
|
+
Status: 'XX',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
const retVal = await app.cancelBooking({
|
|
152
|
+
axios,
|
|
153
|
+
token,
|
|
154
|
+
payload: {
|
|
155
|
+
id: '316559',
|
|
156
|
+
status: 'Cancelled',
|
|
157
|
+
clicked: 1719571452,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
expect(mockCallTourplan).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
|
161
|
+
model: {
|
|
162
|
+
CancelServicesRequest: expect.objectContaining({
|
|
163
|
+
BookingId: '316559',
|
|
164
|
+
ReturnBooking: 'Y',
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
}));
|
|
168
|
+
expect(retVal).toEqual({
|
|
169
|
+
cancelServicesReply: {
|
|
170
|
+
BookingId: '316559',
|
|
171
|
+
Ref: 'A2IN111975',
|
|
172
|
+
ServiceStatuses: {
|
|
173
|
+
ServiceStatus: {
|
|
174
|
+
ServiceLineId: '61738',
|
|
175
|
+
Date: '2026-07-03',
|
|
176
|
+
SequenceNumber: '10',
|
|
177
|
+
Status: 'XX',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
cancellation: {
|
|
182
|
+
id: '316559',
|
|
183
|
+
status: 'XX',
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('cancels a booking by ref', async () => {
|
|
189
|
+
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
190
|
+
CancelServicesReply: {
|
|
191
|
+
BookingId: '316559',
|
|
192
|
+
ServiceStatuses: {
|
|
193
|
+
ServiceStatus: [{
|
|
194
|
+
ServiceLineId: '61738',
|
|
195
|
+
Status: 'XX',
|
|
196
|
+
}],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
}));
|
|
200
|
+
const retVal = await app.cancelBooking({
|
|
201
|
+
axios,
|
|
202
|
+
token,
|
|
203
|
+
payload: {
|
|
204
|
+
ref: 'ALFI393706',
|
|
205
|
+
status: 'Cancelled',
|
|
206
|
+
clicked: 1719571452,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
expect(mockCallTourplan).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
|
210
|
+
model: {
|
|
211
|
+
CancelServicesRequest: expect.objectContaining({
|
|
212
|
+
Ref: 'ALFI393706',
|
|
213
|
+
ReturnBooking: 'Y',
|
|
214
|
+
}),
|
|
215
|
+
},
|
|
216
|
+
}));
|
|
217
|
+
expect(retVal).toEqual({
|
|
218
|
+
cancelServicesReply: {
|
|
219
|
+
BookingId: '316559',
|
|
220
|
+
ServiceStatuses: {
|
|
221
|
+
ServiceStatus: [{
|
|
222
|
+
ServiceLineId: '61738',
|
|
223
|
+
Status: 'XX',
|
|
224
|
+
}],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
cancellation: {
|
|
228
|
+
id: '316559',
|
|
229
|
+
status: 'XX',
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('returns MIXED status when service statuses differ', async () => {
|
|
235
|
+
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
236
|
+
CancelServicesReply: {
|
|
237
|
+
BookingId: '316559',
|
|
238
|
+
ServiceStatuses: {
|
|
239
|
+
ServiceStatus: [{
|
|
240
|
+
ServiceLineId: '61738',
|
|
241
|
+
Status: 'XX',
|
|
242
|
+
}, {
|
|
243
|
+
ServiceLineId: '61739',
|
|
244
|
+
Status: 'RQ',
|
|
245
|
+
}],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
}));
|
|
249
|
+
const retVal = await app.cancelBooking({
|
|
250
|
+
axios,
|
|
251
|
+
token,
|
|
252
|
+
payload: {
|
|
253
|
+
bookingId: '316559',
|
|
254
|
+
status: 'Cancelled',
|
|
255
|
+
clicked: 1719571452,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
expect(retVal).toEqual({
|
|
259
|
+
cancelServicesReply: {
|
|
260
|
+
BookingId: '316559',
|
|
261
|
+
ServiceStatuses: {
|
|
262
|
+
ServiceStatus: [{
|
|
263
|
+
ServiceLineId: '61738',
|
|
264
|
+
Status: 'XX',
|
|
265
|
+
}, {
|
|
266
|
+
ServiceLineId: '61739',
|
|
267
|
+
Status: 'RQ',
|
|
268
|
+
}],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
cancellation: {
|
|
272
|
+
id: '316559',
|
|
273
|
+
status: 'MIXED',
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
135
278
|
describe('template tests', () => {
|
|
136
279
|
let template;
|
|
137
280
|
it('get the template', async () => {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const R = require('ramda');
|
|
3
|
+
const {
|
|
4
|
+
hostConnectXmlOptions,
|
|
5
|
+
escapeInvalidXmlChars,
|
|
6
|
+
} = require('./utils');
|
|
7
|
+
|
|
8
|
+
const getIdentifier = payload => {
|
|
9
|
+
const bookingId = payload.bookingId || payload.id;
|
|
10
|
+
if (bookingId) {
|
|
11
|
+
return {
|
|
12
|
+
field: 'BookingId',
|
|
13
|
+
value: String(bookingId),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const bookingRef = payload.ref || payload.reference;
|
|
17
|
+
if (bookingRef) {
|
|
18
|
+
return {
|
|
19
|
+
field: 'Ref',
|
|
20
|
+
value: escapeInvalidXmlChars(String(bookingRef)),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const extractCancellationReply = replyObj => (
|
|
27
|
+
R.path(['CancelServicesReply'], replyObj)
|
|
28
|
+
|| {}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const getServiceStatusList = cancellationReply => {
|
|
32
|
+
let serviceStatuses = R.pathOr([], ['ServiceStatuses', 'ServiceStatus'], cancellationReply);
|
|
33
|
+
if (!Array.isArray(serviceStatuses)) serviceStatuses = [serviceStatuses];
|
|
34
|
+
return serviceStatuses
|
|
35
|
+
.map(serviceStatus => R.path(['Status'], serviceStatus))
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.map(status => String(status).toUpperCase());
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getAggregateServiceStatus = cancellationReply => {
|
|
41
|
+
const statuses = getServiceStatusList(cancellationReply);
|
|
42
|
+
if (!statuses.length) return null;
|
|
43
|
+
const uniqueStatuses = [...new Set(statuses)];
|
|
44
|
+
if (uniqueStatuses.length === 1) return uniqueStatuses[0];
|
|
45
|
+
return 'MIXED';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const normalizeCancellationResponse = (replyObj, payload, identifier) => {
|
|
49
|
+
const cancellationReply = extractCancellationReply(replyObj);
|
|
50
|
+
const aggregateServiceStatus = getAggregateServiceStatus(cancellationReply);
|
|
51
|
+
return {
|
|
52
|
+
cancelServicesReply: cancellationReply,
|
|
53
|
+
cancellation: {
|
|
54
|
+
id: R.path(['BookingId'], cancellationReply) || identifier.value,
|
|
55
|
+
status: aggregateServiceStatus
|
|
56
|
+
|| R.path(['BookingStatus'], cancellationReply)
|
|
57
|
+
|| R.path(['Status'], cancellationReply)
|
|
58
|
+
|| payload.status
|
|
59
|
+
|| 'Cancelled',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Cancels a booking by id or reference
|
|
66
|
+
* @param {Object} params - The parameters for the cancellation
|
|
67
|
+
* @param {Object} params.axios - The axios instance
|
|
68
|
+
* @param {Object} params.token - The token for the cancellation
|
|
69
|
+
* @param {Object} params.payload - The payload for the cancellation
|
|
70
|
+
* @param {Object} params.callTourplan - The callTourplan function
|
|
71
|
+
* @returns {Promise<Object>} The cancellation response
|
|
72
|
+
*/
|
|
73
|
+
const cancelBooking = async ({
|
|
74
|
+
axios,
|
|
75
|
+
token: {
|
|
76
|
+
hostConnectEndpoint,
|
|
77
|
+
hostConnectAgentID,
|
|
78
|
+
hostConnectAgentPassword,
|
|
79
|
+
},
|
|
80
|
+
payload = {},
|
|
81
|
+
callTourplan,
|
|
82
|
+
}) => {
|
|
83
|
+
assert(hostConnectEndpoint, 'Must provide token.hostConnectEndpoint for booking cancellation');
|
|
84
|
+
assert(hostConnectAgentID, 'Must provide token.hostConnectAgentID for booking cancellation');
|
|
85
|
+
assert(hostConnectAgentPassword, 'Must provide token.hostConnectAgentPassword for booking cancellation');
|
|
86
|
+
const identifier = getIdentifier(payload);
|
|
87
|
+
assert(identifier, 'Must provide booking id or reference for cancellation');
|
|
88
|
+
|
|
89
|
+
const baseRequest = {
|
|
90
|
+
AgentID: hostConnectAgentID,
|
|
91
|
+
Password: hostConnectAgentPassword,
|
|
92
|
+
[identifier.field]: identifier.value,
|
|
93
|
+
};
|
|
94
|
+
const replyObj = await callTourplan({
|
|
95
|
+
model: {
|
|
96
|
+
CancelServicesRequest: {
|
|
97
|
+
...baseRequest,
|
|
98
|
+
ReturnBooking: 'Y',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
endpoint: hostConnectEndpoint,
|
|
102
|
+
axios,
|
|
103
|
+
xmlOptions: hostConnectXmlOptions,
|
|
104
|
+
});
|
|
105
|
+
return normalizeCancellationResponse(replyObj, payload, identifier);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
cancelBooking,
|
|
110
|
+
};
|