ti2-tourplan 1.0.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/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Ti2's tourplan plugin
2
+
3
+ ## Getting Started
4
+
5
+ ### Requirements
6
+
7
+ Some environment variables are required for this plugin to run it's tests
8
+
9
+ - apiKey
10
+ - endpoint
11
+
12
+ ## Contributing
13
+
14
+ Contributions are welcome and ecouraged.
15
+
16
+ If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
17
+
18
+ - Fork the Project
19
+ - Create your Feature Branch (git checkout -b feature/AmazingFeature)
20
+ - Commit your Changes (git commit -m 'Add some AmazingFeature')
21
+ - Push to the Branch (git push origin feature/AmazingFeature)
22
+ - Open a Pull Request
23
+
24
+ Feel free to check the [Issues Page](https://github.com/TourConnect/ti2-tourplan/issues).
25
+
26
+ ## License
27
+
28
+ Distributed under the GPL-3 License. See LICENSE.txt for more information.
29
+
30
+ TL;DR Here's what the license entails:
31
+
32
+ 1. Anyone can copy, modify and distribute this software.
33
+ 2. You have to include the license and copyright notice with each and every distribution.
34
+ 3. You can use this software privately.
35
+ 4. You can use this software for commercial purposes.
36
+ 5. If you dare build your business solely from this code, you risk open-sourcing the whole code base.
37
+ 6. If you modify it, you have to indicate changes made to the code.
38
+ 7. Any modifications of this code base MUST be distributed with the same license, GPLv3.
39
+ 8. This software is provided without warranty.
40
+ 9. The software author or license can not be held liable for any damages inflicted by the software.
@@ -0,0 +1,216 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`search tests read allotment not empty 1`] = `
4
+ Object {
5
+ "allotment": Array [
6
+ Object {
7
+ "appliesTo": "Supplier",
8
+ "bySplitCode": Object {
9
+ "GENERAL": Object {
10
+ "byUnitType": Object {
11
+ "RM": Object {
12
+ "01/08/2020": Object {
13
+ "booked": "0",
14
+ "max": "0",
15
+ "release": "999",
16
+ "request": false,
17
+ },
18
+ "02/08/2020": Object {
19
+ "booked": "0",
20
+ "max": "0",
21
+ "release": "999",
22
+ "request": false,
23
+ },
24
+ "03/08/2020": Object {
25
+ "booked": "0",
26
+ "max": "0",
27
+ "release": "999",
28
+ "request": false,
29
+ },
30
+ "04/08/2020": Object {
31
+ "booked": "0",
32
+ "max": "0",
33
+ "release": "999",
34
+ "request": false,
35
+ },
36
+ "05/08/2020": Object {
37
+ "booked": "0",
38
+ "max": "0",
39
+ "release": "999",
40
+ "request": false,
41
+ },
42
+ "06/08/2020": Object {
43
+ "booked": "0",
44
+ "max": "0",
45
+ "release": "999",
46
+ "request": false,
47
+ },
48
+ "07/08/2020": Object {
49
+ "booked": "0",
50
+ "max": "0",
51
+ "release": "999",
52
+ "request": false,
53
+ },
54
+ "08/08/2020": Object {
55
+ "booked": "0",
56
+ "max": "0",
57
+ "release": "999",
58
+ "request": false,
59
+ },
60
+ "09/08/2020": Object {
61
+ "booked": "0",
62
+ "max": "0",
63
+ "release": "999",
64
+ "request": false,
65
+ },
66
+ "10/08/2020": Object {
67
+ "booked": "0",
68
+ "max": "0",
69
+ "release": "999",
70
+ "request": false,
71
+ },
72
+ "11/08/2020": Object {
73
+ "booked": "0",
74
+ "max": "0",
75
+ "release": "999",
76
+ "request": false,
77
+ },
78
+ "12/08/2020": Object {
79
+ "booked": "0",
80
+ "max": "0",
81
+ "release": "999",
82
+ "request": false,
83
+ },
84
+ "13/08/2020": Object {
85
+ "booked": "0",
86
+ "max": "0",
87
+ "release": "999",
88
+ "request": false,
89
+ },
90
+ "14/08/2020": Object {
91
+ "booked": "0",
92
+ "max": "0",
93
+ "release": "999",
94
+ "request": false,
95
+ },
96
+ "15/08/2020": Object {
97
+ "booked": "0",
98
+ "max": "0",
99
+ "release": "999",
100
+ "request": false,
101
+ },
102
+ },
103
+ },
104
+ },
105
+ },
106
+ "description": "2021 Reboot Year",
107
+ "name": "2021 REBOOT",
108
+ "supplierId": "MAGLUX",
109
+ },
110
+ Object {
111
+ "appliesTo": "Supplier",
112
+ "bySplitCode": Object {
113
+ "GENERAL": Object {
114
+ "byUnitType": Object {
115
+ "RM": Object {
116
+ "01/08/2020": Object {
117
+ "booked": "0",
118
+ "max": "3",
119
+ "release": "14",
120
+ "request": true,
121
+ },
122
+ "02/08/2020": Object {
123
+ "booked": "0",
124
+ "max": "3",
125
+ "release": "14",
126
+ "request": true,
127
+ },
128
+ "03/08/2020": Object {
129
+ "booked": "0",
130
+ "max": "3",
131
+ "release": "14",
132
+ "request": true,
133
+ },
134
+ "04/08/2020": Object {
135
+ "booked": "0",
136
+ "max": "3",
137
+ "release": "14",
138
+ "request": true,
139
+ },
140
+ "05/08/2020": Object {
141
+ "booked": "0",
142
+ "max": "3",
143
+ "release": "14",
144
+ "request": true,
145
+ },
146
+ "06/08/2020": Object {
147
+ "booked": "0",
148
+ "max": "3",
149
+ "release": "14",
150
+ "request": true,
151
+ },
152
+ "07/08/2020": Object {
153
+ "booked": "0",
154
+ "max": "3",
155
+ "release": "14",
156
+ "request": true,
157
+ },
158
+ "08/08/2020": Object {
159
+ "booked": "0",
160
+ "max": "3",
161
+ "release": "14",
162
+ "request": true,
163
+ },
164
+ "09/08/2020": Object {
165
+ "booked": "0",
166
+ "max": "3",
167
+ "release": "14",
168
+ "request": true,
169
+ },
170
+ "10/08/2020": Object {
171
+ "booked": "0",
172
+ "max": "3",
173
+ "release": "14",
174
+ "request": true,
175
+ },
176
+ "11/08/2020": Object {
177
+ "booked": "0",
178
+ "max": "3",
179
+ "release": "14",
180
+ "request": true,
181
+ },
182
+ "12/08/2020": Object {
183
+ "booked": "0",
184
+ "max": "3",
185
+ "release": "14",
186
+ "request": true,
187
+ },
188
+ "13/08/2020": Object {
189
+ "booked": "0",
190
+ "max": "3",
191
+ "release": "14",
192
+ "request": true,
193
+ },
194
+ "14/08/2020": Object {
195
+ "booked": "0",
196
+ "max": "3",
197
+ "release": "14",
198
+ "request": true,
199
+ },
200
+ "15/08/2020": Object {
201
+ "booked": "0",
202
+ "max": "3",
203
+ "release": "14",
204
+ "request": true,
205
+ },
206
+ },
207
+ },
208
+ },
209
+ },
210
+ "description": "",
211
+ "name": "FREESALE",
212
+ "supplierId": "MAGLUX",
213
+ },
214
+ ],
215
+ }
216
+ `;
package/index.js ADDED
@@ -0,0 +1,131 @@
1
+ // const axios = require('axios');
2
+ const R = require('ramda');
3
+ const assert = require('assert');
4
+ const moment = require('moment');
5
+ const js2xmlparser = require('js2xmlparser');
6
+ const xml2js = require('xml2js');
7
+ const axios = require('axios');
8
+
9
+ const Normalizer = require('./normalizer');
10
+
11
+ const xmlParser = new xml2js.Parser();
12
+
13
+ const xmlOptions = {
14
+ prettyPrinting: {
15
+ enabled: false,
16
+ },
17
+ dtd: {
18
+ include: true,
19
+ name: 'tourConnect_4_00_000.dtd',
20
+ },
21
+ };
22
+
23
+ const getHeaders = ({ length }) => ({
24
+ Accept: 'application/xml',
25
+ 'Content-Type': 'application/xml; charset=utf-8',
26
+ 'Content-Length': length,
27
+ });
28
+
29
+ class Plugin {
30
+ constructor(params) { // we get the env variables from here
31
+ Object.entries(params).forEach(([attr, value]) => {
32
+ this[attr] = value;
33
+ });
34
+ }
35
+
36
+ async queryAllotment({
37
+ token: {
38
+ endpoint = this.endpoint,
39
+ username = this.username,
40
+ password = this.password,
41
+ },
42
+ payload: {
43
+ dateFormat = 'DD/MM/YYYY',
44
+ startDate,
45
+ endDate,
46
+ keyPath,
47
+ },
48
+ }) {
49
+ assert(endpoint);
50
+ assert(startDate);
51
+ assert(endDate);
52
+ assert(keyPath, 'Must provide a supplier/product spec');
53
+ const keyPathArr = keyPath.split('|');
54
+ assert(keyPathArr.length > 1, 'Must provide a supplier id and a product id');
55
+ const supplierId = R.path([-2], keyPathArr);
56
+ const productId = R.path([-1], keyPathArr);
57
+ assert(supplierId);
58
+ assert(productId);
59
+ const model = {
60
+ GetInventoryRequest: {
61
+ SupplierCode: supplierId,
62
+ Date_From: moment(startDate, dateFormat).format('YYYY-MM-DD'),
63
+ Date_To: moment(endDate, dateFormat).format('YYYY-MM-DD'),
64
+ OptionCode: productId,
65
+ // AllocationName: '2021 REBOOT',
66
+ // Unit_Type: 'RM',
67
+ Login: username,
68
+ Password: password,
69
+ },
70
+ };
71
+ let data = Normalizer.stripEnclosingQuotes(
72
+ js2xmlparser.parse('Request', model, xmlOptions),
73
+ );
74
+ data = data.replace(xmlOptions.dtd.name, `Request SYSTEM "${xmlOptions.dtd.name}"`);
75
+ const reply = R.path(['data'], await axios({
76
+ metod: 'post',
77
+ url: endpoint,
78
+ data,
79
+ headers: getHeaders({ length: data.length }),
80
+ }));
81
+ const returnObj = await xmlParser.parseStringPromise(reply);
82
+ let allotment = R.path(
83
+ ['Reply', 'GetInventoryReply', 0, 'Allocation'],
84
+ returnObj,
85
+ );
86
+ // remove empty instances
87
+ allotment = allotment.filter(currentAllotment => Array.isArray(currentAllotment.Split));
88
+ allotment = allotment.map(currentAllotment => {
89
+ const appliesTo = R.path(['AllocationAppliesTo', 0, 'AllocationType', 0], currentAllotment);
90
+ const bySplitCode = {};
91
+ currentAllotment.Split.forEach(currentSplit => {
92
+ const currentSplitName = R.path(['Split_Code', 0], currentSplit);
93
+ const byUnitType = {};
94
+ R.path(['UnitTypeInventory'], currentSplit).forEach(currentUnitType => {
95
+ const currentUnitTypeName = R.path(['Unit_Type', 0], currentUnitType);
96
+ const inventoryByDay = {};
97
+ R.path(['PerDayInventory'], currentUnitType).forEach(dayInventory => {
98
+ const dayName = moment(R.path(['Date', 0], dayInventory), 'YYYY-MM-DD').format(dateFormat);
99
+ inventoryByDay[dayName] = {
100
+ release: dayInventory.Release_Period[0],
101
+ max: dayInventory.Max_Qty[0],
102
+ booked: dayInventory.Bkd_Qty[0],
103
+ request: dayInventory.Request_OK[0] === 'Y',
104
+ };
105
+ });
106
+ byUnitType[currentUnitTypeName] = {
107
+ ...(byUnitType[currentUnitTypeName] || {}),
108
+ ...inventoryByDay,
109
+ };
110
+ });
111
+ bySplitCode[currentSplitName] = {
112
+ ...(bySplitCode[currentSplitName] || {}),
113
+ byUnitType,
114
+ };
115
+ });
116
+ return {
117
+ supplierId: R.path(['SupplierCode', 0], currentAllotment),
118
+ name: R.path(['AllocationName', 0], currentAllotment),
119
+ description: R.path(['AllocationDescription', 0], currentAllotment),
120
+ appliesTo: {
121
+ S: 'Supplier',
122
+ P: 'Product',
123
+ }[appliesTo] || appliesTo,
124
+ bySplitCode,
125
+ };
126
+ });
127
+ return { allotment };
128
+ }
129
+ }
130
+
131
+ module.exports = Plugin;
package/index.test.js ADDED
@@ -0,0 +1,41 @@
1
+ /* globals describe, it, expect */
2
+ const Plugin = require('./index');
3
+
4
+ const app = new Plugin({});
5
+
6
+ describe('search tests', () => {
7
+ const token = {
8
+ endpoint: process.env.ti2_tourplan_endpoint,
9
+ username: process.env.ti2_tourplan_username,
10
+ password: process.env.ti2_tourplan_password,
11
+ };
12
+ const dateFormat = 'DD/MM/YYYY';
13
+ describe('allotments', () => {
14
+ it('read allotment empty', async () => {
15
+ const retVal = await app.queryAllotment({
16
+ token,
17
+ payload: {
18
+ dateFormat,
19
+ startDate: '01/08/2022',
20
+ endDate: '15/08/2022',
21
+ keyPath: 'MAGLUX|SYDACMAGLUXDELXRO',
22
+ },
23
+ });
24
+ expect(Array.isArray(retVal.allotment)).toBeTruthy();
25
+ expect(retVal.allotment.length).toBe(0);
26
+ });
27
+ });
28
+ it('read allotment not empty', async () => {
29
+ const retVal = await app.queryAllotment({
30
+ token,
31
+ payload: {
32
+ dateFormat,
33
+ startDate: '01/08/2020',
34
+ endDate: '15/08/2020',
35
+ keyPath: 'MAGLUX|SYDACMAGLUXDELXRO',
36
+ },
37
+ });
38
+ expect(Array.isArray(retVal.allotment)).toBeTruthy();
39
+ expect(retVal).toMatchSnapshot();
40
+ });
41
+ });
package/jest.setup.js ADDED
@@ -0,0 +1,38 @@
1
+ /* globals expect */
2
+ // should not require internals but is the only way to make it work on nested instances
3
+ const { equals } = require('expect/build/jasmineUtils');
4
+ const { diff: diffDefault } = require('jest-diff');
5
+
6
+ require('util').inspect.defaultOptions.depth = 8;
7
+
8
+ expect.extend({
9
+ toContainObject(receivedParam, argument) {
10
+ const received = (() => {
11
+ if (Array.isArray(receivedParam)) return receivedParam;
12
+ return [receivedParam];
13
+ })();
14
+ const pass = (() => {
15
+ if (Array.isArray(argument)) {
16
+ return equals(received,
17
+ expect.arrayContaining(argument.map(arg => expect.objectContaining(arg)))
18
+ );
19
+ }
20
+ return equals(received,
21
+ expect.arrayContaining([
22
+ expect.objectContaining(argument)
23
+ ])
24
+ );
25
+ })();
26
+ if (pass) {
27
+ return {
28
+ message: () => (`expected ${this.utils.printReceived(received)} not to contain object ${this.utils.printExpected(argument)}`),
29
+ pass: true
30
+ }
31
+ } else {
32
+ return {
33
+ message: () => (diffDefault(received, argument)),
34
+ pass: false
35
+ }
36
+ }
37
+ }
38
+ });
package/normalizer.js ADDED
@@ -0,0 +1,165 @@
1
+ const _ = require('underscore');
2
+
3
+ const mapDaysToNumbers = day => {
4
+ switch (day) {
5
+ case 'Mon':
6
+ return 1;
7
+ case 'Tue':
8
+ return 2;
9
+ case 'Wed':
10
+ return 3;
11
+ case 'Thu':
12
+ return 4;
13
+ case 'Fri':
14
+ return 5;
15
+ case 'Sat':
16
+ return 6;
17
+ case 'Sun':
18
+ return 7;
19
+ default:
20
+ return undefined;
21
+ }
22
+ };
23
+
24
+ const mapNumbersToDays = num => {
25
+ switch (num) {
26
+ case 1:
27
+ return 'Mon';
28
+ case 2:
29
+ return 'Tue';
30
+ case 3:
31
+ return 'Wed';
32
+ case 4:
33
+ return 'Thu';
34
+ case 5:
35
+ return 'Fri';
36
+ case 6:
37
+ return 'Sat';
38
+ case 7:
39
+ return 'Sun';
40
+ default:
41
+ return undefined;
42
+ }
43
+ };
44
+
45
+ const normalizeCurrency = valueParam => {
46
+ const value = parseFloat(valueParam);
47
+ if (Number.isNaN(value)) {
48
+ return 'n/a';
49
+ }
50
+ return (value % 1 === 0 ? value.toFixed(4) : value.toFixed(4));
51
+ };
52
+
53
+ const normalizeDate = date => {
54
+ if (!date) return '';
55
+ const d = new Date(date);
56
+ let month = (d.getUTCMonth() + 1).toString();
57
+ let day = d.getUTCDate().toString();
58
+ const year = d.getUTCFullYear().toString();
59
+
60
+ if (month.length < 2) month = `0${month}`;
61
+ if (day.length < 2) day = `0${day}`;
62
+
63
+ return [year, month, day].join('-');
64
+ };
65
+
66
+ const getConsectuiveDays = days => {
67
+ const dayNumbers = _.map(days, mapDaysToNumbers).sort();
68
+ let firstDay;
69
+ let lastDay;
70
+
71
+ for (let i = 0; i < dayNumbers.length; i += 1) {
72
+ const currentDay = dayNumbers[i];
73
+ if (!firstDay) firstDay = currentDay;
74
+ lastDay = currentDay;
75
+
76
+ if ((i > 0) && (i < dayNumbers.length)) {
77
+ const previousDay = dayNumbers[i - 1];
78
+ if (currentDay - previousDay > 1) return false;
79
+ }
80
+ }
81
+
82
+ return `${mapNumbersToDays(firstDay)} - ${mapNumbersToDays(lastDay)}`;
83
+ };
84
+
85
+ module.exports = {
86
+
87
+ stripEnclosingQuotes: strParam => {
88
+ let str = strParam;
89
+ if (str && str.charAt(0) === '"') {
90
+ str = str.slice(1);
91
+ }
92
+ return str;
93
+ },
94
+
95
+ deepContains: (dataArray, searchItem) => {
96
+ for (let i = 0; i < dataArray.length; i += 1) {
97
+ if (_.isEqual(dataArray[i], searchItem)) return true;
98
+ }
99
+ return false;
100
+ },
101
+
102
+ normalizeDateRange: dateRangeParam => {
103
+ const dateRange = {
104
+ fromDate: normalizeDate(dateRangeParam.start_date),
105
+ toDate: normalizeDate(dateRangeParam.end_date),
106
+ };
107
+ if (dateRangeParam.name) dateRange.name = dateRangeParam.name;
108
+ return dateRange;
109
+ },
110
+
111
+ normalizeNA: str => {
112
+ const na = ['N/A', 'NA', 'ON REQUEST'];
113
+ return str && _.indexOf(na, str.trim().toUpperCase()) === -1 ? str.trim() : null;
114
+ },
115
+
116
+ nullableTrim: str => (str ? str.trim() : str),
117
+
118
+ prettifyDays: days => {
119
+ let consecutiveDays;
120
+ if (days && days.length > 0) {
121
+ if (days.length === 7) return 'Everyday';
122
+ if (days.length === 1) return `${days[0]} Only`;
123
+
124
+ consecutiveDays = getConsectuiveDays(days);
125
+ return consecutiveDays || days.join(', ');
126
+ }
127
+ return null;
128
+ },
129
+
130
+ normalizeRateSetDetail: detail => {
131
+ if (detail.not_applicable) return null;
132
+
133
+ if (detail.nett_rate) {
134
+ return {
135
+ name: detail.desc,
136
+ adults: normalizeCurrency(detail.max_adults),
137
+ adultsPlusChild: normalizeCurrency(detail.max_adults_plus_child),
138
+ nettRate: detail.nett_rate.toFixed(2),
139
+ retailRate: detail.retail_rate ? detail.retail_rate.toFixed(2) : 0,
140
+ extraAdult: normalizeCurrency(detail.extra_adult),
141
+ extraChild: normalizeCurrency(detail.extra_child),
142
+ };
143
+ }
144
+ // TODO: Other rate set types
145
+ return null;
146
+ },
147
+
148
+ normalizeLog: text => {
149
+ let logtest = text;
150
+
151
+ if (text != null && text !== undefined) {
152
+ if (typeof text !== 'string') {
153
+ logtest = JSON.stringify(logtest);
154
+ }
155
+ logtest = logtest.replace(/"userName"\s*:\s*"[^"]*"/, '"userName":"..."');
156
+ logtest = logtest.replace(/"password"\s*:\s*"[^"]*"/, '"password":"..."');
157
+ logtest = logtest.replace(
158
+ /<Password>[^<]*<\/Password>/,
159
+ '<Password>...</Password>',
160
+ );
161
+ logtest = logtest.replace(/<Login>[^<]*<\/Login>/, '<Login>...</Login>');
162
+ }
163
+ return logtest;
164
+ },
165
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "ti2-tourplan",
3
+ "version": "1.0.1",
4
+ "description": "Tourplan's TI2 Plugin",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "jest"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/TourConnect/ti2-tourplan.git"
12
+ },
13
+ "author": "",
14
+ "license": "GPLV3",
15
+ "bugs": {
16
+ "url": "https://github.com/TourConnect/ti2-tourplan/issues"
17
+ },
18
+ "homepage": "https://github.com/TourConnect/ti2-tourplan#readme",
19
+ "jest": {
20
+ "testEnvironment": "node",
21
+ "coveragePathIgnorePatterns": [
22
+ "/node_modules/"
23
+ ],
24
+ "setupFilesAfterEnv": [
25
+ "./jest.setup.js"
26
+ ],
27
+ "testTimeout": 10000
28
+ },
29
+ "dependencies": {
30
+ "axios": "^0.24.0",
31
+ "bluebird": "^3.7.2",
32
+ "js2xmlparser": "^4.0.2",
33
+ "jsonwebtoken": "^8.5.1",
34
+ "moment": "^2.29.1",
35
+ "ramda": "^0.27.1",
36
+ "underscore": "^1.13.4",
37
+ "xml2js": "^0.4.23"
38
+ },
39
+ "devDependencies": {
40
+ "eslint": "^8.4.1",
41
+ "eslint-config-airbnb": "^19.0.2",
42
+ "eslint-plugin-import": "^2.25.4",
43
+ "jest": "^27.4.5",
44
+ "jest-cli": "^27.4.7",
45
+ "jest-diff": "^27.4.2"
46
+ }
47
+ }