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.
@@ -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@v2
28
- - uses: actions/setup-node@v1
30
+ - uses: actions/checkout@v6
31
+ - uses: actions/setup-node@v6
29
32
  with:
30
- node-version: 12
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-tourplan",
3
- "version": "1.0.128",
3
+ "version": "1.0.129-beta.1",
4
4
  "description": "Tourplan's TI2 Plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- if (p.dob) EachPaxDetails.DateOfBirth = p.dob;
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', () => {