shopoflex-types 1.0.181 → 1.0.183
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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/samples.d.ts +40 -0
- package/dist/samples.js +418 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Address, Discount, IZone } from "./common";
|
|
2
|
+
import { CartItem } from "./productCart";
|
|
3
|
+
export interface SampleTaxConfig {
|
|
4
|
+
type: "fixed" | "perCountry";
|
|
5
|
+
fixed?: {
|
|
6
|
+
active: boolean;
|
|
7
|
+
show: boolean;
|
|
8
|
+
payer: "vendor" | "customer";
|
|
9
|
+
percentage: number;
|
|
10
|
+
};
|
|
11
|
+
perCountry?: Record<string, {
|
|
12
|
+
active: boolean;
|
|
13
|
+
show: boolean;
|
|
14
|
+
payer: "vendor" | "customer";
|
|
15
|
+
percentage: number;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export declare const calculateCartTotalsWithDiscounts: (items: CartItem[], deliveryFee: number, allDiscounts: Discount[], manualDiscount?: Discount, vendorTaxConfig?: SampleTaxConfig, customerAddress?: Address | any) => {
|
|
19
|
+
items: CartItem[];
|
|
20
|
+
subtotal: number;
|
|
21
|
+
totalQuantity: number;
|
|
22
|
+
delivery: number;
|
|
23
|
+
discount: number;
|
|
24
|
+
total: number;
|
|
25
|
+
taxInfo: {
|
|
26
|
+
taxAmount: number;
|
|
27
|
+
taxRate: number;
|
|
28
|
+
shouldShowTax: boolean;
|
|
29
|
+
customerPays: boolean;
|
|
30
|
+
vendorTaxAmount: number;
|
|
31
|
+
};
|
|
32
|
+
totalDiscount: number;
|
|
33
|
+
};
|
|
34
|
+
export declare const calculateDeliveryFeeForZones: (zones: IZone[], location: {
|
|
35
|
+
lat: number;
|
|
36
|
+
lng: number;
|
|
37
|
+
}, mapkey: string) => Promise<{
|
|
38
|
+
fee: number;
|
|
39
|
+
isValid: boolean;
|
|
40
|
+
}>;
|
package/dist/samples.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateDeliveryFeeForZones = exports.calculateCartTotalsWithDiscounts = void 0;
|
|
4
|
+
const calculateCartTotalsWithDiscounts = (items, deliveryFee, allDiscounts, manualDiscount, vendorTaxConfig, customerAddress) => {
|
|
5
|
+
// Calculate initial totals for each item
|
|
6
|
+
const itemsWithTotals = items.map((item) => {
|
|
7
|
+
const basePrice = item.selectedVariant?.priceModel?.price || 0;
|
|
8
|
+
const modifiersPrice = item.modifiersPrice || 0;
|
|
9
|
+
const unitPrice = basePrice + modifiersPrice;
|
|
10
|
+
const totalPrice = unitPrice * item.quantity;
|
|
11
|
+
return {
|
|
12
|
+
...item,
|
|
13
|
+
basePrice,
|
|
14
|
+
modifiersPrice,
|
|
15
|
+
unitPrice,
|
|
16
|
+
totalPrice,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
const subtotal = itemsWithTotals.reduce((sum, item) => sum + (item.totalPrice ?? 0), 0);
|
|
20
|
+
const totalQuantity = itemsWithTotals.reduce((sum, item) => sum + item.quantity, 0);
|
|
21
|
+
// Create base cart for discount calculations
|
|
22
|
+
const baseCart = {
|
|
23
|
+
items: itemsWithTotals,
|
|
24
|
+
subtotal,
|
|
25
|
+
totalQuantity,
|
|
26
|
+
delivery: deliveryFee,
|
|
27
|
+
discount: 0,
|
|
28
|
+
total: subtotal + deliveryFee,
|
|
29
|
+
};
|
|
30
|
+
// Apply discounts and get updated cart
|
|
31
|
+
const applyDiscounts = (cart, allDiscounts, manualDiscount) => {
|
|
32
|
+
if (!cart)
|
|
33
|
+
return { updatedCart: cart, totalDiscount: 0 };
|
|
34
|
+
const currentDate = new Date();
|
|
35
|
+
let cartLevelDiscountAmount = 0; // Only cart_total discounts
|
|
36
|
+
let deliveryDiscountAmount = 0; // Only free_delivery discounts
|
|
37
|
+
// Helper function to calculate discount amount (inlined)
|
|
38
|
+
const calculateAmount = (discount, subtotal, deliveryFee) => {
|
|
39
|
+
if (!discount)
|
|
40
|
+
return 0;
|
|
41
|
+
let applicableTotal = 0;
|
|
42
|
+
// Determine applicable total based on discount category
|
|
43
|
+
if (discount.discountCategory === "free_delivery") {
|
|
44
|
+
applicableTotal = deliveryFee;
|
|
45
|
+
}
|
|
46
|
+
else if (discount.discountCategory === "cart_total") {
|
|
47
|
+
applicableTotal = subtotal;
|
|
48
|
+
}
|
|
49
|
+
else if (discount.discountCategory === "category_discount") {
|
|
50
|
+
// Calculate total basePrice of items in applicable categories (excludes modifiers)
|
|
51
|
+
applicableTotal = cart.items.reduce((total, item) => {
|
|
52
|
+
const itemCategoryIds = Array.isArray(item.categoriesIds)
|
|
53
|
+
? item.categoriesIds
|
|
54
|
+
: [];
|
|
55
|
+
const hasApplicableCategory = itemCategoryIds.some((catId) => discount.applicableCategories.some((discountCatId) => catId.toString() === discountCatId.toString()));
|
|
56
|
+
const isExcluded = itemCategoryIds.some((catId) => discount.excludedCategories.some((excludedCatId) => catId.toString() === excludedCatId.toString()));
|
|
57
|
+
if (hasApplicableCategory && !isExcluded) {
|
|
58
|
+
return total + (item.basePrice || 0) * (item.quantity || 1);
|
|
59
|
+
}
|
|
60
|
+
return total;
|
|
61
|
+
}, 0);
|
|
62
|
+
}
|
|
63
|
+
else if (discount.discountCategory === "product_discount") {
|
|
64
|
+
// Calculate total basePrice of applicable products (excludes modifiers)
|
|
65
|
+
applicableTotal = cart.items.reduce((total, item) => {
|
|
66
|
+
if (!item._id)
|
|
67
|
+
return total;
|
|
68
|
+
const isApplicable = discount.applicableProducts.some((prodId) => item._id.toString() === prodId.toString());
|
|
69
|
+
const isExcluded = discount.excludedProducts.some((excludedProdId) => item._id.toString() === excludedProdId.toString());
|
|
70
|
+
if (isApplicable && !isExcluded) {
|
|
71
|
+
return total + (item.basePrice || 0) * (item.quantity || 1);
|
|
72
|
+
}
|
|
73
|
+
return total;
|
|
74
|
+
}, 0);
|
|
75
|
+
}
|
|
76
|
+
let discountAmount = 0;
|
|
77
|
+
// Calculate discount based on type
|
|
78
|
+
if (discount.type === "fixed") {
|
|
79
|
+
discountAmount = discount.value;
|
|
80
|
+
}
|
|
81
|
+
else if (discount.type === "percentage") {
|
|
82
|
+
discountAmount = (applicableTotal * discount.value) / 100;
|
|
83
|
+
// Apply maximum discount cap if specified
|
|
84
|
+
if (discount.maxDiscountAmount) {
|
|
85
|
+
discountAmount = Math.min(discountAmount, discount.maxDiscountAmount);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Ensure discount doesn't exceed applicable total
|
|
89
|
+
return Math.min(discountAmount, applicableTotal);
|
|
90
|
+
};
|
|
91
|
+
// Find automatic discounts that should be applied
|
|
92
|
+
const automaticDiscounts = allDiscounts.filter((discount) => {
|
|
93
|
+
return (discount.isAutomatic &&
|
|
94
|
+
discount.status === "active" &&
|
|
95
|
+
(!discount.startDate || new Date(discount.startDate) <= currentDate) &&
|
|
96
|
+
(!discount.endDate || new Date(discount.endDate) >= currentDate) &&
|
|
97
|
+
(!discount.usageLimit || discount.usageCount < discount.usageLimit) &&
|
|
98
|
+
cart.subtotal >= discount.minCartValue);
|
|
99
|
+
});
|
|
100
|
+
// Apply manual discount if provided
|
|
101
|
+
if (manualDiscount) {
|
|
102
|
+
const manualDiscountAmount = calculateAmount(manualDiscount, cart.subtotal, cart.delivery || 0);
|
|
103
|
+
if (manualDiscount.discountCategory === "free_delivery") {
|
|
104
|
+
deliveryDiscountAmount += manualDiscountAmount;
|
|
105
|
+
}
|
|
106
|
+
else if (manualDiscount.discountCategory === "cart_total") {
|
|
107
|
+
cartLevelDiscountAmount += manualDiscountAmount;
|
|
108
|
+
}
|
|
109
|
+
// Product and category discounts are handled at item level, not here
|
|
110
|
+
}
|
|
111
|
+
// Apply automatic discounts with fixed combination rules:
|
|
112
|
+
// 1. Different categories always combine
|
|
113
|
+
// 2. Same categories never combine (manual wins)
|
|
114
|
+
// 3. Manual discount always wins over automatic discounts of same category
|
|
115
|
+
for (const autoDiscount of automaticDiscounts) {
|
|
116
|
+
let canApply = false;
|
|
117
|
+
if (!manualDiscount) {
|
|
118
|
+
// No manual discount, apply automatic discount
|
|
119
|
+
canApply = true;
|
|
120
|
+
}
|
|
121
|
+
else if (autoDiscount.discountCategory !== manualDiscount.discountCategory) {
|
|
122
|
+
// Different categories always combine
|
|
123
|
+
canApply = true;
|
|
124
|
+
}
|
|
125
|
+
// Same category with manual discount = skip automatic (manual wins)
|
|
126
|
+
if (canApply) {
|
|
127
|
+
const autoDiscountAmount = calculateAmount(autoDiscount, cart.subtotal, cart.delivery || 0);
|
|
128
|
+
if (autoDiscount.discountCategory === "free_delivery") {
|
|
129
|
+
deliveryDiscountAmount += autoDiscountAmount;
|
|
130
|
+
}
|
|
131
|
+
else if (autoDiscount.discountCategory === "cart_total") {
|
|
132
|
+
cartLevelDiscountAmount += autoDiscountAmount;
|
|
133
|
+
}
|
|
134
|
+
// Product and category discounts are handled at item level, not here
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Ensure discounts don't exceed their respective totals
|
|
138
|
+
cartLevelDiscountAmount = Math.min(cartLevelDiscountAmount, cart.subtotal);
|
|
139
|
+
deliveryDiscountAmount = Math.min(deliveryDiscountAmount, cart.delivery || 0);
|
|
140
|
+
const finalDeliveryFee = Math.max(0, (cart.delivery || 0) - deliveryDiscountAmount);
|
|
141
|
+
// Apply discounts to individual cart items
|
|
142
|
+
const updatedItems = cart.items.map((item) => {
|
|
143
|
+
// Recalculate item prices based on current quantity
|
|
144
|
+
const basePrice = item.selectedVariant?.priceModel?.price || item.basePrice || 0;
|
|
145
|
+
const modifiersPrice = item.modifiersPrice || 0;
|
|
146
|
+
const unitPrice = basePrice + modifiersPrice;
|
|
147
|
+
let itemDiscountAmount = 0;
|
|
148
|
+
const itemBasePrice = basePrice * (item.quantity || 1);
|
|
149
|
+
// Check if item is affected by manual discount
|
|
150
|
+
if (manualDiscount &&
|
|
151
|
+
manualDiscount.discountCategory !== "free_delivery") {
|
|
152
|
+
if (isItemEligibleForDiscount(item, manualDiscount)) {
|
|
153
|
+
itemDiscountAmount += calculateItemDiscountAmount(item, manualDiscount, cart);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Check if item is affected by automatic discounts
|
|
157
|
+
for (const autoDiscount of automaticDiscounts) {
|
|
158
|
+
if (autoDiscount.discountCategory === "free_delivery")
|
|
159
|
+
continue;
|
|
160
|
+
let canApply = false;
|
|
161
|
+
if (!manualDiscount) {
|
|
162
|
+
canApply = true;
|
|
163
|
+
}
|
|
164
|
+
else if (autoDiscount.discountCategory !== manualDiscount.discountCategory) {
|
|
165
|
+
canApply = true;
|
|
166
|
+
}
|
|
167
|
+
if (canApply && isItemEligibleForDiscount(item, autoDiscount)) {
|
|
168
|
+
itemDiscountAmount += calculateItemDiscountAmount(item, autoDiscount, cart);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Ensure item discount doesn't exceed item's base price
|
|
172
|
+
itemDiscountAmount = Math.min(itemDiscountAmount, itemBasePrice);
|
|
173
|
+
// Calculate new prices using recalculated values
|
|
174
|
+
const discountedBasePrice = Math.max(0, itemBasePrice - itemDiscountAmount);
|
|
175
|
+
const newTotalPrice = discountedBasePrice + modifiersPrice;
|
|
176
|
+
return {
|
|
177
|
+
...item,
|
|
178
|
+
basePrice,
|
|
179
|
+
modifiersPrice,
|
|
180
|
+
unitPrice,
|
|
181
|
+
discountAmount: itemDiscountAmount,
|
|
182
|
+
discountedBasePrice: discountedBasePrice,
|
|
183
|
+
totalPrice: newTotalPrice,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
// Helper function to check if item is eligible for discount
|
|
187
|
+
function isItemEligibleForDiscount(item, discount) {
|
|
188
|
+
if (discount.discountCategory === "cart_total") {
|
|
189
|
+
// Cart total discounts should NOT be applied to individual items
|
|
190
|
+
// They affect the cart totals only
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
else if (discount.discountCategory === "category_discount") {
|
|
194
|
+
const itemCategoryIds = Array.isArray(item.categoriesIds)
|
|
195
|
+
? item.categoriesIds
|
|
196
|
+
: [];
|
|
197
|
+
const hasApplicableCategory = itemCategoryIds.some((catId) => discount.applicableCategories.some((discountCatId) => catId.toString() === discountCatId.toString()));
|
|
198
|
+
const isExcluded = itemCategoryIds.some((catId) => discount.excludedCategories.some((excludedCatId) => catId.toString() === excludedCatId.toString()));
|
|
199
|
+
return hasApplicableCategory && !isExcluded;
|
|
200
|
+
}
|
|
201
|
+
else if (discount.discountCategory === "product_discount") {
|
|
202
|
+
if (!item._id)
|
|
203
|
+
return false;
|
|
204
|
+
const isApplicable = discount.applicableProducts.some((prodId) => item._id.toString() === prodId.toString());
|
|
205
|
+
const isExcluded = discount.excludedProducts.some((excludedProdId) => item._id.toString() === excludedProdId.toString());
|
|
206
|
+
return isApplicable && !isExcluded;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
// Helper function to calculate discount amount for a specific item
|
|
211
|
+
function calculateItemDiscountAmount(item, discount, cart) {
|
|
212
|
+
const itemBasePrice = (item.basePrice || 0) * (item.quantity || 1);
|
|
213
|
+
if (discount.type === "fixed") {
|
|
214
|
+
// For fixed discounts, distribute proportionally across eligible items
|
|
215
|
+
const totalEligibleValue = cart.items.reduce((total, cartItem) => {
|
|
216
|
+
if (isItemEligibleForDiscount(cartItem, discount)) {
|
|
217
|
+
return (total + (cartItem.basePrice || 0) * (cartItem.quantity || 1));
|
|
218
|
+
}
|
|
219
|
+
return total;
|
|
220
|
+
}, 0);
|
|
221
|
+
if (totalEligibleValue > 0) {
|
|
222
|
+
const itemProportion = itemBasePrice / totalEligibleValue;
|
|
223
|
+
return discount.value * itemProportion;
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
else if (discount.type === "percentage") {
|
|
228
|
+
let discountAmount = (itemBasePrice * discount.value) / 100;
|
|
229
|
+
// Apply maximum discount cap proportionally if specified
|
|
230
|
+
if (discount.maxDiscountAmount) {
|
|
231
|
+
const totalEligibleValue = cart.items.reduce((total, cartItem) => {
|
|
232
|
+
if (isItemEligibleForDiscount(cartItem, discount)) {
|
|
233
|
+
return (total + (cartItem.basePrice || 0) * (cartItem.quantity || 1));
|
|
234
|
+
}
|
|
235
|
+
return total;
|
|
236
|
+
}, 0);
|
|
237
|
+
if (totalEligibleValue > 0) {
|
|
238
|
+
const itemProportion = itemBasePrice / totalEligibleValue;
|
|
239
|
+
const maxDiscountForItem = discount.maxDiscountAmount * itemProportion;
|
|
240
|
+
discountAmount = Math.min(discountAmount, maxDiscountForItem);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return discountAmount;
|
|
244
|
+
}
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
// Recalculate subtotal based on updated items
|
|
248
|
+
const newSubtotal = updatedItems.reduce((total, item) => total + (item.totalPrice || 0), 0);
|
|
249
|
+
// Cap cart discount to not exceed the available subtotal
|
|
250
|
+
const cappedCartLevelDiscountAmount = Math.min(cartLevelDiscountAmount, newSubtotal);
|
|
251
|
+
const updatedCart = {
|
|
252
|
+
...cart,
|
|
253
|
+
items: updatedItems,
|
|
254
|
+
subtotal: newSubtotal,
|
|
255
|
+
discount: cappedCartLevelDiscountAmount, // Only cart-level discounts show in discount field
|
|
256
|
+
delivery: finalDeliveryFee, // Already reduced by delivery discounts
|
|
257
|
+
total: Math.max(0, newSubtotal - cappedCartLevelDiscountAmount + finalDeliveryFee),
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
updatedCart,
|
|
261
|
+
totalDiscount: cappedCartLevelDiscountAmount + deliveryDiscountAmount,
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
const { updatedCart, totalDiscount } = applyDiscounts(baseCart, allDiscounts, manualDiscount);
|
|
265
|
+
// Calculate vendor tax if configuration is provided
|
|
266
|
+
let taxInfo = {
|
|
267
|
+
taxAmount: 0,
|
|
268
|
+
taxRate: 0,
|
|
269
|
+
shouldShowTax: false,
|
|
270
|
+
customerPays: false,
|
|
271
|
+
vendorTaxAmount: 0,
|
|
272
|
+
};
|
|
273
|
+
const calculateTaxForOrder = (subtotal, vendorTaxConfig, customerCountry) => {
|
|
274
|
+
if (!vendorTaxConfig || !vendorTaxConfig.type) {
|
|
275
|
+
return {
|
|
276
|
+
taxAmount: 0,
|
|
277
|
+
taxRate: 0,
|
|
278
|
+
shouldShowTax: false,
|
|
279
|
+
customerPays: false,
|
|
280
|
+
vendorTaxAmount: 0,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
let taxConfig = null;
|
|
284
|
+
// Handle fixed tax configuration
|
|
285
|
+
if (vendorTaxConfig.type === "fixed" && vendorTaxConfig.fixed) {
|
|
286
|
+
taxConfig = vendorTaxConfig.fixed;
|
|
287
|
+
}
|
|
288
|
+
// Handle per-country tax configuration
|
|
289
|
+
else if (vendorTaxConfig.type === "perCountry" &&
|
|
290
|
+
vendorTaxConfig.perCountry &&
|
|
291
|
+
customerCountry) {
|
|
292
|
+
// Find exact country match (case-insensitive)
|
|
293
|
+
const countryKey = Object.keys(vendorTaxConfig.perCountry).find((key) => key.toLowerCase() === customerCountry.toLowerCase());
|
|
294
|
+
if (countryKey) {
|
|
295
|
+
taxConfig = vendorTaxConfig.perCountry[countryKey];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!taxConfig || !taxConfig.active) {
|
|
299
|
+
return {
|
|
300
|
+
taxAmount: 0,
|
|
301
|
+
taxRate: 0,
|
|
302
|
+
shouldShowTax: false,
|
|
303
|
+
customerPays: false,
|
|
304
|
+
vendorTaxAmount: 0,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const taxRate = taxConfig.percentage / 100;
|
|
308
|
+
const calculatedTaxAmount = subtotal * taxRate;
|
|
309
|
+
const customerPays = taxConfig.payer === "customer";
|
|
310
|
+
const shouldShowTax = taxConfig.show;
|
|
311
|
+
return {
|
|
312
|
+
taxAmount: customerPays ? calculatedTaxAmount : 0, // Only add tax to total if customer pays
|
|
313
|
+
taxRate,
|
|
314
|
+
shouldShowTax,
|
|
315
|
+
customerPays,
|
|
316
|
+
vendorTaxAmount: !customerPays ? calculatedTaxAmount : 0, // Track vendor tax amount for display
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
if (vendorTaxConfig) {
|
|
320
|
+
const customerCountry = customerAddress?.country;
|
|
321
|
+
const calculatedTax = calculateTaxForOrder(updatedCart.subtotal, vendorTaxConfig, customerCountry);
|
|
322
|
+
taxInfo = {
|
|
323
|
+
...calculatedTax,
|
|
324
|
+
vendorTaxAmount: calculatedTax.vendorTaxAmount || 0,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
// Final total calculation with tax
|
|
328
|
+
const finalTotal = updatedCart.total + taxInfo.taxAmount;
|
|
329
|
+
return {
|
|
330
|
+
items: updatedCart.items,
|
|
331
|
+
subtotal: updatedCart.subtotal,
|
|
332
|
+
totalQuantity: updatedCart.totalQuantity,
|
|
333
|
+
delivery: updatedCart.delivery,
|
|
334
|
+
discount: updatedCart.discount,
|
|
335
|
+
total: finalTotal,
|
|
336
|
+
taxInfo,
|
|
337
|
+
totalDiscount,
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
exports.calculateCartTotalsWithDiscounts = calculateCartTotalsWithDiscounts;
|
|
341
|
+
const calculateDeliveryFeeForZones = async (zones, location, mapkey) => {
|
|
342
|
+
let totalFee = 0;
|
|
343
|
+
let isValid = false;
|
|
344
|
+
const calculateDistance = (point1, point2) => {
|
|
345
|
+
if (!point1 ||
|
|
346
|
+
!point2 ||
|
|
347
|
+
typeof point1.lat !== "number" ||
|
|
348
|
+
typeof point1.lng !== "number" ||
|
|
349
|
+
typeof point2.lat !== "number" ||
|
|
350
|
+
typeof point2.lng !== "number") {
|
|
351
|
+
console.error("❌ [Distance] Invalid coordinates:", { point1, point2 });
|
|
352
|
+
return Infinity; // Invalid coordinates means infinite distance
|
|
353
|
+
}
|
|
354
|
+
const R = 6371;
|
|
355
|
+
const dLat = (point2.lat - point1.lat) * (Math.PI / 180);
|
|
356
|
+
const dLng = (point2.lng - point1.lng) * (Math.PI / 180);
|
|
357
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
358
|
+
Math.cos(point1.lat * (Math.PI / 180)) *
|
|
359
|
+
Math.cos(point2.lat * (Math.PI / 180)) *
|
|
360
|
+
Math.sin(dLng / 2) *
|
|
361
|
+
Math.sin(dLng / 2);
|
|
362
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
363
|
+
const distance = R * c;
|
|
364
|
+
return distance;
|
|
365
|
+
};
|
|
366
|
+
for (let i = 0; i < zones.length; i++) {
|
|
367
|
+
const zone = zones[i];
|
|
368
|
+
const distance = calculateDistance(zone.markerPosition, location);
|
|
369
|
+
let zoneValid = false;
|
|
370
|
+
const isInCountry = async (location, country, mapkey) => {
|
|
371
|
+
if (!mapkey) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${location.lat},${location.lng}&key=${mapkey}`;
|
|
376
|
+
const response = await fetch(url);
|
|
377
|
+
const data = await response.json();
|
|
378
|
+
if (data.status === "OK" && data.results?.[0]) {
|
|
379
|
+
const countryComponent = data.results[0].address_components.find((c) => c.types.includes("country"));
|
|
380
|
+
// Check both long_name and short_name
|
|
381
|
+
const longNameMatch = countryComponent?.long_name?.toLowerCase() ===
|
|
382
|
+
country.toLowerCase();
|
|
383
|
+
const shortNameMatch = countryComponent?.short_name?.toLowerCase() ===
|
|
384
|
+
country.toLowerCase();
|
|
385
|
+
return longNameMatch || shortNameMatch;
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
console.error("❌ [Country Check] Error during country validation:", error);
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
if (zone.deliveryRange === "radius" && distance <= (zone.radius || 0)) {
|
|
395
|
+
zoneValid = true;
|
|
396
|
+
}
|
|
397
|
+
else if (zone.deliveryRange === "country") {
|
|
398
|
+
// For country-based zones, check both country AND radius as fallback
|
|
399
|
+
const countryValid = await isInCountry(location, zone.country || "lebanon", mapkey);
|
|
400
|
+
const radiusValid = zone.radius && distance <= zone.radius;
|
|
401
|
+
// Accept if either country check passes OR within radius (fallback)
|
|
402
|
+
zoneValid = countryValid || radiusValid;
|
|
403
|
+
}
|
|
404
|
+
if (zoneValid) {
|
|
405
|
+
isValid = true;
|
|
406
|
+
if (zone.deliveryType === "constant") {
|
|
407
|
+
const fee = parseFloat(zone.deliveryFee || "0");
|
|
408
|
+
totalFee += fee;
|
|
409
|
+
}
|
|
410
|
+
else if (zone.deliveryType === "distance") {
|
|
411
|
+
const fee = distance * parseFloat(zone.pricePerKm || "0");
|
|
412
|
+
totalFee += fee;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { fee: totalFee, isValid };
|
|
417
|
+
};
|
|
418
|
+
exports.calculateDeliveryFeeForZones = calculateDeliveryFeeForZones;
|