ti2-tourplan 1.0.122 → 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/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`
|
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
|
+
};
|