@jsdev_ninja/core 0.12.8 → 0.12.9
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/core.cjs.js +1 -1
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.es.js +234 -231
- package/dist/core.es.js.map +1 -1
- package/dist/core.umd.js +1 -1
- package/dist/core.umd.js.map +1 -1
- package/dist/entities/Discount/strategies/BundleStrategy.d.ts.map +1 -1
- package/dist/entities/Discount/strategies/BundleStrategy.js +7 -9
- package/dist/entities/Discount/types.d.ts.map +1 -1
- package/dist/entities/Discount/types.js +6 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +11 -3
- package/lib/entities/Discount/strategies/BundleStrategy.ts +115 -119
- package/lib/entities/Discount/types.ts +44 -44
- package/lib/utils/index.ts +17 -7
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BundleStrategy.d.ts","sourceRoot":"","sources":["../../../../lib/entities/Discount/strategies/BundleStrategy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEtE,qBAAa,sBAAuB,YAAW,gBAAgB;
|
|
1
|
+
{"version":3,"file":"BundleStrategy.d.ts","sourceRoot":"","sources":["../../../../lib/entities/Discount/strategies/BundleStrategy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEtE,qBAAa,sBAAuB,YAAW,gBAAgB;IAC9D,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;IAchE,SAAS,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc;IA0CxE,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,sBAAsB;IAI9B,OAAO,CAAC,wBAAwB;IAwBhC,OAAO,CAAC,kBAAkB;CAmB1B"}
|
|
@@ -16,7 +16,7 @@ export class BundleDiscountStrategy {
|
|
|
16
16
|
}
|
|
17
17
|
const { productsId, requiredQuantity, bundlePrice } = discount.variant;
|
|
18
18
|
// Get applicable items (only the products in the bundle)
|
|
19
|
-
const applicableItems = context.cart.filter(item => productsId.includes(item.product.id));
|
|
19
|
+
const applicableItems = context.cart.filter((item) => productsId.includes(item.product.id));
|
|
20
20
|
const totalQuantity = this.getTotalQuantity(context.cart, productsId);
|
|
21
21
|
const bundleCount = Math.floor(totalQuantity / requiredQuantity);
|
|
22
22
|
if (bundleCount === 0) {
|
|
@@ -38,17 +38,15 @@ export class BundleDiscountStrategy {
|
|
|
38
38
|
}
|
|
39
39
|
isDiscountActive(discount) {
|
|
40
40
|
const now = Date.now();
|
|
41
|
-
return discount.active &&
|
|
42
|
-
discount.startDate <= now &&
|
|
43
|
-
discount.endDate >= now;
|
|
41
|
+
return discount.active && discount.startDate <= now && discount.endDate >= now;
|
|
44
42
|
}
|
|
45
43
|
getTotalQuantity(cart, productIds) {
|
|
46
44
|
return cart
|
|
47
|
-
.filter(item => productIds.includes(item.product.id))
|
|
45
|
+
.filter((item) => productIds.includes(item.product.id))
|
|
48
46
|
.reduce((sum, item) => sum + item.amount, 0);
|
|
49
47
|
}
|
|
50
48
|
calculateOriginalPrice(items) {
|
|
51
|
-
return items.reduce((sum, item) => sum +
|
|
49
|
+
return items.reduce((sum, item) => sum + item.product.price * item.amount, 0);
|
|
52
50
|
}
|
|
53
51
|
calculateDiscountedPrice(originalPrice, bundlePrice, bundleCount, requiredQuantity, totalQuantity) {
|
|
54
52
|
// For bundle discounts, we pay the bundle price for each complete bundle
|
|
@@ -66,13 +64,13 @@ export class BundleDiscountStrategy {
|
|
|
66
64
|
}
|
|
67
65
|
distributeDiscount(items, totalDiscount, originalPrice) {
|
|
68
66
|
const discountRatio = totalDiscount / originalPrice;
|
|
69
|
-
return items.map(item => {
|
|
70
|
-
const itemDiscount =
|
|
67
|
+
return items.map((item) => {
|
|
68
|
+
const itemDiscount = item.product.price * item.amount * discountRatio;
|
|
71
69
|
return {
|
|
72
70
|
productId: item.product.id,
|
|
73
71
|
quantity: item.amount,
|
|
74
72
|
originalPrice: Number(item.product.price.toFixed(2)),
|
|
75
|
-
discountedPrice: Number((item.product.price -
|
|
73
|
+
discountedPrice: Number((item.product.price - itemDiscount / item.amount).toFixed(2)),
|
|
76
74
|
discountAmount: Number(itemDiscount.toFixed(2)),
|
|
77
75
|
};
|
|
78
76
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/entities/Discount/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,wBAAwB;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/entities/Discount/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,wBAAwB;;;;;;;;;GAKzB,CAAC;AAGb,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;IAOhC,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWzB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACvD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACpE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAG1E,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,KAAK,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;CACH;AAED,MAAM,WAAW,eAAe;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,KAAK,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE;YACR,EAAE,EAAE,MAAM,CAAC;YACX,KAAK,EAAE,MAAM,CAAC;SACd,CAAC;KACF,CAAC,CAAC;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,gBAAgB,EAAE,eAAe,EAAE,CAAC;CACpC"}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
// Simple discount conditions
|
|
3
|
-
export const DiscountConditionsSchema = z
|
|
3
|
+
export const DiscountConditionsSchema = z
|
|
4
|
+
.object({
|
|
4
5
|
minSpend: z.number().positive().optional(),
|
|
5
6
|
stackable: z.boolean().default(false),
|
|
6
|
-
})
|
|
7
|
+
})
|
|
8
|
+
.optional();
|
|
7
9
|
// Bundle discount - buy X items for Y price
|
|
8
10
|
export const DiscountVariantSchema = z.discriminatedUnion("variantType", [
|
|
9
11
|
z.object({
|
|
10
12
|
variantType: z.literal("bundle"),
|
|
11
|
-
productsId: z.array(z.string()).min(1), // Which products are included
|
|
13
|
+
productsId: z.array(z.string().nonempty()).min(1), // Which products are included
|
|
12
14
|
requiredQuantity: z.number().positive(), // How many items needed (e.g., 3)
|
|
13
15
|
bundlePrice: z.number().positive(), // Total price for the bundle (e.g., $25)
|
|
14
16
|
}),
|
|
@@ -18,7 +20,7 @@ export const DiscountSchema = z.object({
|
|
|
18
20
|
storeId: z.string().min(1),
|
|
19
21
|
companyId: z.string().min(1),
|
|
20
22
|
id: z.string().min(1),
|
|
21
|
-
name: z.array(z.object({ lang: z.enum(["he"]), value: z.string() })),
|
|
23
|
+
name: z.array(z.object({ lang: z.enum(["he"]), value: z.string().nonempty() })),
|
|
22
24
|
active: z.boolean(),
|
|
23
25
|
startDate: z.number(),
|
|
24
26
|
endDate: z.number(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAY,MAAM,EAAE,MAAM,aAAa,CAAC;AA8BjE,wBAAgB,WAAW,CAAC,EAC3B,IAAI,EACJ,SAAS,EACT,KAAK,GACL,EAAE;IACF,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACrB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAY,MAAM,EAAE,MAAM,aAAa,CAAC;AA8BjE,wBAAgB,WAAW,CAAC,EAC3B,IAAI,EACJ,SAAS,EACT,KAAK,GACL,EAAE;IACF,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACrB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsFA"}
|
package/dist/utils/index.js
CHANGED
|
@@ -35,6 +35,7 @@ export function getCartCost({ cart, discounts, store, }) {
|
|
|
35
35
|
}));
|
|
36
36
|
// Apply discounts using the new discount engine
|
|
37
37
|
const discountResult = DiscountEngine.calculateDiscounts(cartForEngine, discounts);
|
|
38
|
+
console.log("discountResult", discountResult);
|
|
38
39
|
// Map the results back to the original format with additional product info
|
|
39
40
|
const result = cart.map((item, index) => {
|
|
40
41
|
const engineItem = discountResult.items[index];
|
|
@@ -64,10 +65,17 @@ export function getCartCost({ cart, discounts, store, }) {
|
|
|
64
65
|
}
|
|
65
66
|
acc.vat = Number((acc.vat + vat).toFixed(2));
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
// Round finalPrice to prevent floating point errors from discount engine
|
|
69
|
+
const roundedFinalPrice = Number(finalPrice.toFixed(2));
|
|
70
|
+
acc.cost += amount * roundedFinalPrice;
|
|
68
71
|
acc.discount += finalDiscount ? amount * finalDiscount : finalDiscount;
|
|
69
|
-
acc.finalCost += amount *
|
|
70
|
-
acc.productsCost += amount *
|
|
72
|
+
acc.finalCost += amount * roundedFinalPrice + (isVatIncludedInPrice ? 0 : productVatValue);
|
|
73
|
+
acc.productsCost += amount * roundedFinalPrice + (isVatIncludedInPrice ? 0 : productVatValue);
|
|
74
|
+
// Round all accumulated values to prevent floating point errors
|
|
75
|
+
acc.cost = Number(acc.cost.toFixed(2));
|
|
76
|
+
acc.discount = Number(acc.discount.toFixed(2));
|
|
77
|
+
acc.finalCost = Number(acc.finalCost.toFixed(2));
|
|
78
|
+
acc.productsCost = Number(acc.productsCost.toFixed(2));
|
|
71
79
|
return acc;
|
|
72
80
|
}, {
|
|
73
81
|
discount: 0,
|
|
@@ -2,122 +2,118 @@ import { DiscountStrategy } from "../strategy";
|
|
|
2
2
|
import { TDiscount, DiscountResult, DiscountContext } from "../types";
|
|
3
3
|
|
|
4
4
|
export class BundleDiscountStrategy implements DiscountStrategy {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
5
|
+
canApply(discount: TDiscount, context: DiscountContext): boolean {
|
|
6
|
+
if (discount.variant.variantType !== "bundle") return false;
|
|
7
|
+
|
|
8
|
+
// Check if discount is active
|
|
9
|
+
if (!this.isDiscountActive(discount)) return false;
|
|
10
|
+
|
|
11
|
+
const { productsId, requiredQuantity } = discount.variant;
|
|
12
|
+
|
|
13
|
+
// Check if cart has enough of the required products
|
|
14
|
+
const totalQuantity = this.getTotalQuantity(context.cart, productsId);
|
|
15
|
+
|
|
16
|
+
return totalQuantity >= requiredQuantity;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
calculate(discount: TDiscount, context: DiscountContext): DiscountResult {
|
|
20
|
+
if (discount.variant.variantType !== "bundle") {
|
|
21
|
+
return { applicable: false, discountAmount: 0, affectedItems: [] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { productsId, requiredQuantity, bundlePrice } = discount.variant;
|
|
25
|
+
|
|
26
|
+
// Get applicable items (only the products in the bundle)
|
|
27
|
+
const applicableItems = context.cart.filter((item) => productsId.includes(item.product.id));
|
|
28
|
+
|
|
29
|
+
const totalQuantity = this.getTotalQuantity(context.cart, productsId);
|
|
30
|
+
const bundleCount = Math.floor(totalQuantity / requiredQuantity);
|
|
31
|
+
|
|
32
|
+
if (bundleCount === 0) {
|
|
33
|
+
return { applicable: false, discountAmount: 0, affectedItems: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Calculate original price for bundled items
|
|
37
|
+
const originalPrice = this.calculateOriginalPrice(applicableItems);
|
|
38
|
+
|
|
39
|
+
// Calculate discounted price
|
|
40
|
+
const cartTotalQuantity = this.getTotalQuantity(context.cart, productsId);
|
|
41
|
+
const discountedPrice = this.calculateDiscountedPrice(
|
|
42
|
+
originalPrice,
|
|
43
|
+
bundlePrice,
|
|
44
|
+
bundleCount,
|
|
45
|
+
requiredQuantity,
|
|
46
|
+
cartTotalQuantity
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const totalDiscount = originalPrice - discountedPrice;
|
|
50
|
+
|
|
51
|
+
// Distribute discount proportionally among items
|
|
52
|
+
const affectedItems = this.distributeDiscount(applicableItems, totalDiscount, originalPrice);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
applicable: true,
|
|
56
|
+
discountAmount: Number(totalDiscount.toFixed(2)),
|
|
57
|
+
affectedItems,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private isDiscountActive(discount: TDiscount): boolean {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
return discount.active && discount.startDate <= now && discount.endDate >= now;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private getTotalQuantity(cart: DiscountContext["cart"], productIds: string[]): number {
|
|
67
|
+
return cart
|
|
68
|
+
.filter((item) => productIds.includes(item.product.id))
|
|
69
|
+
.reduce((sum, item) => sum + item.amount, 0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private calculateOriginalPrice(items: DiscountContext["cart"]): number {
|
|
73
|
+
return items.reduce((sum, item) => sum + item.product.price * item.amount, 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private calculateDiscountedPrice(
|
|
77
|
+
originalPrice: number,
|
|
78
|
+
bundlePrice: number,
|
|
79
|
+
bundleCount: number,
|
|
80
|
+
requiredQuantity: number,
|
|
81
|
+
totalQuantity: number
|
|
82
|
+
): number {
|
|
83
|
+
// For bundle discounts, we pay the bundle price for each complete bundle
|
|
84
|
+
// and full price for any remaining items
|
|
85
|
+
const bundledItemsPrice = bundlePrice * bundleCount;
|
|
86
|
+
|
|
87
|
+
// For bundle discounts, we pay bundle price for complete bundles
|
|
88
|
+
// and full price for remaining items
|
|
89
|
+
// Since we're dealing with the total original price, we need to calculate
|
|
90
|
+
// how much of the original price represents the bundled items vs remaining items
|
|
91
|
+
const itemsInBundles = bundleCount * requiredQuantity;
|
|
92
|
+
const remainingItems = Math.max(0, totalQuantity - itemsInBundles);
|
|
93
|
+
|
|
94
|
+
// Calculate the proportion of original price that represents remaining items
|
|
95
|
+
const remainingItemsPrice = (remainingItems / totalQuantity) * originalPrice;
|
|
96
|
+
|
|
97
|
+
return bundledItemsPrice + remainingItemsPrice;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private distributeDiscount(
|
|
101
|
+
items: DiscountContext["cart"],
|
|
102
|
+
totalDiscount: number,
|
|
103
|
+
originalPrice: number
|
|
104
|
+
): DiscountResult["affectedItems"] {
|
|
105
|
+
const discountRatio = totalDiscount / originalPrice;
|
|
106
|
+
|
|
107
|
+
return items.map((item) => {
|
|
108
|
+
const itemDiscount = item.product.price * item.amount * discountRatio;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
productId: item.product.id,
|
|
112
|
+
quantity: item.amount,
|
|
113
|
+
originalPrice: Number(item.product.price.toFixed(2)),
|
|
114
|
+
discountedPrice: Number((item.product.price - itemDiscount / item.amount).toFixed(2)),
|
|
115
|
+
discountAmount: Number(itemDiscount.toFixed(2)),
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
// Simple discount conditions
|
|
4
|
-
export const DiscountConditionsSchema = z
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
export const DiscountConditionsSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
minSpend: z.number().positive().optional(),
|
|
7
|
+
stackable: z.boolean().default(false),
|
|
8
|
+
})
|
|
9
|
+
.optional();
|
|
8
10
|
|
|
9
11
|
// Bundle discount - buy X items for Y price
|
|
10
12
|
export const DiscountVariantSchema = z.discriminatedUnion("variantType", [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
z.object({
|
|
14
|
+
variantType: z.literal("bundle"),
|
|
15
|
+
productsId: z.array(z.string().nonempty()).min(1), // Which products are included
|
|
16
|
+
requiredQuantity: z.number().positive(), // How many items needed (e.g., 3)
|
|
17
|
+
bundlePrice: z.number().positive(), // Total price for the bundle (e.g., $25)
|
|
18
|
+
}),
|
|
17
19
|
]);
|
|
18
20
|
|
|
19
21
|
export const DiscountSchema = z.object({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
type: z.literal("Discount"),
|
|
23
|
+
storeId: z.string().min(1),
|
|
24
|
+
companyId: z.string().min(1),
|
|
25
|
+
id: z.string().min(1),
|
|
26
|
+
name: z.array(z.object({ lang: z.enum(["he"]), value: z.string().nonempty() })),
|
|
27
|
+
active: z.boolean(),
|
|
28
|
+
startDate: z.number(),
|
|
29
|
+
endDate: z.number(),
|
|
30
|
+
variant: DiscountVariantSchema,
|
|
31
|
+
conditions: DiscountConditionsSchema,
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
export type TDiscount = z.infer<typeof DiscountSchema>;
|
|
@@ -35,34 +37,32 @@ export type DiscountConditions = z.infer<typeof DiscountConditionsSchema>;
|
|
|
35
37
|
|
|
36
38
|
// Simple result types
|
|
37
39
|
export interface DiscountResult {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
applicable: boolean;
|
|
41
|
+
discountAmount: number;
|
|
42
|
+
affectedItems: Array<{
|
|
43
|
+
productId: string;
|
|
44
|
+
quantity: number;
|
|
45
|
+
originalPrice: number;
|
|
46
|
+
discountedPrice: number;
|
|
47
|
+
discountAmount: number;
|
|
48
|
+
}>;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface AppliedDiscount {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
discountId: string;
|
|
53
|
+
discountName: string;
|
|
54
|
+
discountAmount: number;
|
|
55
|
+
affectedItems: DiscountResult["affectedItems"];
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface DiscountContext {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
cart: Array<{
|
|
60
|
+
amount: number;
|
|
61
|
+
product: {
|
|
62
|
+
id: string;
|
|
63
|
+
price: number;
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
66
|
+
user?: Record<string, unknown>;
|
|
67
|
+
appliedDiscounts: AppliedDiscount[];
|
|
66
68
|
}
|
|
67
|
-
|
|
68
|
-
|
package/lib/utils/index.ts
CHANGED
|
@@ -38,19 +38,19 @@ export function getCartCost({
|
|
|
38
38
|
store: TStore;
|
|
39
39
|
}) {
|
|
40
40
|
const { isVatIncludedInPrice } = store;
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
// Convert cart items to the format expected by the discount engine
|
|
43
|
-
const cartForEngine = cart.map(item => ({
|
|
43
|
+
const cartForEngine = cart.map((item) => ({
|
|
44
44
|
amount: item.amount,
|
|
45
45
|
product: {
|
|
46
46
|
id: item.product.id,
|
|
47
47
|
price: item.product.price,
|
|
48
48
|
},
|
|
49
49
|
}));
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
// Apply discounts using the new discount engine
|
|
52
52
|
const discountResult = DiscountEngine.calculateDiscounts(cartForEngine, discounts);
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
// Map the results back to the original format with additional product info
|
|
55
55
|
const result = cart.map((item, index) => {
|
|
56
56
|
const engineItem = discountResult.items[index];
|
|
@@ -86,10 +86,20 @@ export function getCartCost({
|
|
|
86
86
|
acc.vat = Number((acc.vat + vat).toFixed(2));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
// Round finalPrice to prevent floating point errors from discount engine
|
|
90
|
+
const roundedFinalPrice = Number(finalPrice.toFixed(2));
|
|
91
|
+
|
|
92
|
+
acc.cost += amount * roundedFinalPrice;
|
|
90
93
|
acc.discount += finalDiscount ? amount * finalDiscount : finalDiscount;
|
|
91
|
-
acc.finalCost += amount *
|
|
92
|
-
acc.productsCost +=
|
|
94
|
+
acc.finalCost += amount * roundedFinalPrice + (isVatIncludedInPrice ? 0 : productVatValue);
|
|
95
|
+
acc.productsCost +=
|
|
96
|
+
amount * roundedFinalPrice + (isVatIncludedInPrice ? 0 : productVatValue);
|
|
97
|
+
|
|
98
|
+
// Round all accumulated values to prevent floating point errors
|
|
99
|
+
acc.cost = Number(acc.cost.toFixed(2));
|
|
100
|
+
acc.discount = Number(acc.discount.toFixed(2));
|
|
101
|
+
acc.finalCost = Number(acc.finalCost.toFixed(2));
|
|
102
|
+
acc.productsCost = Number(acc.productsCost.toFixed(2));
|
|
93
103
|
|
|
94
104
|
return acc;
|
|
95
105
|
},
|