tripstorekrabi-shared 1.0.0

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.
Files changed (4) hide show
  1. package/availability.js +179 -0
  2. package/package.json +19 -0
  3. package/price.js +122 -0
  4. package/utils.js +589 -0
@@ -0,0 +1,179 @@
1
+ import dayjs from "dayjs";
2
+ import customParseFormat from 'dayjs/plugin/customParseFormat.js';
3
+ import isBetween from 'dayjs/plugin/isBetween.js';
4
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore.js';
5
+ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter.js';
6
+ import utc from 'dayjs/plugin/utc.js';
7
+ import timezone from 'dayjs/plugin/timezone.js';
8
+
9
+ dayjs.extend(customParseFormat);
10
+ dayjs.extend(isBetween);
11
+ dayjs.extend(isSameOrBefore);
12
+ dayjs.extend(isSameOrAfter);
13
+ dayjs.extend(utc);
14
+ dayjs.extend(timezone);
15
+
16
+ const WEEKDAYS = new Array(7);
17
+ WEEKDAYS[0] = "Sunday";
18
+ WEEKDAYS[1] = "Monday";
19
+ WEEKDAYS[2] = "Tuesday";
20
+ WEEKDAYS[3] = "Wednesday";
21
+ WEEKDAYS[4] = "Thursday";
22
+ WEEKDAYS[5] = "Friday";
23
+ WEEKDAYS[6] = "Saturday";
24
+
25
+ export const INACTIVE = "inactive";
26
+ export const SINGLE_DATES = "singleDates";
27
+ export const BLOCKOUT = "blockout";
28
+ export const SCHEDULE_MODES = [INACTIVE, SINGLE_DATES, BLOCKOUT];
29
+
30
+ export function isAvailable(availability, date, schedule, options = {}) {
31
+ let available = availability.available;
32
+
33
+ if (available && options.viatorApi === true && (!availability.br?.connectedToApi || !availability.br?.available)) {
34
+ return false;
35
+ }
36
+
37
+ if (available && options.gygApi === true && (!availability.gyg?.connectedToApi || !availability.gyg?.available)) {
38
+ return false;
39
+ }
40
+
41
+ if (available) {
42
+ if (schedule) {
43
+ let scheduleObject = findSchedule(availability, schedule);
44
+ if (scheduleObject) {
45
+ available = checkAvailability(date, scheduleObject);
46
+ } else {
47
+ available = false;
48
+ }
49
+ } else if (availability.schedules) {
50
+ available = Object.values(availability.schedules).every(
51
+ (schedule) => checkAvailability(date, schedule)
52
+ );
53
+ } else if (availability.openingHours) {
54
+ available = Object.values(availability.openingHours).every(
55
+ (schedule) => checkAvailability(date, schedule)
56
+ );
57
+ }
58
+ }
59
+
60
+ if (available) {
61
+ date = dayjs(date, "DD.MM.YYYY");
62
+ let today = dayjs().startOf('day');
63
+ available = date.isSameOrAfter(today);
64
+ }
65
+
66
+ return available;
67
+ }
68
+
69
+ function checkAvailability(date, schedule) {
70
+ let mode = schedule.mode;
71
+
72
+ switch (mode) {
73
+ case INACTIVE: {
74
+ return false;
75
+ }
76
+ case SINGLE_DATES: {
77
+ return schedule.singleDates.includes(date);
78
+ }
79
+ case BLOCKOUT: {
80
+ let available = true;
81
+
82
+ if (schedule.blockoutDates) {
83
+ available = !schedule.blockoutDates.includes(date);
84
+ }
85
+ if (available && schedule.blockoutPeriods) {
86
+ let day = dayjs(date, "DD.MM.YYYY");
87
+ let year = day.year();
88
+ available = !schedule.blockoutPeriods.some((period) => {
89
+ let start = dayjs(`${period.start}.${year}`, "DD.MM.YYYY");
90
+ let end = dayjs(`${period.end}.${year}`, "DD.MM.YYYY");
91
+ return day.isBetween(start, end, 'day', '[]');
92
+ });
93
+ }
94
+
95
+ if (available && schedule.weekDays) {
96
+ let weekDay = WEEKDAYS[dayjs(date, "DD.MM.YYYY").day()];
97
+ available = schedule.weekDays.includes(weekDay);
98
+ }
99
+
100
+ return available;
101
+ }
102
+ default: {
103
+ return true;
104
+ }
105
+ }
106
+ }
107
+
108
+ export function getSchedules(availability) {
109
+ if (availability.schedules) {
110
+ return Object.keys(availability.schedules).sort();
111
+ } else if (availability.openingHours) {
112
+ return Object.keys(availability.openingHours).sort();
113
+ } else {
114
+ return [];
115
+ }
116
+ }
117
+
118
+ export function findSchedule(availability, schedule) {
119
+ if (availability.schedules) {
120
+ return availability.schedules[schedule] ?? Object.entries(availability.schedules)[0];
121
+ } else if (availability.openingHours) {
122
+ return availability.openingHours[schedule] ?? Object.entries(availability.openingHours)[0];
123
+ }
124
+ }
125
+
126
+ export function getAvailabilityLabel(availability, schedule) {
127
+ if (!availability.available) {
128
+ return "Not available";
129
+ } else {
130
+ let scheduleObject = findSchedule(availability, schedule);
131
+
132
+ if (scheduleObject.mode === INACTIVE) {
133
+ return "Not available";
134
+ } else if (scheduleObject.mode === SINGLE_DATES) {
135
+ return "Available on selected dates";
136
+ } else if (scheduleObject.mode === BLOCKOUT) {
137
+ if (scheduleObject.blockoutPeriods && scheduleObject.blockoutPeriods.length > 0) {
138
+ let starts = [];
139
+ let ends = [];
140
+ let startPeriod = scheduleObject.blockoutPeriods[0];
141
+ let year = dayjs().year();
142
+ scheduleObject.blockoutPeriods.forEach((period, i) => {
143
+ starts.push(dayjs(`${period.end}.${year}`, "DD.MM.YYYY").add(1, 'day'));
144
+ if (i + 1 < scheduleObject.blockoutPeriods.length) {
145
+ ends.push(dayjs(`${scheduleObject.blockoutPeriods[i + 1].start}.${year}`, "DD.MM.YYYY").subtract(1, 'day'));
146
+ }
147
+ });
148
+ ends.push(dayjs(`${startPeriod.start}.${year + 1}`, "DD.MM.YYYY").subtract(1, 'day'));
149
+
150
+ let availability = [];
151
+ starts.forEach((start, i) => {
152
+ if (ends[i].diff(start, "month") > 1) {
153
+ availability.push(start.format("MMM") + " - " + ends[i].format("MMM"));
154
+ }
155
+ });
156
+
157
+ return availability.join(", ");
158
+ } else if (scheduleObject.weekDays) {
159
+ return scheduleObject.weekDays.map((item) => item.substring(0, 3)).join(", ");
160
+ } else {
161
+ return "Daily";
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ export function isLastMinuteDate(dateValue, cutoffTime) {
168
+ let date = dayjs(dateValue, "DD.MM.YYYY").tz("Asia/Bangkok");
169
+ let now = dayjs().tz("Asia/Bangkok");
170
+
171
+ return now.add(cutoffTime, "hour").isSame(date, "day");
172
+ }
173
+
174
+ export function isPastDate(dateValue, schedule) {
175
+ let date = dayjs(dateValue + " " + schedule, "DD.MM.YYYY HH:mm").tz("Asia/Bangkok");
176
+ let now = dayjs().tz("Asia/Bangkok");
177
+
178
+ return date.isSameOrBefore(now);
179
+ }
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "tripstorekrabi-shared",
3
+ "version": "1.0.0",
4
+ "description": "Shared library for Trip Store Krabi",
5
+ "author": "Stefan Sommer",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/tonprince/tripstorekrabi.git"
9
+ },
10
+ "exports": {
11
+ "./utils": "./utils.js",
12
+ "./price": "./price.js",
13
+ "./availability": "./availability.js"
14
+ },
15
+ "dependencies": {
16
+ "dayjs": "^1.10.8"
17
+ },
18
+ "type": "module"
19
+ }
package/price.js ADDED
@@ -0,0 +1,122 @@
1
+
2
+ import { getBookingType } from "./utils.js";
3
+
4
+ export const MAX_PAX = 20;
5
+
6
+ export function calculateProductPrice(booking, pricing, selling = true) {
7
+ let type = pricing.type;
8
+ let total = 0;
9
+
10
+ switch (type) {
11
+ case "person": {
12
+ let volume = findVolume(booking, pricing);
13
+ if (!volume) {
14
+ throw new Error(`No volume price found for the total amount of ${booking.adults + booking.children} people`);
15
+ } else {
16
+ let bookingType = getBookingType(booking.referenceNumber);
17
+
18
+ if (volume[bookingType]?.active) {
19
+ let customPricing = volume[bookingType];
20
+ total = booking.adults * (selling ? customPricing.adult.partnerNetPrice : customPricing.adult.operatorNetPrice) + (pricing.ageBands.child.active && volume.child && booking.children ? booking.children * (selling ? customPricing.child.partnerNetPrice : customPricing.child.operatorNetPrice) : 0);
21
+ } else {
22
+ total = booking.adults * (selling ? volume.adult.sellingPrice : volume.adult.operatorNetPrice) + (pricing.ageBands.child.active && volume.child && booking.children ? booking.children * (selling ? volume.child.sellingPrice : volume.child.operatorNetPrice) : 0);
23
+ }
24
+ break;
25
+ }
26
+ }
27
+
28
+ case "group": {
29
+ let volume = findVolume(booking, pricing);
30
+ if (!volume) {
31
+ throw new Error(`No volume price found for the total amount of ${booking.adults + booking.children} people`);
32
+ } else {
33
+ let bookingType = getBookingType(booking.referenceNumber);
34
+
35
+ if (volume[bookingType]?.active) {
36
+ let customPricing = volume[bookingType];
37
+ total = selling ? customPricing.partnerNetPrice : customPricing.operatorNetPrice;
38
+ } else {
39
+ total = (selling ? volume.sellingPrice : volume.operatorNetPrice);
40
+ }
41
+
42
+ break;
43
+ }
44
+ }
45
+ default:
46
+ throw new Error(`Pricing type ${type} is not supported`);
47
+ }
48
+
49
+ return total;
50
+ }
51
+
52
+ export function findVolume(people, pricing) {
53
+ let total = people.adults + people.children ?? 0;
54
+ return pricing.volumes.sort((v1, v2) => v1.max - v2.max).find((item) => total <= item.max);
55
+ }
56
+
57
+ export function countMinPeople(pricing) {
58
+ let volume = pricing.volumes.map((item) => item.min);
59
+ return volume ? Math.max(volume.reduce((p, v) => p < v ? p : v), 1) : 1;
60
+ }
61
+
62
+ export function countMaxPeople(pricing) {
63
+ let volume = pricing.volumes.map((item) => item.max);
64
+ return volume ? volume.reduce((p, v) => p > v ? p : v) : MAX_PAX;
65
+ }
66
+
67
+ export function calculateTotalPrice(data, product, selling = true) {
68
+ let total = calculateProductPrice(data, product.pricing, selling);
69
+ total += calculateStartEndPointPrice(data, findStartingPoint(product, data.startingPointId), selling);
70
+ total += calculateStartEndPointPrice(data, findEndPoint(product, data.endPointId), selling);
71
+ total += calculateNationalParkFee(data, product);
72
+ total += calculateAddOnsPrice(data, product, selling);
73
+ return total;
74
+ }
75
+
76
+ export function calculateNationalParkFee(data, product) {
77
+ return data.nationalParkFee === "Already paid" && product.nationalPark ? product.nationalPark.pricing.adult * data.adults + product.nationalPark.pricing.child * data.children : 0;
78
+ }
79
+
80
+ export function calculateAddOnsPrice(data, product, selling = true) {
81
+ let total = 0;
82
+ if (data.addOns) {
83
+ for (const [id, addOn] of Object.entries(data.addOns)) {
84
+ let addOnDef = findAddOn(id, product.addOns);
85
+ if (addOnDef) {
86
+ total += addOn.adults * (selling ? addOnDef.pricing.adult
87
+ .sellingPrice : addOnDef.pricing.adult
88
+ .operatorNetPrice) + (addOn.children > 0 ? addOn.children * (selling ? addOnDef.pricing.child
89
+ .sellingPrice : addOnDef.pricing.child
90
+ .operatorNetPrice) : 0);
91
+ }
92
+ }
93
+ }
94
+ return total;
95
+ }
96
+
97
+ export function calculateStartEndPointPrice(people, startEndPoint, selling = true) {
98
+ let pricing = startEndPoint?.pricing;
99
+ if (pricing) {
100
+ if (pricing.type === "group") {
101
+ return (selling ? pricing.sellingPrice : pricing.operatorNetPrice) * Math.ceil((people.adults + people.children) / pricing.max);
102
+ } else if (pricing.type === "person") {
103
+ return (selling ? pricing.sellingPrice : pricing.operatorNetPrice) * (people.adults + people.children);
104
+ }
105
+ } else {
106
+ return 0;
107
+ }
108
+ }
109
+
110
+ export function findStartingPoint(product, startingPointId) {
111
+ return product.startingPoints.find((item) => item.id === startingPointId) ?? product.startingPoints[0];
112
+ }
113
+
114
+ export function findEndPoint(product, endPointId) {
115
+ return product.endPoints.find((item) => item.id === endPointId) ?? product.endPoints[0] ?? { name: "" };
116
+ }
117
+
118
+ export function findAddOn(id, addOns) {
119
+ return addOns.find((item) =>
120
+ item.id === id
121
+ );
122
+ }
package/utils.js ADDED
@@ -0,0 +1,589 @@
1
+ import dayjs from "dayjs";
2
+ import customParseFormat from "dayjs/plugin/customParseFormat.js";
3
+ import advancedFormat from 'dayjs/plugin/advancedFormat.js';
4
+ import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js";
5
+ import utc from 'dayjs/plugin/utc.js';
6
+ import timezone from 'dayjs/plugin/timezone.js';
7
+ import { findEndPoint, findStartingPoint } from "./price.js";
8
+
9
+ dayjs.extend(customParseFormat);
10
+ dayjs.extend(advancedFormat);
11
+ dayjs.extend(isSameOrBefore);
12
+ dayjs.extend(utc);
13
+ dayjs.extend(timezone);
14
+
15
+ export const PROD_URL = "https://www.tripstorekrabi.com";
16
+ export const MAX_AGE_SEC = 60 * 60 * 24;
17
+ const EMAIL_PATTERN = new RegExp(/^[a-zA-Z0-9.!#$%&' * +/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/);
18
+ const MEALS = ["lunch", "meal", "breakfast", "dinner"];
19
+ const SCHEDULE_PATTERN = new RegExp('^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$');
20
+
21
+ export function validateEmail(email) {
22
+ return EMAIL_PATTERN.test(email);
23
+ }
24
+
25
+ export function resizeGoogleImage(image, size) {
26
+ return image.replace(/\/w.\d+-.\d+-p\//, "/" + size + "/").replace(
27
+ /\/w.\d+-.\d+-c\//, "/" + size + "/").replace(/\/w.\d+-.\d+-no\//,
28
+ "/" + size + "/").replace(/\/w.\d+-.\d+\//, "/" + size + "/").replace(
29
+ /\/s.\d+\//, "/" + size + "/").replace(/\=w.\d+-.\d+-p/, "=" + size)
30
+ .replace(/\=w.\d+-.\d+-c/, "=" + size).replace(/\=w.\d+-.\d+-no/,
31
+ "=" + size).replace(/\=w.\d+-.\d+/, "=" + size).replace(/\=s.\d+/,
32
+ "=" + size).replace(/\=s.\d+/, "=" + size);
33
+ }
34
+
35
+ export function resizeStorageImage(path, size) {
36
+ return `https://tripstorekrabi-next.firebaseapp.com/images/${path}?${size}`;
37
+ }
38
+
39
+ export function createPdfPreview(path, size) {
40
+ return `https://tripstorekrabi-next.firebaseapp.com/pdf/preview/${path}?${size}`;
41
+ }
42
+
43
+ export function formatNumber(number, fraction = 2) {
44
+ return number.toLocaleString("th-TH", {
45
+ minimumFractionDigits: fraction,
46
+ });
47
+ }
48
+
49
+ export function getPickupTimeFromTo(product) {
50
+ return product.pickupZones;
51
+ }
52
+
53
+ export function getDurationSummary(schedules) {
54
+ let minDuration = Number.MAX_VALUE;
55
+ let maxDuration = -1;
56
+ schedules.forEach((schedule) => {
57
+ let duration = getDuration(schedule);
58
+ if (duration > maxDuration) {
59
+ maxDuration = duration;
60
+ }
61
+ if (duration < minDuration) {
62
+ minDuration = duration;
63
+ }
64
+ });
65
+
66
+ return minDuration == maxDuration ? formatDuration(minDuration) : `${formatDuration(minDuration)} - ${formatDuration(maxDuration)}`;
67
+ }
68
+
69
+ export function getDuration(schedule) {
70
+ if (schedule) {
71
+ let schedules = schedule.split("-");
72
+ let minutes = dayjs(schedules[1].trim(), "HH:mm").diff(dayjs(schedules[0].trim(), "HH:mm"), 'minute');
73
+ if (minutes < 0) {
74
+ minutes = 24 * 60 + minutes;
75
+ }
76
+ return minutes;
77
+ }
78
+ else {
79
+ return 0;
80
+ }
81
+ }
82
+
83
+ export function formatDuration(minutes) {
84
+ if (minutes === 0) {
85
+ return "-";
86
+ } else if (minutes > 60) {
87
+ return (minutes / 60).toFixed(1) + " hours";
88
+ } else {
89
+ return minutes + " minutes";
90
+ }
91
+ }
92
+
93
+ export function getPickuptimeSummary(pickupZones) {
94
+ if (pickupZones) {
95
+ let pickupTimesArray = [];
96
+ pickupZones.forEach((pickupZone) => {
97
+ var pickupLength = pickupZone.indexOf(": ");
98
+ var pickupTimes = pickupZone.substring(pickupLength + 1).trim();
99
+ if (pickupZone.indexOf(" / ") != -1) {
100
+ pickupTimesArray.push(pickupTimes.split(" / "));
101
+ } else {
102
+ pickupTimesArray.push([pickupTimes]);
103
+ }
104
+ });
105
+
106
+
107
+ pickupTimesArray = pickupTimesArray[0].map((_, colIndex) => pickupTimesArray.map(row => row[colIndex]));
108
+
109
+ return pickupTimesArray.map((items) => {
110
+ let pickupTimes = [...new Set(items.reduce((r, e) => {
111
+ e.split(" - ").forEach((item) => {
112
+ r.push(item);
113
+ });
114
+ return r;
115
+ }, []))].sort();
116
+
117
+ if (pickupTimes.length == 1) {
118
+ return pickupTimes[0];
119
+ } else {
120
+ return `${pickupTimes[0]} - ${pickupTimes[pickupTimes.length - 1]}`;
121
+ }
122
+ });
123
+ } else {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ export function getDeparture(schedule) {
129
+ return schedule.split("-")[0].trim();
130
+ }
131
+
132
+ export function getPickupTime(schedule, offset = 30) {
133
+ if (schedule.indexOf("-") != -1) {
134
+ return dayjs(schedule.split("-")[0].trim(), "HH:mm").subtract(offset, "minute").format("HH:mm");
135
+ } else {
136
+ return dayjs(schedule, "HH:mm").subtract(offset, "minute").format("HH:mm");
137
+ }
138
+ }
139
+
140
+ export function getArrival(schedule) {
141
+ return schedule.split("-")[1].trim();
142
+ }
143
+
144
+ export function isHotelPickup(value) {
145
+ value = value.toLowerCase();
146
+ return (value.includes("picked up") || value.includes("pickup") || value.includes("round-trip")) && value.includes("hotel");
147
+ }
148
+
149
+ export function isHotelDropOff(value) {
150
+ value = value.toLowerCase();
151
+ return (value.includes("dropped off") || value.includes("drop-off") || value.includes("transfer")) && value.includes("hotel");
152
+ }
153
+
154
+ export function isAirportPickup(value) {
155
+ value = value.toLowerCase();
156
+ return (value.includes("pickup") || value.includes("meet")) && value.includes("airport");
157
+ }
158
+
159
+ export function isAirportDropOff(value) {
160
+ value = value.toLowerCase();
161
+
162
+ let result = (value.includes("drop-off") || value.includes("transfer")) && value.includes("airport");
163
+
164
+ if (!result && value.includes(" to ")) {
165
+ result = value.substring(value.indexOf(" to ")).includes(" airport");
166
+ }
167
+ return result;
168
+ }
169
+
170
+ export function isMeetingPoint(value) {
171
+ value = value.toLowerCase();
172
+ value = value.replace('pick up', 'pickup').replace('picked up', 'pickup').replace('pick-up', 'pickup');
173
+ return (value.indexOf("meet ") != -1) || (value.indexOf('pickup') != -1 && value.indexOf('hotel') == -1);
174
+ }
175
+
176
+ export function isTrainStation(value) {
177
+ value = value.toLowerCase();
178
+ return value.includes("train") && value.includes("station");
179
+ }
180
+
181
+ export function isMealIncluded(includes) {
182
+ if (includes) {
183
+ let includesString = includes.join(" ").toLowerCase();
184
+ return MEALS.some((item) => includesString.includes(item));
185
+ } else {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ export function validateName(name) {
191
+ name = name.trim();
192
+ if (name) {
193
+ return name.indexOf(" ") != -1;
194
+ } else {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ export function applySchedule(schedule, schedules, pickupZones, includeZonePrice = false) {
200
+ let index = schedules.indexOf(schedule);
201
+ return pickupZones.map((zone) => {
202
+ let zoneArray = zone.split(": ");
203
+ let zoneName = zoneArray[0].trim();
204
+ let zonePickupTimes = zoneArray[1].trim();
205
+ let pickupTimes = zonePickupTimes.split("/");
206
+ return pickupTimes[index] ? (includeZonePrice ? zoneName : zoneName.replace(/ *\([^)]*\) */g, "")) + ": " + pickupTimes[index].trim() : undefined;
207
+ }).filter((item) => item !== undefined);
208
+ }
209
+
210
+ export function scrollContainer() {
211
+ return document.documentElement || document.body;
212
+ }
213
+
214
+ export function toTitleCase(str) {
215
+ return str.replace(/\b\S/g, t => t.toUpperCase());
216
+ }
217
+
218
+ export function capitalize(string) {
219
+ return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
220
+ }
221
+
222
+ export function transformToNCols(array, n) {
223
+ let converted = [];
224
+ let chunks = chunkArray(array, Math.ceil(array.length / n));
225
+
226
+ let chunk = chunks[0];
227
+ for (let i = 0; i < chunk.length; i++) {
228
+ let item = chunk[i];
229
+ if (item) {
230
+ converted.push(item);
231
+ }
232
+
233
+ for (let j = 1; j < chunks.length; j++) {
234
+ let nextChunk = chunks[j];
235
+ let item = nextChunk[i];
236
+ if (item) {
237
+ converted.push(item);
238
+ }
239
+ }
240
+ }
241
+ return converted;
242
+ }
243
+
244
+ function chunkArray(array, chunk_size) {
245
+ array = [...array];
246
+ var results = [];
247
+
248
+ while (array.length) {
249
+ results.push(array.splice(0, chunk_size));
250
+ }
251
+
252
+ return results;
253
+ }
254
+
255
+ export function encodeURL(url) {
256
+ return toTitleCase(url).replace(/ /g, "-");
257
+ }
258
+
259
+ export function formatPeople(people) {
260
+ let result = [];
261
+ if (people.adults > 0) {
262
+ result.push(`${people.adults} ${people.adults > 1 ? "adults" : "adult"}`);
263
+ }
264
+ if (people.children > 0) {
265
+ result.push(`${people.children} ${people.children > 1 ? "children" : "child"
266
+ }`);
267
+ }
268
+ if (people.infants > 0) {
269
+ result.push(`${people.infants} ${people.infants > 1 ? "infants" : "infant"
270
+ }`);
271
+ }
272
+ return result.join(", ");
273
+ }
274
+
275
+ export function countPeople(people) {
276
+ return people.adults ?? 0 + people.children ?? 0 + people.infants ?? 0;
277
+ }
278
+
279
+ export function recaptchaChallenge(secret, token) {
280
+ const url = `https://www.google.com/recaptcha/api/siteverify?secret=${secret}&response=${token}`;
281
+ return fetch(url, {
282
+ method: "POST",
283
+ }).then(async (result) => result.json())
284
+ .then((result) => result.success
285
+ ).catch((e) => {
286
+ console.error(e);
287
+ return false
288
+ });
289
+ }
290
+
291
+ export function isNumeric(n) {
292
+ return !isNaN(parseFloat(n)) && isFinite(n);
293
+ }
294
+
295
+ export function validURL(str) {
296
+ var pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
297
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
298
+ '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
299
+ '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
300
+ '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
301
+ '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator
302
+ return !!pattern.test(str);
303
+ }
304
+
305
+ export function getBookingSummary(booking, options = {}) {
306
+ let summary = "";
307
+ if (!options.excludeReferenceNumber) {
308
+ summary += booking.referenceNumber;
309
+ if (booking.code) {
310
+ summary += ", Code: " + booking.code;
311
+ }
312
+
313
+ if (options.includeVersion && booking.version) {
314
+ summary += ", Version: " + booking.version;
315
+ }
316
+ summary += ": ";
317
+ }
318
+
319
+ if (booking.productDescription && booking.operator && booking.departureDate && booking.schedule && booking.leadTraveller) {
320
+ let departureDate = dayjs(booking.departureDate.toDate()).format("DD.MM.YYYY");
321
+ summary += booking.productDescription + ", " + departureDate + ", " + booking.operator + ", " + booking.adults + ", " + booking.children + ", "
322
+ + booking.infants + ", " + booking.leadTraveller;
323
+
324
+ if (booking.startingPointDescription) {
325
+ summary += ", " + (isHotelPickup(booking.startingPointDescription) && booking.noHotelYet ? "NO HOTEL YET" : booking.startingPointDescription);
326
+ } else if (booking.noHotelYet) {
327
+ summary += ", NO HOTEL YET";
328
+ }
329
+
330
+ let hasHotelPickup = isHotelPickup(booking.startingPointDescription);
331
+ let hasRoomNumber = booking.roomNumber;
332
+
333
+ if (hasHotelPickup && hasRoomNumber) {
334
+ summary += " (room " + booking.roomNumber + ")";
335
+ }
336
+
337
+ summary += ", " + booking.schedule;
338
+
339
+ if (booking.pickupTime && booking.pickupTime != "?") {
340
+ if (hasHotelPickup) {
341
+ summary += " (Pickup";
342
+
343
+ if (booking.pickupTime.indexOf("-") != -1) {
344
+ summary += " between " + booking.pickupTime.replace(" - ", " and ").replace("-", " and ");
345
+ } else {
346
+ summary += " at " + booking.pickupTime;
347
+ }
348
+ summary += ")";
349
+ } else if (isMeetingPoint(booking.startingPointDescription)) {
350
+ summary += " (Meet";
351
+
352
+ if (booking.pickupTime.indexOf("-") != -1) {
353
+ summary += " at " + booking.pickupTime.split("-")[0].trim();
354
+ } else {
355
+ summary += " at " + booking.pickupTime;
356
+ }
357
+ summary += ")";
358
+ }
359
+ }
360
+ } else {
361
+ summary += "«THE BOOKING IS INCOMPLETE»";
362
+ }
363
+
364
+ return summary;
365
+ }
366
+
367
+ export function getItemSummary(item, product) {
368
+ let summary = item.description;
369
+
370
+ if (product) {
371
+ if (item.departureDate) {
372
+ summary += ", " + dayjs(item.departureDate).tz("Asia/Bangkok").format("DD.MM.YYYY");
373
+ }
374
+
375
+ if (item.schedule) {
376
+ summary += ", " + item.schedule;
377
+ }
378
+
379
+ if (item.adults || item.children || item.infants) {
380
+ summary += ", " + formatPeople(item) + ", ";
381
+ }
382
+
383
+ if (item.startingPointId) {
384
+ let startingPointDescription = findStartingPoint(product, item.startingPointId).value;
385
+ if (isHotelPickup(startingPointDescription)) {
386
+ summary += "Hotel pickup from " + item.pickupHotel;
387
+
388
+ if (item.roomNumber) {
389
+ summary += ` (${item.roomNumber})`;
390
+ }
391
+ } else {
392
+ summary += startingPointDescription;
393
+ }
394
+ }
395
+
396
+ if (item.endPointId) {
397
+ let endPointDescription = findEndPoint(product, item.endPointId).value;
398
+ if (isHotelDropOff(endPointDescription) && item.dropoffHotel) {
399
+ summary += ", Hotel drop-off at " + item.dropoffHotel;
400
+ }
401
+ }
402
+ }
403
+
404
+ return summary;
405
+ }
406
+
407
+ export function validateSchedule(schedule, schedules = []) {
408
+ if (schedule === "?") {
409
+ return true;
410
+ } else if (schedule.indexOf(" - ") != -1) {
411
+ var times = schedule.split(" - ");
412
+ var from = times[0];
413
+ var to = times[1];
414
+
415
+ if (!SCHEDULE_PATTERN.test(from) || !SCHEDULE_PATTERN.test(to) || (from === to)) {
416
+ return "Invalid schedule format: for example use '09:00 - 15:30'";
417
+ }
418
+ } else {
419
+ return "Invalid schedule format: for example use '09:00 - 15:30'";
420
+ }
421
+
422
+
423
+ if (schedule != "") {
424
+ if (schedules.length > 0 && schedules.indexOf(schedule) == -1) {
425
+ return { status: "warning", message: "The value does not match the product schedules" };
426
+ }
427
+ }
428
+
429
+ return true;
430
+ }
431
+
432
+ export function validatePickupTime(pickupTime, schedule) {
433
+ let departureTime = schedule.split(" - ")[0];
434
+
435
+ if (pickupTime === "?" || schedule === "?") {
436
+ return true;
437
+ } else if (pickupTime.indexOf(" - ") != -1) {
438
+ var times = pickupTime.split(" - ");
439
+ var from = times[0];
440
+ var to = times[1];
441
+
442
+ if (!SCHEDULE_PATTERN.test(from) || !SCHEDULE_PATTERN.test(to) || (from === to)) {
443
+ return "Invalid pickup time format: for example use '09:00' or '08:45 - 09:00'";
444
+ } else {
445
+ return checkTime(from, departureTime);
446
+ }
447
+ } else if (pickupTime.indexOf("-") != -1) {
448
+ return `Invalid pickup time format, use spaces around the dash: ${pickupTime.replace(/ /g, '').replace("-", " - ")}`;
449
+ } else {
450
+ if (!SCHEDULE_PATTERN.test(pickupTime)) {
451
+ return "Invalid pickup time format: for example use '09:00' or '08:45 - 09:00'";
452
+ } else {
453
+ return checkTime(pickupTime, departureTime);
454
+ }
455
+ }
456
+ }
457
+
458
+ function checkTime(pickupTimeValue, departureTimeValue) {
459
+ if (pickupTimeValue === "" || departureTimeValue === "") {
460
+ return false;
461
+ }
462
+
463
+ try {
464
+ let pickupTimeValues = pickupTimeValue.split(":");
465
+ let pickupTime = dayjs().hour(pickupTimeValues[0]).minute(pickupTimeValues[1]).second(0);
466
+
467
+ let departureTimeValues = departureTimeValue.split(":");
468
+ let departureTime = dayjs().hour(departureTimeValues[0]).minute(departureTimeValues[1]).second(0);
469
+
470
+ return pickupTime.isSameOrBefore(departureTime) ? true : "Pickup time must be before or equals departure time";
471
+ } catch (e) {
472
+ return false;
473
+ }
474
+ }
475
+
476
+ export function hasCustomerEmail(email) {
477
+ return email && email !== "info@tripstorekrabi.com";
478
+ }
479
+
480
+ export function isActivity(url) {
481
+ return url.includes("/Activities/");
482
+ }
483
+
484
+ export function getGoogleDocId(url) {
485
+ return url.match(/[-\w]{25,}/)[0];
486
+ }
487
+
488
+ export function createVoucherRequest(booking, product) {
489
+ let request = {
490
+ booking: {
491
+ referenceNumber: booking.referenceNumber,
492
+ specialRequirements: booking.specialRequirements,
493
+ nationalParkFee:
494
+ product && product.nationalPark
495
+ ? "NATIONAL PARK ENTRANCE FEE: " +
496
+ booking.nationalParkFee.toUpperCase()
497
+ : "",
498
+ productDescription: booking.productDescription,
499
+ leadTraveller: booking.leadTraveller,
500
+ adults: booking.adults,
501
+ children: booking.children,
502
+ infants: booking.infants,
503
+ departureDate: dayjs(booking.departureDate.toDate()).tz("Asia/Bangkok").format("Do MMM YYYY"),
504
+ operator: booking.operator,
505
+ startingPointDescription: booking.startingPointDescription,
506
+ roomNumber: booking.roomNumber,
507
+ schedule: booking.schedule,
508
+ pickupTime: booking.pickupTime,
509
+ mobileVoucher: product
510
+ ? product.operator.mobileVoucher ??
511
+ product.operator.default.mobileVoucher
512
+ : true,
513
+ },
514
+ };
515
+ if (booking.voucherUrl) {
516
+ request["id"] = getGoogleDocId(booking.voucherUrl);
517
+ }
518
+
519
+ return request;
520
+ }
521
+
522
+ export function isGYGBooking(referenceNumber) {
523
+ return referenceNumber.startsWith("GYG");
524
+ }
525
+
526
+ export function isViatorBooking(referenceNumber) {
527
+ return referenceNumber.startsWith("BR-");
528
+ }
529
+
530
+ export function isExpediaBooking(referenceNumber) {
531
+ return referenceNumber.startsWith("EXP-");
532
+ }
533
+
534
+ export function isKlookBooking(referenceNumber) {
535
+ return referenceNumber.startsWith("KLK-");
536
+ }
537
+
538
+ export function isMusementBooking(referenceNumber) {
539
+ return referenceNumber.startsWith("MUS");
540
+ }
541
+
542
+ export function isAirbnbBooking(referenceNumber) {
543
+ return referenceNumber.startsWith("Airbnb-");
544
+ }
545
+
546
+ export function isKKdayBooking(referenceNumber) {
547
+ return referenceNumber.startsWith("KKday-");
548
+ }
549
+
550
+ export function isTiqetsBooking(referenceNumber) {
551
+ return referenceNumber.startsWith("TIQ-");
552
+ }
553
+
554
+ export function isTSKBooking(referenceNumber) {
555
+ return referenceNumber.startsWith("TSK");
556
+ }
557
+
558
+ export function getBookingType(referenceNumber) {
559
+ if (!referenceNumber) {
560
+ return "";
561
+ }
562
+ if (isGYGBooking(referenceNumber)) {
563
+ return "gyg";
564
+ } else if (isViatorBooking(referenceNumber)) {
565
+ return "br";
566
+ } else if (isExpediaBooking(referenceNumber)) {
567
+ return "exp";
568
+ } else if (isKlookBooking(referenceNumber)) {
569
+ return "klk";
570
+ } else if (isMusementBooking(referenceNumber)) {
571
+ return "mus";
572
+ } else if (isAirbnbBooking(referenceNumber)) {
573
+ return "air";
574
+ } else if (isKKdayBooking(referenceNumber)) {
575
+ return "kkday";
576
+ } else if (isTiqetsBooking(referenceNumber)) {
577
+ return "tiq";
578
+ } else if (isTSKBooking(referenceNumber)) {
579
+ return "tsk";
580
+ }
581
+ }
582
+
583
+ export function hasCustomPricing(pricing, bookingType) {
584
+ return pricing[bookingType] || bookingType === "tsk";
585
+ }
586
+
587
+ export function truncate(str, num) {
588
+ return str.length > num ? str.slice(0, num - 1) : str;
589
+ }