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 CHANGED
@@ -2,3 +2,4 @@ export * from './common';
2
2
  export * from './productCart';
3
3
  export * from './filters';
4
4
  export * from './accounting';
5
+ export * from './samples';
package/dist/index.js CHANGED
@@ -18,3 +18,4 @@ __exportStar(require("./common"), exports);
18
18
  __exportStar(require("./productCart"), exports);
19
19
  __exportStar(require("./filters"), exports);
20
20
  __exportStar(require("./accounting"), exports);
21
+ __exportStar(require("./samples"), exports);
@@ -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
+ }>;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopoflex-types",
3
- "version": "1.0.181",
3
+ "version": "1.0.183",
4
4
  "description": "Shared TypeScript types for Shopoflex applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",