tripstorekrabi-shared 1.24.4 → 1.24.6

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 (3) hide show
  1. package/booking.js +102 -129
  2. package/package.json +4 -4
  3. package/price.js +167 -285
package/booking.js CHANGED
@@ -2,7 +2,6 @@ import dayjs from 'dayjs';
2
2
  import advancedFormat from 'dayjs/plugin/advancedFormat.js';
3
3
  import utc from 'dayjs/plugin/utc.js';
4
4
  import timezone from 'dayjs/plugin/timezone.js';
5
- import { FieldValue, Timestamp } from 'firebase-admin/firestore';
6
5
  import { PAY_ON_TOUR, calculateTotalOperatorPrice } from './price.js';
7
6
  import { nanoid } from 'nanoid';
8
7
  import { CHECK_IN_LATE, IN_HOUSE, isAirportDropOff, isAirportPickup, isHotelPickup, isTrainStation, KEEP_PRIVATE, NO_PICKUP_HOTEL_YET, NO_ROOM, STAY_NEARBY } from './utils.js';
@@ -28,148 +27,122 @@ export function addBookingFromInvoice(
28
27
  ) {
29
28
  let bookingNumberDoc = firestore.collection('meta').doc('BOOKING_NUMBER');
30
29
 
31
- return firestore.runTransaction((transaction) => {
32
- return bookingNumberDoc.get().then(async (snapshot) => {
33
- let nextBookingNumber = (snapshot.get('last') ?? 0) + 1;
34
- let processed = 0;
35
-
36
- for (let i = 0; i < invoice.items.length; i++) {
37
- let item = invoice.items[i];
38
-
39
- // process only product items
40
- let hasProduct = item.productId?.length > 0;
41
- let productSnapshot = null;
42
- let productDescription;
43
- if (hasProduct) {
44
- productSnapshot = await firestore
45
- .collection('products')
46
- .doc(item.productId)
47
- .get();
48
- productDescription = productSnapshot.get('shortName');
49
- } else {
50
- productDescription = item.description;
51
- }
30
+ return firestore.runTransaction(async (transaction) => {
31
+ const snapshot = await transaction.get(bookingNumberDoc);
32
+ const currentLast = snapshot.get('last') ?? 0;
52
33
 
53
- let notes = {};
34
+ const productSnapshots = {};
35
+ for (const item of invoice.items) {
36
+ if (item.productId?.length > 0 && !productSnapshots[item.productId]) {
37
+ const productRef = firestore.collection('products').doc(item.productId);
38
+ productSnapshots[item.productId] = await transaction.get(productRef);
39
+ }
40
+ }
54
41
 
55
- if (comment) {
56
- notes[nanoid()] = {
57
- description: comment.trim(),
58
- visibility: [],
59
- lastModified: dayjs().toDate(),
60
- lastModifiedBy: 'Trip Store Krabi Next',
61
- };
62
- }
42
+ let nextBookingNumber = currentLast + 1;
43
+ let processed = 0;
63
44
 
64
- if (invoice.comment) {
65
- notes[nanoid()] = {
66
- description: invoice.comment.trim(),
67
- visibility: [],
68
- lastModified: dayjs().toDate(),
69
- lastModifiedBy: 'Trip Store Krabi Next',
70
- };
71
- }
45
+ for (let i = 0; i < invoice.items.length; i++) {
46
+ let item = invoice.items[i];
47
+ let hasProduct = item.productId?.length > 0;
48
+
49
+ let productSnapshot = hasProduct ? productSnapshots[item.productId] : null;
50
+ let productDescription = hasProduct ? productSnapshot.get('shortName') : item.description;
72
51
 
73
- let data = {
74
- referenceNumber: 'TSK' + (nextBookingNumber + processed),
75
- notes,
76
- productDescription,
77
- productRef: hasProduct
78
- ? firestore.collection('products').doc(item.productId)
79
- : null,
80
- operator: productSnapshot?.get('operator.default.name') ?? '',
81
- departureDate: Timestamp.fromDate(
82
- dayjs(item.departureDate).toDate()
83
- ),
84
- schedule: item.schedule ?? '',
85
- pickupTime: '?',
86
- adults: item.adults ?? 0,
87
- children: item.children ?? 0,
88
- infants: item.infants ?? 0,
89
- leadTraveller: invoice.name,
90
- email: invoice.email ?? '',
91
- phoneNumber: invoice.phoneNumber ?? '',
92
- cc: '',
93
- customerBookingStatus: 'Pending',
94
- operatorBookingStatus: 'Pending',
95
- sentToOperator: false,
96
- startingPointDescription: item.startingPointDescription ?? "",
97
- pickupHotel: item.pickupHotel ?? null,
98
- roomNumber: item.roomNumber ?? '',
99
- googleMapsLocation: item.googleMapsLocation ?? '',
100
- dietary: item.dietary ?? "",
101
- flight: item.flight ?? null,
102
- train: item.train ?? null,
103
- customerPaymentStatus: 'Paid',
104
- operatorPaymentStatus: 'Pending',
105
- extraPayments: {},
106
- addOns: item.addOns ?? null,
107
- labels: ['NEW'],
108
- paypalInvoiceId: '',
109
- paypalTransactionId: '',
110
- omiseChargeId: '',
111
- invoiceNumber: '',
112
- date: FieldValue.serverTimestamp(),
113
- createdBy: 'Trip Store Krabi Next',
52
+ let notes = {};
53
+
54
+ if (comment) {
55
+ notes[nanoid()] = {
56
+ description: comment.trim(),
57
+ visibility: [],
114
58
  lastModified: dayjs().toDate(),
115
59
  lastModifiedBy: 'Trip Store Krabi Next',
116
60
  };
61
+ }
117
62
 
118
- if (item.startingPointId) {
119
- data.startingPointId = item.startingPointId;
120
- }
121
-
122
- if (item.dropoffHotel && item.dropoffHotel.length > 0) {
123
- data.dropoffHotel = item.dropoffHotel;
124
- }
125
-
126
- if (i > 0) {
127
- data.partnerNetPrice = 0;
128
- }
129
-
130
- if (item.nationalParkFee) {
131
- data.nationalParkFee = item.nationalParkFee;
132
- }
133
-
134
- if (item.tourGuideLanguage) {
135
- data.tourGuideLanguage = item.tourGuideLanguage;
136
- }
63
+ if (invoice.comment) {
64
+ notes[nanoid()] = {
65
+ description: invoice.comment.trim(),
66
+ visibility: [],
67
+ lastModified: dayjs().toDate(),
68
+ lastModifiedBy: 'Trip Store Krabi Next',
69
+ };
70
+ }
137
71
 
138
- let endPointId = item.endPointId;
139
- if (!endPointId) {
140
- let endPoints = productSnapshot?.get('endPoints');
141
- if (endPoints && endPoints.length === 1) {
142
- endPointId = endPoints[0].id;
143
- }
144
- }
145
- if (endPointId) {
146
- data.endPointId = endPointId;
72
+ let data = {
73
+ date: dayjs().toDate(),
74
+ departureDate: dayjs(item.departureDate).toDate(),
75
+ referenceNumber: 'TSK' + (nextBookingNumber + processed),
76
+ notes,
77
+ productDescription,
78
+ productRef: hasProduct ? firestore.collection('products').doc(item.productId) : null,
79
+ operator: productSnapshot?.get('operator.default.name') ?? '',
80
+ schedule: item.schedule ?? '',
81
+ pickupTime: '?',
82
+ adults: item.adults ?? 0,
83
+ children: item.children ?? 0,
84
+ infants: item.infants ?? 0,
85
+ leadTraveller: invoice.name,
86
+ email: invoice.email ?? '',
87
+ phoneNumber: invoice.phoneNumber ?? '',
88
+ cc: '',
89
+ customerBookingStatus: 'Pending',
90
+ operatorBookingStatus: 'Pending',
91
+ sentToOperator: false,
92
+ startingPointDescription: item.startingPointDescription ?? "",
93
+ pickupHotel: item.pickupHotel ?? null,
94
+ roomNumber: item.roomNumber ?? '',
95
+ googleMapsLocation: item.googleMapsLocation ?? '',
96
+ dietary: item.dietary ?? "",
97
+ flight: item.flight ?? null,
98
+ train: item.train ?? null,
99
+ customerPaymentStatus: 'Paid',
100
+ operatorPaymentStatus: 'Pending',
101
+ extraPayments: {},
102
+ addOns: item.addOns ?? null,
103
+ labels: ['NEW'],
104
+ paypalInvoiceId: '',
105
+ paypalTransactionId: '',
106
+ omiseChargeId: '',
107
+ invoiceNumber: '',
108
+ createdBy: 'Trip Store Krabi Next',
109
+ lastModified: dayjs().toDate(),
110
+ lastModifiedBy: 'Trip Store Krabi Next',
111
+ };
112
+
113
+ if (item.startingPointId) data.startingPointId = item.startingPointId;
114
+ if (item.dropoffHotel && item.dropoffHotel.length > 0) data.dropoffHotel = item.dropoffHotel;
115
+ if (i > 0) data.partnerNetPrice = 0;
116
+ if (item.nationalParkFee) data.nationalParkFee = item.nationalParkFee;
117
+ if (item.tourGuideLanguage) data.tourGuideLanguage = item.tourGuideLanguage;
118
+
119
+ let endPointId = item.endPointId;
120
+ if (!endPointId) {
121
+ let endPoints = productSnapshot?.get('endPoints');
122
+ if (endPoints && endPoints.length === 1) {
123
+ endPointId = endPoints[0].id;
147
124
  }
125
+ }
126
+ if (endPointId) data.endPointId = endPointId;
148
127
 
149
- if (productSnapshot) {
150
- data.operatorNetPrice = calculateTotalOperatorPrice(
151
- data,
152
- productSnapshot.data()
153
- );
154
- } else {
155
- data.operatorNetPrice = 0;
156
- }
128
+ if (productSnapshot) {
129
+ data.operatorNetPrice = calculateTotalOperatorPrice(data, productSnapshot.data());
130
+ } else {
131
+ data.operatorNetPrice = 0;
132
+ }
157
133
 
158
- data = { ...data, ...paymentData };
134
+ data = { ...data, ...paymentData };
159
135
 
160
- let doc = firestore.collection('bookings').doc();
161
- transaction.set(doc, data);
162
- processed++;
163
- }
136
+ let doc = firestore.collection('bookings').doc();
137
+ transaction.set(doc, data);
138
+ processed++;
139
+ }
164
140
 
165
- transaction.set(
166
- bookingNumberDoc,
167
- {
168
- last: FieldValue.increment(processed),
169
- },
170
- { merge: true }
171
- );
172
- });
141
+ transaction.set(
142
+ bookingNumberDoc,
143
+ { last: currentLast + processed },
144
+ { merge: true }
145
+ );
173
146
  });
174
147
  }
175
148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tripstorekrabi-shared",
3
- "version": "1.24.4",
3
+ "version": "1.24.6",
4
4
  "description": "Shared library for Trip Store Krabi",
5
5
  "author": "Stefan Sommer <tonprince@gmail.com>",
6
6
  "repository": {
@@ -18,9 +18,9 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "dayjs": "^1.11.21",
21
- "libphonenumber-js": "^1.13.6",
22
- "nanoid": "^5.1.11",
23
- "firebase-admin": "^14.0.0"
21
+ "libphonenumber-js": "^1.13.7",
22
+ "nanoid": "^5.1.16",
23
+ "firebase-admin": "^14.1.0"
24
24
  },
25
25
  "scripts": {
26
26
  "deploy": "npm publish"
package/price.js CHANGED
@@ -15,67 +15,102 @@ export const PAX_EXTRA_PAYMENT = 'PAX';
15
15
  export const ADD_ON_EXTRA_PAYMENT = 'ADD-ON';
16
16
  export const PRODUCT_CHANGE_EXTRA_PAYMENT = 'PRODUCT CHANGE';
17
17
  export const OTHER_EXTRA_PAYMENT = 'OTHER';
18
- export const EXTRA_PAYMENT_CATEGORIES = [STARTING_POINT_EXTRA_PAYMENT,
18
+ export const EXTRA_PAYMENT_CATEGORIES = [
19
+ STARTING_POINT_EXTRA_PAYMENT,
19
20
  END_POINT_EXTRA_PAYMENT,
20
21
  NATIONAL_PARK_FEE_EXTRA_PAYMENT,
21
22
  PAX_EXTRA_PAYMENT,
22
23
  ADD_ON_EXTRA_PAYMENT,
23
24
  PRODUCT_CHANGE_EXTRA_PAYMENT,
24
- OTHER_EXTRA_PAYMENT];
25
+ OTHER_EXTRA_PAYMENT
26
+ ];
25
27
 
26
28
  const SELLING_MODE = 0;
27
29
  const PARTNER_MODE = 1;
28
30
  const OPERATOR_MODE = 2;
29
31
 
30
- export function getActiveRevision(date, pricing) {
31
- if (!pricing?.revisions || pricing.revisions.length === 0) return null;
32
+ function parseSafeDate(dateField) {
33
+ if (!dateField) return null;
34
+ if (typeof dateField.toDate === 'function') return dayjs(dateField.toDate());
35
+ if (dateField instanceof Date) return dayjs(dateField);
36
+ const parsed = dayjs(dateField);
37
+ return parsed.isValid() ? parsed : null;
38
+ }
32
39
 
33
- const getSafeDate = (dateField) => {
34
- if (!dateField) return null;
35
- if (typeof dateField.toDate === 'function') return dayjs(dateField.toDate());
36
- return dayjs(dateField);
37
- };
40
+ export function resolvePricingContext(pricing, bookingDate = dayjs(), departureDate = null) {
41
+ if (!pricing) return null;
38
42
 
39
- const getSortedActive = () => [...pricing.revisions]
40
- .filter(r => r.isActive !== false)
41
- .sort((a, b) => getSafeDate(a.startDate)?.valueOf() - getSafeDate(b.startDate)?.valueOf());
43
+ const safeBookingDate = parseSafeDate(bookingDate)?.startOf('day') || dayjs().startOf('day');
44
+ let activeRevision = pricing;
42
45
 
43
- const bookingDate = getSafeDate(date)?.startOf('day');
46
+ if (pricing.revisions?.length > 0) {
47
+ const validRevisions = pricing.revisions
48
+ .filter(r => r.isActive !== false)
49
+ .map(r => ({ ...r, parsedDate: parseSafeDate(r.startDate)?.startOf('day') }))
50
+ .filter(r => r.parsedDate && (r.parsedDate.isSame(safeBookingDate) || r.parsedDate.isBefore(safeBookingDate)))
51
+ .sort((a, b) => b.parsedDate.valueOf() - a.parsedDate.valueOf());
44
52
 
45
- if (!bookingDate || !bookingDate.isValid()) {
46
- return getSortedActive()[0] || pricing.revisions[0];
53
+ if (validRevisions.length > 0) {
54
+ activeRevision = { ...pricing, ...validRevisions[0] };
55
+ } else {
56
+ const sortedAll = [...pricing.revisions]
57
+ .filter(r => r.isActive !== false)
58
+ .sort((a, b) => (parseSafeDate(a.startDate)?.valueOf() || 0) - (parseSafeDate(b.startDate)?.valueOf() || 0));
59
+ activeRevision = { ...pricing, ...(sortedAll[0] || pricing.revisions[0]) };
60
+ }
47
61
  }
48
62
 
49
- const candidateRevisions = pricing.revisions.filter((rev) => {
50
- if (rev.isActive === false) return false;
51
- const revStart = getSafeDate(rev.startDate)?.startOf('day');
52
- if (!revStart || !revStart.isValid()) return false;
63
+ let activeVolumes = activeRevision.volumes || [];
53
64
 
54
- return revStart.isSame(bookingDate) || revStart.isBefore(bookingDate);
55
- });
65
+ if (activeRevision.promotion && departureDate) {
66
+ const depDate = parseSafeDate(departureDate);
67
+ const promoStart = parseSafeDate(activeRevision.promotion.startDate);
68
+ const promoEnd = parseSafeDate(activeRevision.promotion.endDate);
56
69
 
57
- if (candidateRevisions.length === 0) {
58
- return getSortedActive()[0] || pricing.revisions[0];
70
+ if (depDate?.isValid() && promoStart?.isValid() && promoEnd?.isValid() &&
71
+ depDate.isBetween(promoStart, promoEnd, "day", "[]")) {
72
+ activeVolumes = activeRevision.promotion.volumes || activeVolumes;
73
+ }
59
74
  }
60
75
 
61
- return candidateRevisions.sort((a, b) => {
62
- const aTime = getSafeDate(a.startDate)?.valueOf() || 0;
63
- const bTime = getSafeDate(b.startDate)?.valueOf() || 0;
64
- return bTime - aTime;
65
- })[0];
76
+ return {
77
+ ...activeRevision,
78
+ volumes: activeVolumes,
79
+ _isResolved: true
80
+ };
81
+ }
82
+
83
+ export function getActiveRevision(date, pricing) {
84
+ return resolvePricingContext(pricing, date, null);
66
85
  }
67
86
 
68
87
  export function getActivePricing(date, pricing) {
69
- const activeRevision = getActiveRevision(date, pricing);
88
+ return resolvePricingContext(pricing, date, null);
89
+ }
70
90
 
71
- if (!activeRevision) {
72
- return pricing;
73
- }
91
+ export function findVolume(people, context) {
92
+ const total = (people?.adults ?? 0) + (people?.children ?? 0);
74
93
 
75
- return {
76
- ...pricing,
77
- ...activeRevision
78
- };
94
+ const activeContext = context?._isResolved ? context : resolvePricingContext(context, people?.date, people?.departureDate);
95
+ const volumes = activeContext?.volumes;
96
+
97
+ if (!volumes || volumes.length === 0) return undefined;
98
+
99
+ const sortedVolumes = [...volumes].sort((v1, v2) => v1.max - v2.max);
100
+
101
+ return sortedVolumes.find(v => total <= v.max) || sortedVolumes[sortedVolumes.length - 1];
102
+ }
103
+
104
+ export function countMinPeople(pricing, bookingDate = dayjs(), departureDate = null) {
105
+ const context = resolvePricingContext(pricing, bookingDate, departureDate);
106
+ const minVals = context?.volumes?.map(v => v.min ?? 1) || [];
107
+ return minVals.length > 0 ? Math.max(Math.min(...minVals), 1) : 1;
108
+ }
109
+
110
+ export function countMaxPeople(pricing, bookingDate = dayjs(), departureDate = null) {
111
+ const context = resolvePricingContext(pricing, bookingDate, departureDate);
112
+ const maxVals = context?.volumes?.map(v => v.max ?? MAX_PAX) || [];
113
+ return maxVals.length > 0 ? Math.max(...maxVals) : MAX_PAX;
79
114
  }
80
115
 
81
116
  export function calculateProductSellingPrice(booking, pricing) {
@@ -91,26 +126,16 @@ export function calculateProductOperatorPrice(booking, pricing) {
91
126
  }
92
127
 
93
128
  function calculateSellingPrice(commissionRate = 0, price) {
94
- return Math.floor(
95
- (price * 100) /
96
- (100 -
97
- commissionRate)
98
- );
129
+ return Math.floor((price * 100) / (100 - commissionRate));
99
130
  }
100
131
 
101
132
  export function calculateInflatedSellingPrice(price, config, forceInflation = false) {
102
- if (!config || (!config.inflatedPrices && !forceInflation)) {
103
- return price;
104
- }
133
+ if (!config || (!config.inflatedPrices && !forceInflation)) return price;
105
134
 
106
135
  const discountRate = Number(config.discount) || 0;
107
-
108
- if (discountRate === 0 || !isFinite(discountRate)) {
109
- return price;
110
- }
136
+ if (discountRate === 0 || !isFinite(discountRate)) return price;
111
137
 
112
138
  const denominator = 100 - discountRate;
113
-
114
139
  if (denominator <= 0) {
115
140
  console.warn("Invalid discount rate for inflation calculation:", discountRate);
116
141
  return price;
@@ -121,163 +146,82 @@ export function calculateInflatedSellingPrice(price, config, forceInflation = fa
121
146
  }
122
147
 
123
148
  function calculateProductPrice(booking, pricing, options = { mode: SELLING_MODE }) {
124
- const activeRevision = getActiveRevision(booking.date, pricing) || pricing;
125
-
126
- if (!activeRevision) {
127
- throw new Error(`Pricing fail-safe triggered: No active or standard pricing found for booking date ${booking.date}.`);
128
- }
129
-
130
- let activeVolumes = activeRevision.volumes || [];
149
+ const context = resolvePricingContext(pricing, booking.date, booking.departureDate);
131
150
 
132
- if (activeRevision.promotion) {
133
- const departureDate = booking.departureDate instanceof Date ? dayjs(booking.departureDate) : typeof booking.departureDate?.toDate === 'function' ? dayjs(booking.departureDate.toDate()) : null;
134
- const startDate = activeRevision.promotion.startDate instanceof Date ? activeRevision.promotion.startDate : typeof activeRevision.promotion.startDate.toDate === 'function' ? activeRevision.promotion.startDate.toDate() : null;
135
- const endDate = activeRevision.promotion.endDate instanceof Date ? activeRevision.promotion.endDate : typeof activeRevision.promotion.endDate.toDate === 'function' ? activeRevision.promotion.endDate.toDate() : null;
136
-
137
- if (departureDate?.isValid() && departureDate?.isBetween(dayjs(startDate), dayjs(endDate), "day", "[]")) {
138
- activeVolumes = activeRevision.promotion.volumes || [];
139
- }
151
+ if (!context || !context.volumes || context.volumes.length === 0) {
152
+ console.warn("Pricing fail-safe triggered: No valid context or volumes found.");
153
+ return 0;
140
154
  }
141
155
 
142
- if (activeVolumes.length === 0) {
143
- console.warn("Pricing fail-safe triggered: Active revision has no volumes.");
156
+ const volume = findVolume(booking, context);
157
+ if (!volume) {
158
+ console.error(`No volume price found for ${booking.adults + booking.children} people`);
144
159
  return 0;
145
160
  }
146
161
 
147
- const context = {
148
- ...pricing,
149
- volumes: activeVolumes
150
- };
151
-
152
162
  let total = 0;
153
- const type = context.type;
154
-
155
- switch (type) {
156
- case "person": {
157
- const volume = findVolume(booking, context);
158
- if (!volume) {
159
- console.error(`No volume price found for the total amount of ${booking.adults + booking.children} people`);
160
- return 0;
161
- } else {
162
- const bookingSourceKey = getBookingSourceKey(booking.otaPartnerRef);
163
-
164
- if (volume[bookingSourceKey]) {
165
- const customPricing = volume[bookingSourceKey];
166
-
167
- if (options.mode === SELLING_MODE) {
168
- const commissionRate = context.commissionRates?.[bookingSourceKey];
169
- const baseNetPriceAdult = customPricing.adult.partnerNetPrice;
170
- const baseRetailPriceAdult = calculateSellingPrice(commissionRate, baseNetPriceAdult);
171
- const finalPriceAdult = calculateInflatedSellingPrice(baseRetailPriceAdult, customPricing);
172
-
173
- let finalPriceChild = 0;
174
- if (context.ageBands.child.active && volume.child && booking.children) {
175
- const baseNetPriceChild = customPricing.child.partnerNetPrice;
176
- const baseRetailPriceChild = calculateSellingPrice(commissionRate, baseNetPriceChild);
177
- finalPriceChild = calculateInflatedSellingPrice(baseRetailPriceChild, customPricing);
178
- }
179
-
180
- total = booking.adults * finalPriceAdult + (booking.children ? booking.children * finalPriceChild : 0);
181
- } else if (options.mode === PARTNER_MODE) {
182
- total = booking.adults * customPricing.adult.partnerNetPrice + (context.ageBands.child.active && volume.child && booking.children ? booking.children * customPricing.child.partnerNetPrice : 0);
183
- } else if (options.mode === OPERATOR_MODE) {
184
- total = booking.adults * (customPricing.adult.operatorNetPrice ?? volume.adult.operatorNetPrice) + (context.ageBands.child.active && volume.child && booking.children ? (booking.children * (customPricing.child.operatorNetPrice ?? volume.child.operatorNetPrice)) : 0);
185
- }
186
- } else {
187
- if (options.mode === SELLING_MODE || options.mode === PARTNER_MODE) {
188
- total = booking.adults * volume.adult.sellingPrice + (context.ageBands.child.active && volume.child && booking.children ? booking.children * volume.child.sellingPrice : 0);
189
- } else if (options.mode === OPERATOR_MODE) {
190
- total = booking.adults * volume.adult.operatorNetPrice + (context.ageBands.child.active && volume.child && booking.children ? booking.children * volume.child.operatorNetPrice : 0);
191
- }
163
+ const bookingSourceKey = getBookingSourceKey(booking.otaPartnerRef);
164
+
165
+ if (context.type === "person") {
166
+ if (volume[bookingSourceKey]) {
167
+ const customPricing = volume[bookingSourceKey];
168
+
169
+ if (options.mode === SELLING_MODE) {
170
+ const commRate = context.commissionRates?.[bookingSourceKey] || 0;
171
+ const finalPriceAdult = calculateInflatedSellingPrice(calculateSellingPrice(commRate, customPricing.adult.partnerNetPrice), customPricing);
172
+
173
+ let finalPriceChild = 0;
174
+ if (context.ageBands?.child?.active && volume.child && booking.children) {
175
+ finalPriceChild = calculateInflatedSellingPrice(calculateSellingPrice(commRate, customPricing.child.partnerNetPrice), customPricing);
192
176
  }
193
- break;
177
+ total = (booking.adults * finalPriceAdult) + (booking.children ? booking.children * finalPriceChild : 0);
178
+ } else if (options.mode === PARTNER_MODE) {
179
+ total = (booking.adults * customPricing.adult.partnerNetPrice) + (context.ageBands?.child?.active && volume.child && booking.children ? booking.children * customPricing.child.partnerNetPrice : 0);
180
+ } else if (options.mode === OPERATOR_MODE) {
181
+ total = (booking.adults * (customPricing.adult.operatorNetPrice ?? volume.adult.operatorNetPrice)) + (context.ageBands?.child?.active && volume.child && booking.children ? booking.children * (customPricing.child.operatorNetPrice ?? volume.child.operatorNetPrice) : 0);
182
+ }
183
+ } else {
184
+ if (options.mode === SELLING_MODE || options.mode === PARTNER_MODE) {
185
+ total = (booking.adults * volume.adult.sellingPrice) + (context.ageBands?.child?.active && volume.child && booking.children ? booking.children * volume.child.sellingPrice : 0);
186
+ } else if (options.mode === OPERATOR_MODE) {
187
+ total = (booking.adults * volume.adult.operatorNetPrice) + (context.ageBands?.child?.active && volume.child && booking.children ? booking.children * volume.child.operatorNetPrice : 0);
194
188
  }
195
189
  }
196
- case "group": {
197
- const volume = findVolume(booking, context);
198
- if (!volume) {
199
- console.error(`No volume price found for the total amount of ${booking.adults + booking.children} people`);
200
- return 0;
201
- } else {
202
- const bookingSourceKey = getBookingSourceKey(booking.otaPartnerRef);
203
-
204
- if (volume[bookingSourceKey]) {
205
- const customPricing = volume[bookingSourceKey];
206
-
207
- if (options.mode === SELLING_MODE) {
208
- const commissionRate = context.commissionRates?.[bookingSourceKey];
209
- const baseNetPrice = customPricing.partnerNetPrice;
210
- const baseRetailPrice = calculateSellingPrice(commissionRate, baseNetPrice);
211
- const finalPrice = calculateInflatedSellingPrice(baseRetailPrice, customPricing);
212
- total = finalPrice;
213
- } else if (options.mode === PARTNER_MODE) {
214
- total = customPricing.partnerNetPrice
215
- } else if (options.mode === OPERATOR_MODE) {
216
- total = customPricing.operatorNetPrice ?? volume.operatorNetPrice;
217
- }
218
- } else {
219
- if (options.mode === SELLING_MODE || options.mode === PARTNER_MODE) {
220
- total = volume.sellingPrice;
221
- } else if (options.mode === OPERATOR_MODE) {
222
- total = volume.operatorNetPrice;
223
- }
224
- }
225
-
226
- break;
190
+ } else if (context.type === "group") {
191
+ if (volume[bookingSourceKey]) {
192
+ const customPricing = volume[bookingSourceKey];
193
+
194
+ if (options.mode === SELLING_MODE) {
195
+ const commRate = context.commissionRates?.[bookingSourceKey] || 0;
196
+ total = calculateInflatedSellingPrice(calculateSellingPrice(commRate, customPricing.partnerNetPrice), customPricing);
197
+ } else if (options.mode === PARTNER_MODE) {
198
+ total = customPricing.partnerNetPrice;
199
+ } else if (options.mode === OPERATOR_MODE) {
200
+ total = customPricing.operatorNetPrice ?? volume.operatorNetPrice;
201
+ }
202
+ } else {
203
+ if (options.mode === SELLING_MODE || options.mode === PARTNER_MODE) {
204
+ total = volume.sellingPrice;
205
+ } else if (options.mode === OPERATOR_MODE) {
206
+ total = volume.operatorNetPrice;
227
207
  }
228
208
  }
229
- default:
230
- throw new Error(`Pricing type ${type} is not supported`);
209
+ } else {
210
+ throw new Error(`Pricing type ${context.type} is not supported`);
231
211
  }
232
212
 
233
213
  return total;
234
214
  }
235
215
 
236
- export function findVolume(people, context) {
237
- let total = (people.adults ?? 0) + (people.children ?? 0);
238
-
239
- if (!context?.volumes || context.volumes.length === 0) return undefined;
240
-
241
- const sortedVolumes = [...context.volumes].sort((v1, v2) => v1.max - v2.max);
242
-
243
- return sortedVolumes.find((item, i) => {
244
- if (total <= item.max) {
245
- return true;
246
- }
247
- else if (i === sortedVolumes.length - 1) {
248
- return true;
249
- }
250
- return false;
251
- });
252
- }
253
-
254
- export function countMinPeople(pricing) {
255
- const volumes = pricing?.volumes?.map((item) => item.min) || [];
256
- if (volumes.length === 0) return 1;
257
- return Math.max(Math.min(...volumes), 1);
258
- }
259
-
260
- export function countMaxPeople(pricing) {
261
- const volumes = pricing?.volumes?.map((item) => item.max) || [];
262
- if (volumes.length === 0) return MAX_PAX;
263
- return Math.max(...volumes);
264
- }
265
-
266
216
  export function getTotalAmountToBeReceived(booking, options = {}) {
267
217
  let filter = (item) => (!options.filter || options.filter(item)) && !item.disabled && item.paymentMethod !== PAY_ON_TOUR;
268
218
  return booking.customerPaymentStatus === "Refunded" ? 0 : booking.partnerNetPrice + calculateExtraPayments(booking, { ...options, mode: SELLING_MODE, filter })
269
219
  }
270
220
 
271
221
  export function calculatePartnerNetPrice(booking, product) {
272
- return getExtraPaymentsPaidWithPartner(
273
- booking,
274
- product
275
- ).reduce(
222
+ return getExtraPaymentsPaidWithPartner(booking, product).reduce(
276
223
  (prev, curr) => (prev ?? 0) + curr.price,
277
- calculateProductPartnerNetPrice(
278
- booking,
279
- product.pricing
280
- )
224
+ calculateProductPartnerNetPrice(booking, product.pricing)
281
225
  );
282
226
  }
283
227
 
@@ -301,24 +245,15 @@ function calculateTotalPrice(booking, product, options = { mode: SELLING_MODE })
301
245
  total += calculateExtraPayments(booking, {
302
246
  ...options,
303
247
  filter: (item) => {
304
- if (item.disabled === true) {
305
- return false;
306
- } else if (item.paymentMethod === PAY_ON_TOUR) {
307
- return true;
308
- } else if (item.paymentMethod === PAY_BEFORE && item.category === OTHER_EXTRA_PAYMENT) {
309
- return true;
310
- }
248
+ if (item.disabled === true) return false;
249
+ if (item.paymentMethod === PAY_ON_TOUR) return true;
250
+ if (item.paymentMethod === PAY_BEFORE && item.category === OTHER_EXTRA_PAYMENT) return true;
311
251
  },
312
252
  getPrice: (item, options) => {
313
253
  if (item.paymentMethod === PAY_ON_TOUR) {
314
- if (item.category !== OTHER_EXTRA_PAYMENT) {
315
- return -getExtraPaymentPrice(item, options);
316
- } else {
317
- return 0;
318
- }
319
- } else {
320
- return getExtraPaymentPrice(item, options);
254
+ return item.category !== OTHER_EXTRA_PAYMENT ? -getExtraPaymentPrice(item, options) : 0;
321
255
  }
256
+ return getExtraPaymentPrice(item, options);
322
257
  }
323
258
  });
324
259
  total += calculateAddOnsPrice(booking, product, options);
@@ -326,7 +261,9 @@ function calculateTotalPrice(booking, product, options = { mode: SELLING_MODE })
326
261
  }
327
262
 
328
263
  export function calculateNationalParkFee(booking, product) {
329
- return booking.nationalParkFee === ALREADY_PAID && product.nationalPark ? product.nationalPark.pricing.adult * booking.adults + product.nationalPark.pricing.child * booking.children : 0;
264
+ return booking.nationalParkFee === ALREADY_PAID && product.nationalPark
265
+ ? (product.nationalPark.pricing.adult * booking.adults) + (product.nationalPark.pricing.child * booking.children)
266
+ : 0;
330
267
  }
331
268
 
332
269
  function getExtraPaymentPrice(item, options) {
@@ -350,19 +287,15 @@ export function calculateTotalExtraPaymentsOperatorPrice(booking) {
350
287
  function calculateExtraPayments(booking, options = { mode: SELLING_MODE }) {
351
288
  let filter = options.filter ?? ((item) => !item.disabled);
352
289
  let getPrice = options.getPrice ?? getExtraPaymentPrice;
353
-
354
290
  let total = 0;
355
- for (const item of Object.values(booking.extraPayments).filter(filter)) {
356
- total += getPrice(item, options);
357
291
 
292
+ for (const item of Object.values(booking.extraPayments ?? {}).filter(filter)) {
293
+ total += getPrice(item, options);
358
294
  if (item.paymentMethod === PAY_ON_TOUR && options.mode === OPERATOR_MODE) {
359
295
  let ourCommission = item.price - item.operatorPrice;
360
- if (ourCommission > 0) {
361
- total -= ourCommission;
362
- }
296
+ if (ourCommission > 0) total -= ourCommission;
363
297
  }
364
298
  }
365
-
366
299
  return total;
367
300
  }
368
301
 
@@ -370,56 +303,32 @@ export function getExtraPaymentsPaidWithPartner(booking, product) {
370
303
  let startingPoint = findStartingPoint(product, booking.startingPointId);
371
304
  let endPoint = findEndPoint(product, booking.endPointId);
372
305
  let addOnDefs = {};
306
+
373
307
  (product.addOns ?? []).forEach((addOn) => {
374
308
  addOnDefs[addOn.id] = addOn;
375
309
  });
310
+
376
311
  let addOns = Object.entries(booking.addOns ?? {}).filter(([id]) => addOnDefs[id] != null).map(([id, addOn]) => {
377
312
  let addOnDef = addOnDefs[id];
378
-
379
313
  return {
380
314
  category: ADD_ON_EXTRA_PAYMENT,
381
315
  description: addOnDef.title,
382
- price: addOn.adults * addOnDef.pricing.adult.sellingPrice +
383
- (addOn.children ?? 0) * (addOnDef.pricing.child?.sellingPrice ?? 0),
384
- operatorPrice: addOn.adults * addOnDef.pricing.adult.operatorNetPrice +
385
- (addOn.children ?? 0) * (addOnDef.pricing.child?.operatorNetPrice ?? 0)
316
+ price: (addOn.adults * addOnDef.pricing.adult.sellingPrice) + ((addOn.children ?? 0) * (addOnDef.pricing.child?.sellingPrice ?? 0)),
317
+ operatorPrice: (addOn.adults * addOnDef.pricing.adult.operatorNetPrice) + ((addOn.children ?? 0) * (addOnDef.pricing.child?.operatorNetPrice ?? 0))
386
318
  }
387
319
  });
388
320
 
389
321
  return [
390
- {
391
- category: STARTING_POINT_EXTRA_PAYMENT,
392
- description: startingPoint.name,
393
- price: calculateStartEndPointSellingPrice(booking, startingPoint),
394
- operatorPrice: calculateStartEndPointOperatorPrice(booking, startingPoint),
395
- },
396
- {
397
- category: END_POINT_EXTRA_PAYMENT,
398
- description: endPoint.name,
399
- price: calculateStartEndPointSellingPrice(booking, endPoint),
400
- operatorPrice: calculateStartEndPointOperatorPrice(booking, endPoint),
401
- },
402
- {
403
- category: NATIONAL_PARK_FEE_EXTRA_PAYMENT,
404
- description: `Entrance fee to ${product.nationalPark?.title}`,
405
- price: calculateNationalParkFee(booking, product),
406
- operatorPrice: calculateNationalParkFee(booking, product),
407
- },
322
+ { category: STARTING_POINT_EXTRA_PAYMENT, description: startingPoint.name, price: calculateStartEndPointSellingPrice(booking, startingPoint), operatorPrice: calculateStartEndPointOperatorPrice(booking, startingPoint) },
323
+ { category: END_POINT_EXTRA_PAYMENT, description: endPoint.name, price: calculateStartEndPointSellingPrice(booking, endPoint), operatorPrice: calculateStartEndPointOperatorPrice(booking, endPoint) },
324
+ { category: NATIONAL_PARK_FEE_EXTRA_PAYMENT, description: `Entrance fee to ${product.nationalPark?.title}`, price: calculateNationalParkFee(booking, product), operatorPrice: calculateNationalParkFee(booking, product) },
408
325
  ...addOns
409
326
  ].filter(
410
- (item) =>
411
- (item.price > 0 || item.operatorPrice > 0) &&
412
- Object.values(booking.extraPayments).find(
413
- (extraPayment) =>
414
- extraPayment.category === item.category && !extraPayment.disabled
415
- ) == null
327
+ (item) => (item.price > 0 || item.operatorPrice > 0) &&
328
+ !Object.values(booking.extraPayments ?? {}).find(ep => ep.category === item.category && !ep.disabled)
416
329
  ).map((item) => {
417
- if (booking.customerPaymentStatus === "Refunded") {
418
- item.price = 0;
419
- }
420
- if (booking.operatorPaymentStatus === "No Charge") {
421
- item.operatorPrice = 0;
422
- }
330
+ if (booking.customerPaymentStatus === "Refunded") item.price = 0;
331
+ if (booking.operatorPaymentStatus === "No Charge") item.operatorPrice = 0;
423
332
  return item;
424
333
  });
425
334
  }
@@ -427,31 +336,18 @@ export function getExtraPaymentsPaidWithPartner(booking, product) {
427
336
  export function getActiveExtraPayments(extraPayments) {
428
337
  return [...extraPayments]
429
338
  .filter(([id, item]) => !item.disabled)
430
- .sort(([id1, item1], [id2, item2]) =>
431
- item1.description.localeCompare(item2.description)
432
- );
339
+ .sort(([id1, item1], [id2, item2]) => item1.description.localeCompare(item2.description));
433
340
  }
434
341
 
435
342
  export function getExtraPaymentsOperatorPrice(extraPayments) {
436
- return getActiveExtraPayments(extraPayments).reduce(
437
- (prev, curr) => {
438
- let price;
439
- let item = curr[1];
440
- if (item.paymentMethod === PAY_ON_TOUR) {
441
- let ourCommission = item.price - item.operatorPrice;
442
- if (ourCommission > 0) {
443
- price = -ourCommission;
444
- } else {
445
- price = 0;
446
- }
447
- } else {
448
- price = item.operatorPrice;
449
- }
450
-
451
- return prev + price;
452
- },
453
- 0
454
- );
343
+ return getActiveExtraPayments(extraPayments).reduce((prev, curr) => {
344
+ let item = curr[1];
345
+ if (item.paymentMethod === PAY_ON_TOUR) {
346
+ let ourCommission = item.price - item.operatorPrice;
347
+ return prev + (ourCommission > 0 ? -ourCommission : 0);
348
+ }
349
+ return prev + item.operatorPrice;
350
+ }, 0);
455
351
  }
456
352
 
457
353
  export function calculateAddOnsPrice(booking, product, options = { mode: SELLING_MODE }) {
@@ -461,11 +357,8 @@ export function calculateAddOnsPrice(booking, product, options = { mode: SELLING
461
357
  for (const [id, addOn] of Object.entries(booking.addOns ?? {})) {
462
358
  let addOnDef = findAddOn(id, product.addOns ?? []);
463
359
  if (addOnDef) {
464
- total += addOn.adults * (isSelling ? addOnDef.pricing.adult
465
- .sellingPrice : addOnDef.pricing.adult
466
- .operatorNetPrice) + (addOn.children > 0 ? addOn.children * (isSelling ? addOnDef.pricing.child
467
- .sellingPrice : addOnDef.pricing.child
468
- .operatorNetPrice) : 0);
360
+ total += (addOn.adults * (isSelling ? addOnDef.pricing.adult.sellingPrice : addOnDef.pricing.adult.operatorNetPrice)) +
361
+ (addOn.children > 0 ? addOn.children * (isSelling ? addOnDef.pricing.child.sellingPrice : addOnDef.pricing.child.operatorNetPrice) : 0);
469
362
  }
470
363
  }
471
364
  return total;
@@ -486,32 +379,23 @@ export function calculateStartEndPointPartnerNetPrice(people, startEndPoint) {
486
379
  function calculateStartEndPointPrice(people, startEndPoint, options = { mode: SELLING_MODE }) {
487
380
  let pricing = startEndPoint?.pricing;
488
381
  let isSelling = [SELLING_MODE, PARTNER_MODE].includes(options.mode);
382
+
489
383
  if (pricing) {
384
+ const priceAmount = isSelling ? (pricing.sellingPrice ?? pricing.amount) : pricing.operatorNetPrice;
490
385
  if (pricing.type === "group") {
491
- return (isSelling ? (pricing.sellingPrice ?? pricing.amount) : pricing.operatorNetPrice) * Math.ceil((people.adults + people.children) / pricing.max);
386
+ return priceAmount * Math.ceil((people.adults + people.children) / pricing.max);
492
387
  } else if (pricing.type === "person") {
493
- return (isSelling ? (pricing.sellingPrice ?? pricing.amount) : pricing.operatorNetPrice) * (people.adults + people.children);
494
- } else {
495
- return 0;
388
+ return priceAmount * (people.adults + people.children);
496
389
  }
497
- } else {
498
- return 0;
499
390
  }
391
+ return 0;
500
392
  }
501
393
 
502
394
  export function findStartingPoint(product, startingPointId, useDefault = true) {
503
395
  return [
504
396
  ...product.startingPoints,
505
- {
506
- id: TELL_HOTEL_LATER_STARTING_POINT.id,
507
- name: TELL_HOTEL_LATER_STARTING_POINT.description,
508
- value: '',
509
- },
510
- {
511
- id: CUSTOM_MEETING_POINT.id,
512
- name: CUSTOM_MEETING_POINT.description,
513
- value: '',
514
- },
397
+ { id: TELL_HOTEL_LATER_STARTING_POINT.id, name: TELL_HOTEL_LATER_STARTING_POINT.description, value: '' },
398
+ { id: CUSTOM_MEETING_POINT.id, name: CUSTOM_MEETING_POINT.description, value: '' },
515
399
  ].find((item) => item.id === startingPointId) ?? (useDefault ? product.startingPoints[0] : null);
516
400
  }
517
401
 
@@ -520,9 +404,7 @@ export function findEndPoint(product, endPointId, useDefault = true) {
520
404
  }
521
405
 
522
406
  export function findAddOn(id, addOns) {
523
- return addOns.find((item) =>
524
- item.id === id
525
- );
407
+ return addOns.find((item) => item.id === id);
526
408
  }
527
409
 
528
410
  export function inRange(expected, actual, boundary = 10) {