ti2-tourplan 1.0.128 → 1.0.129-beta.1
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/.github/workflows/nodejs.yml +8 -7
- package/index.test.js +65 -1
- package/package.json +1 -1
- package/utils.js +23 -2
- package/utils.test.js +102 -1
|
@@ -22,12 +22,17 @@ jobs:
|
|
|
22
22
|
publish-npm:
|
|
23
23
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
24
24
|
needs: build_and_test
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
id-token: write
|
|
25
28
|
runs-on: ubuntu-latest
|
|
26
29
|
steps:
|
|
27
|
-
- uses: actions/checkout@
|
|
28
|
-
- uses: actions/setup-node@
|
|
30
|
+
- uses: actions/checkout@v6
|
|
31
|
+
- uses: actions/setup-node@v6
|
|
29
32
|
with:
|
|
30
|
-
node-version:
|
|
33
|
+
node-version: '24'
|
|
34
|
+
registry-url: 'https://registry.npmjs.org'
|
|
35
|
+
package-manager-cache: false
|
|
31
36
|
- name: Validate release tag
|
|
32
37
|
id: release_meta
|
|
33
38
|
env:
|
|
@@ -55,9 +60,5 @@ jobs:
|
|
|
55
60
|
|
|
56
61
|
echo "tag_version=${TAG_VERSION}" >> "$GITHUB_OUTPUT"
|
|
57
62
|
echo "dist_tag=${DIST_TAG}" >> "$GITHUB_OUTPUT"
|
|
58
|
-
- name: Configure npm auth
|
|
59
|
-
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
|
60
|
-
env:
|
|
61
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
62
63
|
- name: Publish package
|
|
63
64
|
run: npm publish --tag "${{ steps.release_meta.outputs.dist_tag }}"
|
package/index.test.js
CHANGED
|
@@ -5,10 +5,11 @@ const path = require('path');
|
|
|
5
5
|
const xml2js = require('xml2js');
|
|
6
6
|
const R = require('ramda');
|
|
7
7
|
const hash = require('object-hash');
|
|
8
|
+
const js2xmlparser = require('js2xmlparser');
|
|
8
9
|
const { XMLParser } = require('fast-xml-parser');
|
|
9
10
|
const { typeDefs: itineraryProductTypeDefs, query: itineraryProductQuery } = require('./node_modules/ti2/controllers/graphql-schemas/itinerary-product');
|
|
10
11
|
const { typeDefs: itineraryBookingTypeDefs, query: itineraryBookingQuery } = require('./node_modules/ti2/controllers/graphql-schemas/itinerary-booking');
|
|
11
|
-
const { CUSTOM_RATE_ID_NAME } = require('./utils');
|
|
12
|
+
const { CUSTOM_RATE_ID_NAME, hostConnectXmlOptions } = require('./utils');
|
|
12
13
|
const {
|
|
13
14
|
getRatesObjectArray,
|
|
14
15
|
getImmediateLastDateRange,
|
|
@@ -169,6 +170,69 @@ describe('search tests', () => {
|
|
|
169
170
|
expect(newBookingName).toBe(`${'Ae'.repeat(29)}A`);
|
|
170
171
|
});
|
|
171
172
|
|
|
173
|
+
it('serializes valid DOB instead of age and falls back to age for invalid DOB', async () => {
|
|
174
|
+
mockCallTourplan
|
|
175
|
+
.mockImplementationOnce(async () => ({
|
|
176
|
+
AddServiceReply: { BookingId: 'valid-dob' },
|
|
177
|
+
}))
|
|
178
|
+
.mockImplementationOnce(async () => ({
|
|
179
|
+
AddServiceReply: { BookingId: 'invalid-dob' },
|
|
180
|
+
}));
|
|
181
|
+
const payloadFor = passenger => ({
|
|
182
|
+
quoteName: 'DOB serialization test',
|
|
183
|
+
optionId: 'ABC123',
|
|
184
|
+
startDate: '2026-07-03',
|
|
185
|
+
reference: 'TESTREF',
|
|
186
|
+
paxConfigs: [{
|
|
187
|
+
roomType: 'Double',
|
|
188
|
+
adults: 1,
|
|
189
|
+
passengers: [passenger],
|
|
190
|
+
}],
|
|
191
|
+
notes: '',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await app.addServiceToItinerary({
|
|
195
|
+
axios,
|
|
196
|
+
token,
|
|
197
|
+
payload: payloadFor({
|
|
198
|
+
firstName: 'Ada',
|
|
199
|
+
lastName: 'Lovelace',
|
|
200
|
+
passengerType: 'Adult',
|
|
201
|
+
dob: '1990-02-28',
|
|
202
|
+
age: 36,
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
await app.addServiceToItinerary({
|
|
206
|
+
axios,
|
|
207
|
+
token,
|
|
208
|
+
payload: payloadFor({
|
|
209
|
+
firstName: 'Grace',
|
|
210
|
+
lastName: 'Hopper',
|
|
211
|
+
passengerType: 'Adult',
|
|
212
|
+
dob: '1990-02-29',
|
|
213
|
+
age: 36,
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const validModel = mockCallTourplan.mock.calls[0][0].model;
|
|
218
|
+
const invalidModel = mockCallTourplan.mock.calls[1][0].model;
|
|
219
|
+
const validPax = validModel.AddServiceRequest.RoomConfigs.RoomConfig[0]
|
|
220
|
+
.PaxList.PaxDetails[0];
|
|
221
|
+
const invalidPax = invalidModel.AddServiceRequest.RoomConfigs.RoomConfig[0]
|
|
222
|
+
.PaxList.PaxDetails[0];
|
|
223
|
+
const validXml = js2xmlparser.parse('Request', validModel, hostConnectXmlOptions);
|
|
224
|
+
const invalidXml = js2xmlparser.parse('Request', invalidModel, hostConnectXmlOptions);
|
|
225
|
+
|
|
226
|
+
expect(validPax).toMatchObject({ DateOfBirth: '1990-02-28' });
|
|
227
|
+
expect(validPax).not.toHaveProperty('Age');
|
|
228
|
+
expect(validXml).toContain('<DateOfBirth>1990-02-28</DateOfBirth>');
|
|
229
|
+
expect(validXml).not.toContain('<Age>');
|
|
230
|
+
expect(invalidPax).toMatchObject({ Age: 36 });
|
|
231
|
+
expect(invalidPax).not.toHaveProperty('DateOfBirth');
|
|
232
|
+
expect(invalidXml).toContain('<Age>36</Age>');
|
|
233
|
+
expect(invalidXml).not.toContain('<DateOfBirth>');
|
|
234
|
+
});
|
|
235
|
+
|
|
172
236
|
it('limits new booking names supplied by directHeaderPayload after escaping XML characters', async () => {
|
|
173
237
|
mockCallTourplan.mockImplementationOnce(async () => ({
|
|
174
238
|
AddServiceReply: {
|
package/package.json
CHANGED
package/utils.js
CHANGED
|
@@ -71,6 +71,26 @@ const escapeInvalidXmlChars = str => {
|
|
|
71
71
|
.replace(BAD_XML_CHARS, '');
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
const getValidDateOfBirth = dob => {
|
|
75
|
+
if (typeof dob !== 'string') return undefined;
|
|
76
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dob);
|
|
77
|
+
if (!match) return undefined;
|
|
78
|
+
|
|
79
|
+
const year = Number(match[1]);
|
|
80
|
+
const month = Number(match[2]);
|
|
81
|
+
const day = Number(match[3]);
|
|
82
|
+
if (year === 0) return undefined;
|
|
83
|
+
|
|
84
|
+
// setUTCFullYear avoids Date.UTC's 0-99 year remapping.
|
|
85
|
+
const date = new Date(0);
|
|
86
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
87
|
+
if (date.getUTCFullYear() !== year ||
|
|
88
|
+
date.getUTCMonth() !== month - 1 ||
|
|
89
|
+
date.getUTCDate() !== day) return undefined;
|
|
90
|
+
|
|
91
|
+
return dob;
|
|
92
|
+
};
|
|
93
|
+
|
|
74
94
|
const getRoomConfigs = (paxConfigs, noPaxList) => {
|
|
75
95
|
// There should be only 1 RoomConfigs for AddServiceRequest
|
|
76
96
|
const RoomConfigs = {};
|
|
@@ -139,12 +159,13 @@ const getRoomConfigs = (paxConfigs, noPaxList) => {
|
|
|
139
159
|
}[p.passengerType] || 'A',
|
|
140
160
|
};
|
|
141
161
|
if (p.salutation) EachPaxDetails.Title = escapeInvalidXmlChars(p.salutation);
|
|
142
|
-
|
|
162
|
+
const validDateOfBirth = getValidDateOfBirth(p.dob);
|
|
163
|
+
if (validDateOfBirth) EachPaxDetails.DateOfBirth = validDateOfBirth;
|
|
143
164
|
// NOTE: TourPlan API doesn't accept age as empty string, i.e. empty XML tag <Age/>
|
|
144
165
|
// and trhows and error like - "1000 SCN System.InvalidOperationException: There is an
|
|
145
166
|
// error in XML document (29, 8). (Input string was not in a correct format.)"
|
|
146
167
|
// The solution is to NOT send the Age tag if it's empty
|
|
147
|
-
if (!R.isNil(p.age) && !Number.isNaN(p.age) && p.age) {
|
|
168
|
+
if (!validDateOfBirth && !R.isNil(p.age) && !Number.isNaN(p.age) && p.age) {
|
|
148
169
|
if (!(p.passengerType === passengerTypeMap.Adult && p.age === 0)) {
|
|
149
170
|
EachPaxDetails.Age = p.age;
|
|
150
171
|
}
|
package/utils.test.js
CHANGED
|
@@ -1,4 +1,105 @@
|
|
|
1
|
-
const { escapeInvalidXmlChars } = require('./utils');
|
|
1
|
+
const { escapeInvalidXmlChars, getRoomConfigs } = require('./utils');
|
|
2
|
+
|
|
3
|
+
describe('getRoomConfigs', () => {
|
|
4
|
+
const getPaxDetails = passenger => getRoomConfigs([{
|
|
5
|
+
roomType: 'Double',
|
|
6
|
+
passengers: [passenger],
|
|
7
|
+
}]).RoomConfig[0].PaxList.PaxDetails[0];
|
|
8
|
+
|
|
9
|
+
it('uses a valid DOB instead of age', () => {
|
|
10
|
+
const withDobAndAge = getPaxDetails({
|
|
11
|
+
firstName: 'Ada',
|
|
12
|
+
lastName: 'Lovelace',
|
|
13
|
+
passengerType: 'Adult',
|
|
14
|
+
dob: '1990-02-28',
|
|
15
|
+
age: 36,
|
|
16
|
+
});
|
|
17
|
+
const withDobOnly = getPaxDetails({
|
|
18
|
+
passengerType: 'Child',
|
|
19
|
+
dob: '2000-02-29',
|
|
20
|
+
});
|
|
21
|
+
const withPre100Dob = getPaxDetails({
|
|
22
|
+
passengerType: 'Adult',
|
|
23
|
+
dob: '0099-12-31',
|
|
24
|
+
age: 99,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(withDobAndAge.DateOfBirth).toBe('1990-02-28');
|
|
28
|
+
expect(withDobAndAge).not.toHaveProperty('Age');
|
|
29
|
+
expect(withDobOnly.DateOfBirth).toBe('2000-02-29');
|
|
30
|
+
expect(withDobOnly).not.toHaveProperty('Age');
|
|
31
|
+
expect(withPre100Dob.DateOfBirth).toBe('0099-12-31');
|
|
32
|
+
expect(withPre100Dob).not.toHaveProperty('Age');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('uses age when DOB is absent', () => {
|
|
36
|
+
const paxDetails = getPaxDetails({
|
|
37
|
+
passengerType: 'Adult',
|
|
38
|
+
age: 36,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(paxDetails.Age).toBe(36);
|
|
42
|
+
expect(paxDetails).not.toHaveProperty('DateOfBirth');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it.each([
|
|
46
|
+
['blank', ''],
|
|
47
|
+
['malformed', '1990/02/28'],
|
|
48
|
+
['unpadded', '1990-2-28'],
|
|
49
|
+
['date-time', '1990-02-28T00:00:00Z'],
|
|
50
|
+
['boxed string', Object('1990-02-28')],
|
|
51
|
+
['Date object', new Date('1990-02-28T00:00:00Z')],
|
|
52
|
+
['number', 19900228],
|
|
53
|
+
])('falls back to age for a %s DOB', (description, dob) => {
|
|
54
|
+
const paxDetails = getPaxDetails({
|
|
55
|
+
passengerType: 'Adult',
|
|
56
|
+
dob,
|
|
57
|
+
age: 36,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(paxDetails.Age).toBe(36);
|
|
61
|
+
expect(paxDetails).not.toHaveProperty('DateOfBirth');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each([
|
|
65
|
+
['year zero', '0000-01-01'],
|
|
66
|
+
['month zero', '1990-00-01'],
|
|
67
|
+
['day zero', '1990-01-00'],
|
|
68
|
+
['day rollover', '1990-04-31'],
|
|
69
|
+
['non-leap day', '2025-02-29'],
|
|
70
|
+
['non-leap century day', '1900-02-29'],
|
|
71
|
+
])('falls back to age for an impossible %s DOB', (description, dob) => {
|
|
72
|
+
const paxDetails = getPaxDetails({
|
|
73
|
+
passengerType: 'Adult',
|
|
74
|
+
dob,
|
|
75
|
+
age: 36,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(paxDetails.Age).toBe(36);
|
|
79
|
+
expect(paxDetails).not.toHaveProperty('DateOfBirth');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('preserves the PersonId early return', () => {
|
|
83
|
+
expect(getPaxDetails({
|
|
84
|
+
personId: 'person-123',
|
|
85
|
+
passengerType: 'Adult',
|
|
86
|
+
dob: '1990-02-28',
|
|
87
|
+
age: 36,
|
|
88
|
+
})).toEqual({ PersonId: 'person-123' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('preserves zero, blank, and NaN age behavior without a valid DOB', () => {
|
|
92
|
+
const adultZero = getPaxDetails({ passengerType: 'Adult', age: 0 });
|
|
93
|
+
const childZero = getPaxDetails({ passengerType: 'Child', age: 0 });
|
|
94
|
+
const blank = getPaxDetails({ passengerType: 'Adult', age: '' });
|
|
95
|
+
const nan = getPaxDetails({ passengerType: 'Adult', age: NaN });
|
|
96
|
+
|
|
97
|
+
[adultZero, childZero, blank, nan].forEach(paxDetails => {
|
|
98
|
+
expect(paxDetails).not.toHaveProperty('Age');
|
|
99
|
+
expect(paxDetails).not.toHaveProperty('DateOfBirth');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
2
103
|
|
|
3
104
|
describe('escapeInvalidXmlChars', () => {
|
|
4
105
|
it('returns empty string for falsy input', () => {
|