@jsdev_ninja/core 0.13.27 → 0.13.28
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 +2 -0
- package/dist/core.cjs.js.map +1 -0
- package/dist/core.es.js +3614 -0
- package/dist/core.es.js.map +1 -0
- package/dist/core.umd.js +2 -0
- package/dist/core.umd.js.map +1 -0
- package/dist/lib/entities/Address.d.ts +28 -0
- package/dist/lib/entities/Address.d.ts.map +1 -0
- package/dist/lib/entities/Address.js +10 -0
- package/dist/lib/entities/Atoms.d.ts +16 -0
- package/dist/lib/entities/Atoms.d.ts.map +1 -0
- package/{lib/entities/Atoms.ts → dist/lib/entities/Atoms.js} +2 -6
- package/dist/lib/entities/Cart.d.ts +1278 -0
- package/dist/lib/entities/Cart.d.ts.map +1 -0
- package/dist/lib/entities/Cart.js +18 -0
- package/dist/lib/entities/Category.d.ts +103 -0
- package/dist/lib/entities/Category.d.ts.map +1 -0
- package/dist/lib/entities/Category.js +20 -0
- package/dist/lib/entities/Company.d.ts +16 -0
- package/dist/lib/entities/Company.d.ts.map +1 -0
- package/dist/lib/entities/Company.js +6 -0
- package/dist/lib/entities/DeliveryNote.d.ts +248 -0
- package/dist/lib/entities/DeliveryNote.d.ts.map +1 -0
- package/dist/lib/entities/DeliveryNote.js +61 -0
- package/dist/lib/entities/Discount/__tests__/engine.test.d.ts +2 -0
- package/dist/lib/entities/Discount/__tests__/engine.test.d.ts.map +1 -0
- package/dist/lib/entities/Discount/__tests__/engine.test.js +298 -0
- package/dist/lib/entities/Discount/__tests__/factory.test.d.ts +2 -0
- package/dist/lib/entities/Discount/__tests__/factory.test.d.ts.map +1 -0
- package/dist/lib/entities/Discount/__tests__/factory.test.js +75 -0
- package/dist/lib/entities/Discount/__tests__/integration.test.d.ts +2 -0
- package/dist/lib/entities/Discount/__tests__/integration.test.d.ts.map +1 -0
- package/dist/lib/entities/Discount/__tests__/integration.test.js +115 -0
- package/dist/lib/entities/Discount/__tests__/simple.test.d.ts +2 -0
- package/dist/lib/entities/Discount/__tests__/simple.test.d.ts.map +1 -0
- package/dist/lib/entities/Discount/__tests__/simple.test.js +186 -0
- package/dist/lib/entities/Discount/__tests__/utils.test.d.ts +2 -0
- package/dist/lib/entities/Discount/__tests__/utils.test.d.ts.map +1 -0
- package/dist/lib/entities/Discount/__tests__/utils.test.js +55 -0
- package/dist/lib/entities/Discount/engine.d.ts +29 -0
- package/dist/lib/entities/Discount/engine.d.ts.map +1 -0
- package/dist/lib/entities/Discount/engine.js +85 -0
- package/dist/lib/entities/Discount/factory.d.ts +10 -0
- package/dist/lib/entities/Discount/factory.d.ts.map +1 -0
- package/dist/lib/entities/Discount/factory.js +18 -0
- package/dist/lib/entities/Discount/index.d.ts +9 -0
- package/dist/lib/entities/Discount/index.d.ts.map +1 -0
- package/{lib/entities/Discount/index.ts → dist/lib/entities/Discount/index.js} +0 -2
- package/dist/lib/entities/Discount/strategies/BundleStrategy.d.ts +12 -0
- package/dist/lib/entities/Discount/strategies/BundleStrategy.d.ts.map +1 -0
- package/dist/lib/entities/Discount/strategies/BundleStrategy.js +78 -0
- package/dist/lib/entities/Discount/strategies/__tests__/BundleStrategy.test.d.ts +2 -0
- package/dist/lib/entities/Discount/strategies/__tests__/BundleStrategy.test.d.ts.map +1 -0
- package/dist/lib/entities/Discount/strategies/__tests__/BundleStrategy.test.js +265 -0
- package/dist/lib/entities/Discount/strategy.d.ts +6 -0
- package/dist/lib/entities/Discount/strategy.d.ts.map +1 -0
- package/dist/lib/entities/Discount/types.d.ts +148 -0
- package/dist/lib/entities/Discount/types.d.ts.map +1 -0
- package/dist/lib/entities/Discount/types.js +29 -0
- package/dist/lib/entities/Discount/utils.d.ts +29 -0
- package/dist/lib/entities/Discount/utils.d.ts.map +1 -0
- package/{lib/entities/Discount/utils.ts → dist/lib/entities/Discount/utils.js} +12 -15
- package/dist/lib/entities/FavoriteProduct.d.ts +25 -0
- package/dist/lib/entities/FavoriteProduct.d.ts.map +1 -0
- package/dist/lib/entities/FavoriteProduct.js +9 -0
- package/dist/lib/entities/Invoice.d.ts +210 -0
- package/dist/lib/entities/Invoice.d.ts.map +1 -0
- package/dist/lib/entities/Invoice.js +49 -0
- package/dist/lib/entities/Locale.d.ts +22 -0
- package/dist/lib/entities/Locale.d.ts.map +1 -0
- package/{lib/entities/Locale.ts → dist/lib/entities/Locale.js} +2 -4
- package/dist/lib/entities/Order.d.ts +1806 -0
- package/dist/lib/entities/Order.d.ts.map +1 -0
- package/dist/lib/entities/Order.js +62 -0
- package/dist/lib/entities/Organization.d.ts +100 -0
- package/dist/lib/entities/Organization.d.ts.map +1 -0
- package/dist/lib/entities/Organization.js +16 -0
- package/dist/lib/entities/Product.d.ts +694 -0
- package/dist/lib/entities/Product.d.ts.map +1 -0
- package/dist/lib/entities/Product.js +61 -0
- package/dist/lib/entities/Profile.d.ts +98 -0
- package/dist/lib/entities/Profile.d.ts.map +1 -0
- package/dist/lib/entities/Profile.js +50 -0
- package/dist/lib/entities/Store.d.ts +93 -0
- package/dist/lib/entities/Store.d.ts.map +1 -0
- package/dist/lib/entities/Store.js +20 -0
- package/dist/lib/entities/index.d.ts +15 -0
- package/dist/lib/entities/index.d.ts.map +1 -0
- package/dist/lib/firebase-api/app.d.ts +2 -0
- package/dist/lib/firebase-api/app.d.ts.map +1 -0
- package/dist/lib/firebase-api/app.js +16 -0
- package/dist/lib/firebase-api/index.d.ts +75 -0
- package/dist/lib/firebase-api/index.d.ts.map +1 -0
- package/dist/lib/firebase-api/index.js +33 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/utils/index.d.ts +109 -0
- package/dist/lib/utils/index.d.ts.map +1 -0
- package/dist/lib/utils/index.js +95 -0
- package/dist/tsconfig.app.tsbuildinfo +1 -0
- package/dist/tsconfig.node.tsbuildinfo +1 -0
- package/dist/vite.config.d.ts +3 -0
- package/dist/vite.config.d.ts.map +1 -0
- package/dist/vite.config.js +25 -0
- package/package.json +8 -1
- package/eslint.config.js +0 -28
- package/index.html +0 -13
- package/lib/entities/Address.ts +0 -13
- package/lib/entities/Cart.ts +0 -24
- package/lib/entities/Category.ts +0 -35
- package/lib/entities/Company.ts +0 -9
- package/lib/entities/DeliveryNote.ts +0 -70
- package/lib/entities/Discount/__tests__/engine.test.ts +0 -337
- package/lib/entities/Discount/__tests__/factory.test.ts +0 -91
- package/lib/entities/Discount/__tests__/integration.test.ts +0 -132
- package/lib/entities/Discount/__tests__/simple.test.ts +0 -215
- package/lib/entities/Discount/__tests__/utils.test.ts +0 -69
- package/lib/entities/Discount/engine.ts +0 -138
- package/lib/entities/Discount/factory.ts +0 -25
- package/lib/entities/Discount/strategies/BundleStrategy.ts +0 -119
- package/lib/entities/Discount/strategies/__tests__/BundleStrategy.test.ts +0 -295
- package/lib/entities/Discount/strategy.ts +0 -6
- package/lib/entities/Discount/types.ts +0 -68
- package/lib/entities/FavoriteProduct.ts +0 -12
- package/lib/entities/Invoice.ts +0 -57
- package/lib/entities/Order.ts +0 -67
- package/lib/entities/Organization.ts +0 -23
- package/lib/entities/Product.ts +0 -68
- package/lib/entities/Profile.ts +0 -56
- package/lib/entities/Store.ts +0 -24
- package/lib/firebase-api/app.ts +0 -18
- package/lib/firebase-api/index.ts +0 -46
- package/lib/utils/index.ts +0 -127
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.app.json +0 -20
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -27
- package/vitest.config.ts +0 -10
- /package/{src/main.tsx → dist/lib/entities/Discount/strategy.js} +0 -0
- /package/{lib/entities/index.ts → dist/lib/entities/index.js} +0 -0
- /package/{lib/index.tsx → dist/lib/index.js} +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { DiscountEngine } from "../engine";
|
|
3
|
+
import { DiscountStrategyFactory } from "../factory";
|
|
4
|
+
import { BundleDiscountStrategy } from "../strategies/BundleStrategy";
|
|
5
|
+
describe("Simple Discount System Tests", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
DiscountStrategyFactory.clearStrategies();
|
|
8
|
+
DiscountStrategyFactory.registerStrategy("bundle", new BundleDiscountStrategy());
|
|
9
|
+
});
|
|
10
|
+
it("should calculate basic bundle discount correctly", () => {
|
|
11
|
+
const cart = [
|
|
12
|
+
{
|
|
13
|
+
amount: 2,
|
|
14
|
+
product: {
|
|
15
|
+
id: "product1",
|
|
16
|
+
price: 10,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
amount: 1,
|
|
21
|
+
product: {
|
|
22
|
+
id: "product2",
|
|
23
|
+
price: 15,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
const discounts = [
|
|
28
|
+
{
|
|
29
|
+
type: "Discount",
|
|
30
|
+
storeId: "store1",
|
|
31
|
+
companyId: "company1",
|
|
32
|
+
id: "bundle1",
|
|
33
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
34
|
+
active: true,
|
|
35
|
+
startDate: Date.now() - 1000,
|
|
36
|
+
endDate: Date.now() + 1000,
|
|
37
|
+
variant: {
|
|
38
|
+
variantType: "bundle",
|
|
39
|
+
productsId: ["product1", "product2"],
|
|
40
|
+
requiredQuantity: 3,
|
|
41
|
+
bundlePrice: 25,
|
|
42
|
+
},
|
|
43
|
+
conditions: {
|
|
44
|
+
stackable: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
const result = DiscountEngine.calculateDiscounts(cart, discounts);
|
|
49
|
+
expect(result.totalDiscount).toBe(10);
|
|
50
|
+
expect(result.appliedDiscounts).toHaveLength(1);
|
|
51
|
+
});
|
|
52
|
+
it("should not apply inactive discount", () => {
|
|
53
|
+
const cart = [
|
|
54
|
+
{
|
|
55
|
+
amount: 3,
|
|
56
|
+
product: {
|
|
57
|
+
id: "product1",
|
|
58
|
+
price: 10,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
const discounts = [
|
|
63
|
+
{
|
|
64
|
+
type: "Discount",
|
|
65
|
+
storeId: "store1",
|
|
66
|
+
companyId: "company1",
|
|
67
|
+
id: "bundle1",
|
|
68
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
69
|
+
active: false, // Inactive
|
|
70
|
+
startDate: Date.now() - 1000,
|
|
71
|
+
endDate: Date.now() + 1000,
|
|
72
|
+
variant: {
|
|
73
|
+
variantType: "bundle",
|
|
74
|
+
productsId: ["product1"],
|
|
75
|
+
requiredQuantity: 3,
|
|
76
|
+
bundlePrice: 25,
|
|
77
|
+
},
|
|
78
|
+
conditions: {
|
|
79
|
+
stackable: false,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
const result = DiscountEngine.calculateDiscounts(cart, discounts);
|
|
84
|
+
expect(result.totalDiscount).toBe(0);
|
|
85
|
+
expect(result.appliedDiscounts).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
it("should not apply discount when not enough items", () => {
|
|
88
|
+
const cart = [
|
|
89
|
+
{
|
|
90
|
+
amount: 2, // Only 2 items, need 3
|
|
91
|
+
product: {
|
|
92
|
+
id: "product1",
|
|
93
|
+
price: 10,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
const discounts = [
|
|
98
|
+
{
|
|
99
|
+
type: "Discount",
|
|
100
|
+
storeId: "store1",
|
|
101
|
+
companyId: "company1",
|
|
102
|
+
id: "bundle1",
|
|
103
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
104
|
+
active: true,
|
|
105
|
+
startDate: Date.now() - 1000,
|
|
106
|
+
endDate: Date.now() + 1000,
|
|
107
|
+
variant: {
|
|
108
|
+
variantType: "bundle",
|
|
109
|
+
productsId: ["product1"],
|
|
110
|
+
requiredQuantity: 3,
|
|
111
|
+
bundlePrice: 25,
|
|
112
|
+
},
|
|
113
|
+
conditions: {
|
|
114
|
+
stackable: false,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
const result = DiscountEngine.calculateDiscounts(cart, discounts);
|
|
119
|
+
expect(result.totalDiscount).toBe(0);
|
|
120
|
+
expect(result.appliedDiscounts).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
it("should handle Buy 2 Get 1 Free scenario", () => {
|
|
123
|
+
const cart = [
|
|
124
|
+
{
|
|
125
|
+
amount: 3, // Buy 3 items
|
|
126
|
+
product: {
|
|
127
|
+
id: "product1",
|
|
128
|
+
price: 10, // $10 each
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
const bogoDiscount = {
|
|
133
|
+
type: "Discount",
|
|
134
|
+
storeId: "store1",
|
|
135
|
+
companyId: "company1",
|
|
136
|
+
id: "bogo",
|
|
137
|
+
name: [{ lang: "he", value: "Buy 2 Get 1 Free" }],
|
|
138
|
+
active: true,
|
|
139
|
+
startDate: Date.now() - 1000,
|
|
140
|
+
endDate: Date.now() + 1000,
|
|
141
|
+
variant: {
|
|
142
|
+
variantType: "bundle",
|
|
143
|
+
productsId: ["product1"],
|
|
144
|
+
requiredQuantity: 3, // Buy 2 + get 1 free = 3 items
|
|
145
|
+
bundlePrice: 20, // Price of 2 items (10 × 2)
|
|
146
|
+
},
|
|
147
|
+
conditions: {
|
|
148
|
+
stackable: false,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const result = DiscountEngine.calculateDiscounts(cart, [bogoDiscount]);
|
|
152
|
+
// Original: 3 × $10 = $30
|
|
153
|
+
// Bundle: 3 items for $20
|
|
154
|
+
// Discount: $10
|
|
155
|
+
expect(result.totalDiscount).toBe(10);
|
|
156
|
+
expect(result.appliedDiscounts).toHaveLength(1);
|
|
157
|
+
});
|
|
158
|
+
it("should handle empty cart", () => {
|
|
159
|
+
const cart = [];
|
|
160
|
+
const discounts = [
|
|
161
|
+
{
|
|
162
|
+
type: "Discount",
|
|
163
|
+
storeId: "store1",
|
|
164
|
+
companyId: "company1",
|
|
165
|
+
id: "bundle1",
|
|
166
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
167
|
+
active: true,
|
|
168
|
+
startDate: Date.now() - 1000,
|
|
169
|
+
endDate: Date.now() + 1000,
|
|
170
|
+
variant: {
|
|
171
|
+
variantType: "bundle",
|
|
172
|
+
productsId: ["product1"],
|
|
173
|
+
requiredQuantity: 3,
|
|
174
|
+
bundlePrice: 25,
|
|
175
|
+
},
|
|
176
|
+
conditions: {
|
|
177
|
+
stackable: false,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
const result = DiscountEngine.calculateDiscounts(cart, discounts);
|
|
182
|
+
expect(result.totalDiscount).toBe(0);
|
|
183
|
+
expect(result.appliedDiscounts).toHaveLength(0);
|
|
184
|
+
expect(result.items).toHaveLength(0);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../../../../lib/entities/Discount/__tests__/utils.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatCurrency, formatCurrencyString, ensureNonNegative, calculatePercentageDiscount } from "../utils";
|
|
3
|
+
describe("Discount Utils", () => {
|
|
4
|
+
describe("formatCurrency", () => {
|
|
5
|
+
it("should format numbers to 2 decimal places", () => {
|
|
6
|
+
expect(formatCurrency(10)).toBe(10.00);
|
|
7
|
+
expect(formatCurrency(10.123)).toBe(10.12);
|
|
8
|
+
expect(formatCurrency(10.125)).toBe(10.13); // Rounds up
|
|
9
|
+
expect(formatCurrency(10.124)).toBe(10.12); // Rounds down
|
|
10
|
+
expect(formatCurrency(0)).toBe(0.00);
|
|
11
|
+
});
|
|
12
|
+
it("should return a number type", () => {
|
|
13
|
+
const result = formatCurrency(10.123);
|
|
14
|
+
expect(typeof result).toBe("number");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe("formatCurrencyString", () => {
|
|
18
|
+
it("should format numbers to 2 decimal places as string", () => {
|
|
19
|
+
expect(formatCurrencyString(10)).toBe("10.00");
|
|
20
|
+
expect(formatCurrencyString(10.123)).toBe("10.12");
|
|
21
|
+
expect(formatCurrencyString(10.125)).toBe("10.13");
|
|
22
|
+
expect(formatCurrencyString(0)).toBe("0.00");
|
|
23
|
+
});
|
|
24
|
+
it("should return a string type", () => {
|
|
25
|
+
const result = formatCurrencyString(10.123);
|
|
26
|
+
expect(typeof result).toBe("string");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe("ensureNonNegative", () => {
|
|
30
|
+
it("should return 0 for negative values", () => {
|
|
31
|
+
expect(ensureNonNegative(-5)).toBe(0.00);
|
|
32
|
+
expect(ensureNonNegative(-0.01)).toBe(0.00);
|
|
33
|
+
});
|
|
34
|
+
it("should return formatted value for positive values", () => {
|
|
35
|
+
expect(ensureNonNegative(10.123)).toBe(10.12);
|
|
36
|
+
expect(ensureNonNegative(0)).toBe(0.00);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("calculatePercentageDiscount", () => {
|
|
40
|
+
it("should calculate percentage discount correctly", () => {
|
|
41
|
+
expect(calculatePercentageDiscount(100, 80)).toBe(20.00); // 20% discount
|
|
42
|
+
expect(calculatePercentageDiscount(50, 25)).toBe(50.00); // 50% discount
|
|
43
|
+
expect(calculatePercentageDiscount(100, 100)).toBe(0.00); // No discount
|
|
44
|
+
});
|
|
45
|
+
it("should handle edge cases", () => {
|
|
46
|
+
expect(calculatePercentageDiscount(0, 0)).toBe(0.00);
|
|
47
|
+
expect(calculatePercentageDiscount(100, 0)).toBe(100.00); // 100% discount
|
|
48
|
+
});
|
|
49
|
+
it("should return formatted number", () => {
|
|
50
|
+
const result = calculatePercentageDiscount(100, 90.5);
|
|
51
|
+
expect(typeof result).toBe("number");
|
|
52
|
+
expect(result).toBe(9.50); // 9.5% discount
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TDiscount, AppliedDiscount } from "./types";
|
|
2
|
+
export declare class DiscountEngine {
|
|
3
|
+
static calculateDiscounts(cart: Array<{
|
|
4
|
+
amount: number;
|
|
5
|
+
product: {
|
|
6
|
+
id: string;
|
|
7
|
+
price: number;
|
|
8
|
+
};
|
|
9
|
+
}>, discounts: TDiscount[], user?: Record<string, unknown>): {
|
|
10
|
+
items: Array<{
|
|
11
|
+
amount: number;
|
|
12
|
+
product: {
|
|
13
|
+
id: string;
|
|
14
|
+
price: number;
|
|
15
|
+
};
|
|
16
|
+
originalPrice: number;
|
|
17
|
+
finalPrice: number;
|
|
18
|
+
finalDiscount: number;
|
|
19
|
+
appliedDiscounts: string[];
|
|
20
|
+
}>;
|
|
21
|
+
totalDiscount: number;
|
|
22
|
+
appliedDiscounts: AppliedDiscount[];
|
|
23
|
+
};
|
|
24
|
+
private static filterActiveDiscounts;
|
|
25
|
+
private static calculateFinalPrices;
|
|
26
|
+
static isDiscountActive(discount: TDiscount): boolean;
|
|
27
|
+
static getActiveDiscounts(discounts: TDiscount[]): TDiscount[];
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../../lib/entities/Discount/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAmB,MAAM,SAAS,CAAC;AAYtE,qBAAa,cAAc;IAC1B,MAAM,CAAC,kBAAkB,CACxB,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,EACF,SAAS,EAAE,SAAS,EAAE,EACtB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B;QACF,KAAK,EAAE,KAAK,CAAC;YACZ,MAAM,EAAE,MAAM,CAAC;YACf,OAAO,EAAE;gBACR,EAAE,EAAE,MAAM,CAAC;gBACX,KAAK,EAAE,MAAM,CAAC;aACd,CAAC;YACF,aAAa,EAAE,MAAM,CAAC;YACtB,UAAU,EAAE,MAAM,CAAC;YACnB,aAAa,EAAE,MAAM,CAAC;YACtB,gBAAgB,EAAE,MAAM,EAAE,CAAC;SAC3B,CAAC,CAAC;QACH,aAAa,EAAE,MAAM,CAAC;QACtB,gBAAgB,EAAE,eAAe,EAAE,CAAC;KACpC;IAuDD,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAOpC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IA8BnC,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,SAAS,GAAG,OAAO;IAKrD,MAAM,CAAC,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE;CAG9D"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { DiscountStrategyFactory } from "./factory";
|
|
2
|
+
// discount engine - calculate discounts and return final prices
|
|
3
|
+
// should register all discount types
|
|
4
|
+
// should receive all store discounts
|
|
5
|
+
// should check active discount that should be applied on cart
|
|
6
|
+
// should find best discount per product
|
|
7
|
+
// should apply discount to if stackable or best one
|
|
8
|
+
// any applyd discount should mark products that effected and price before and after and average price per product in cart
|
|
9
|
+
// should return items list with all calculations and final prices and discounts
|
|
10
|
+
export class DiscountEngine {
|
|
11
|
+
static calculateDiscounts(cart, discounts, user) {
|
|
12
|
+
const context = {
|
|
13
|
+
cart,
|
|
14
|
+
user,
|
|
15
|
+
appliedDiscounts: [],
|
|
16
|
+
};
|
|
17
|
+
// Filter active discounts
|
|
18
|
+
const activeDiscounts = this.filterActiveDiscounts(discounts);
|
|
19
|
+
// Apply discounts
|
|
20
|
+
const appliedDiscounts = [];
|
|
21
|
+
for (const discount of activeDiscounts) {
|
|
22
|
+
const strategy = DiscountStrategyFactory.getStrategy(discount);
|
|
23
|
+
if (!strategy)
|
|
24
|
+
continue;
|
|
25
|
+
// Check if discount can be applied
|
|
26
|
+
if (!strategy.canApply(discount, context))
|
|
27
|
+
continue;
|
|
28
|
+
// Check if discount is stackable
|
|
29
|
+
// todo
|
|
30
|
+
if (!discount.conditions?.stackable && appliedDiscounts.length > 0)
|
|
31
|
+
continue;
|
|
32
|
+
// Calculate discount
|
|
33
|
+
const result = strategy.calculate(discount, context);
|
|
34
|
+
if (!result.applicable)
|
|
35
|
+
continue;
|
|
36
|
+
// Apply discount
|
|
37
|
+
appliedDiscounts.push({
|
|
38
|
+
discountId: discount.id,
|
|
39
|
+
discountName: discount.name[0]?.value || "Discount",
|
|
40
|
+
discountAmount: Number(result.discountAmount.toFixed(2)),
|
|
41
|
+
affectedItems: result.affectedItems,
|
|
42
|
+
});
|
|
43
|
+
// Update context for next iteration
|
|
44
|
+
context.appliedDiscounts = appliedDiscounts;
|
|
45
|
+
}
|
|
46
|
+
// Calculate final prices
|
|
47
|
+
const items = this.calculateFinalPrices(cart, appliedDiscounts);
|
|
48
|
+
const totalDiscount = appliedDiscounts.reduce((sum, discount) => sum + discount.discountAmount, 0);
|
|
49
|
+
return {
|
|
50
|
+
items,
|
|
51
|
+
totalDiscount: Number(totalDiscount.toFixed(2)),
|
|
52
|
+
appliedDiscounts,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
static filterActiveDiscounts(discounts) {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
return discounts.filter((discount) => discount.active && discount.startDate <= now && discount.endDate >= now);
|
|
58
|
+
}
|
|
59
|
+
static calculateFinalPrices(cart, appliedDiscounts) {
|
|
60
|
+
return cart.map((item) => {
|
|
61
|
+
const itemDiscounts = appliedDiscounts.filter((discount) => discount.affectedItems.some((affected) => affected.productId === item.product.id));
|
|
62
|
+
const totalItemDiscount = itemDiscounts.reduce((sum, discount) => {
|
|
63
|
+
const affectedItem = discount.affectedItems.find((affected) => affected.productId === item.product.id);
|
|
64
|
+
return sum + (affectedItem?.discountAmount || 0);
|
|
65
|
+
}, 0);
|
|
66
|
+
const discountPerUnit = totalItemDiscount / item.amount;
|
|
67
|
+
const finalPrice = item.product.price - discountPerUnit;
|
|
68
|
+
return {
|
|
69
|
+
amount: item.amount,
|
|
70
|
+
product: item.product,
|
|
71
|
+
originalPrice: Number(item.product.price.toFixed(2)),
|
|
72
|
+
finalPrice: Number(Math.max(0, finalPrice).toFixed(2)),
|
|
73
|
+
finalDiscount: Number(totalItemDiscount.toFixed(2)),
|
|
74
|
+
appliedDiscounts: itemDiscounts.map((d) => d.discountId),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
static isDiscountActive(discount) {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
return discount.active && discount.startDate <= now && discount.endDate >= now;
|
|
81
|
+
}
|
|
82
|
+
static getActiveDiscounts(discounts) {
|
|
83
|
+
return this.filterActiveDiscounts(discounts);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TDiscount } from "./types";
|
|
2
|
+
import { DiscountStrategy } from "./strategy";
|
|
3
|
+
export declare class DiscountStrategyFactory {
|
|
4
|
+
private static strategies;
|
|
5
|
+
static getStrategy(discount: TDiscount): DiscountStrategy | null;
|
|
6
|
+
static registerStrategy(type: string, strategy: DiscountStrategy): void;
|
|
7
|
+
static getRegisteredTypes(): string[];
|
|
8
|
+
static clearStrategies(): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../lib/entities/Discount/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C,qBAAa,uBAAuB;IAClC,OAAO,CAAC,MAAM,CAAC,UAAU,CAEtB;IAEH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,gBAAgB,GAAG,IAAI;IAIhE,MAAM,CAAC,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAIvE,MAAM,CAAC,kBAAkB,IAAI,MAAM,EAAE;IAIrC,MAAM,CAAC,eAAe,IAAI,IAAI;CAG/B"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BundleDiscountStrategy } from "./strategies/BundleStrategy";
|
|
2
|
+
export class DiscountStrategyFactory {
|
|
3
|
+
static getStrategy(discount) {
|
|
4
|
+
return this.strategies.get(discount.variant.variantType) || null;
|
|
5
|
+
}
|
|
6
|
+
static registerStrategy(type, strategy) {
|
|
7
|
+
this.strategies.set(type, strategy);
|
|
8
|
+
}
|
|
9
|
+
static getRegisteredTypes() {
|
|
10
|
+
return Array.from(this.strategies.keys());
|
|
11
|
+
}
|
|
12
|
+
static clearStrategies() {
|
|
13
|
+
this.strategies.clear();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
DiscountStrategyFactory.strategies = new Map([
|
|
17
|
+
["bundle", new BundleDiscountStrategy()],
|
|
18
|
+
]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./strategy";
|
|
3
|
+
export * from "./factory";
|
|
4
|
+
export * from "./engine";
|
|
5
|
+
export * from "./strategies/BundleStrategy";
|
|
6
|
+
export * from "./utils";
|
|
7
|
+
export { DiscountSchema } from "./types";
|
|
8
|
+
export type { TDiscount } from "./types";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../lib/entities/Discount/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,6BAA6B,CAAC;AAC5C,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -4,7 +4,5 @@ export * from "./factory";
|
|
|
4
4
|
export * from "./engine";
|
|
5
5
|
export * from "./strategies/BundleStrategy";
|
|
6
6
|
export * from "./utils";
|
|
7
|
-
|
|
8
7
|
// Re-export the main discount schema for backward compatibility
|
|
9
8
|
export { DiscountSchema } from "./types";
|
|
10
|
-
export type { TDiscount } from "./types";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DiscountStrategy } from "../strategy";
|
|
2
|
+
import { TDiscount, DiscountResult, DiscountContext } from "../types";
|
|
3
|
+
export declare class BundleDiscountStrategy implements DiscountStrategy {
|
|
4
|
+
canApply(discount: TDiscount, context: DiscountContext): boolean;
|
|
5
|
+
calculate(discount: TDiscount, context: DiscountContext): DiscountResult;
|
|
6
|
+
private isDiscountActive;
|
|
7
|
+
private getTotalQuantity;
|
|
8
|
+
private calculateOriginalPrice;
|
|
9
|
+
private calculateDiscountedPrice;
|
|
10
|
+
private distributeDiscount;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=BundleStrategy.d.ts.map
|
|
@@ -0,0 +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;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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export class BundleDiscountStrategy {
|
|
2
|
+
canApply(discount, context) {
|
|
3
|
+
if (discount.variant.variantType !== "bundle")
|
|
4
|
+
return false;
|
|
5
|
+
// Check if discount is active
|
|
6
|
+
if (!this.isDiscountActive(discount))
|
|
7
|
+
return false;
|
|
8
|
+
const { productsId, requiredQuantity } = discount.variant;
|
|
9
|
+
// Check if cart has enough of the required products
|
|
10
|
+
const totalQuantity = this.getTotalQuantity(context.cart, productsId);
|
|
11
|
+
return totalQuantity >= requiredQuantity;
|
|
12
|
+
}
|
|
13
|
+
calculate(discount, context) {
|
|
14
|
+
if (discount.variant.variantType !== "bundle") {
|
|
15
|
+
return { applicable: false, discountAmount: 0, affectedItems: [] };
|
|
16
|
+
}
|
|
17
|
+
const { productsId, requiredQuantity, bundlePrice } = discount.variant;
|
|
18
|
+
// Get applicable items (only the products in the bundle)
|
|
19
|
+
const applicableItems = context.cart.filter((item) => productsId.includes(item.product.id));
|
|
20
|
+
const totalQuantity = this.getTotalQuantity(context.cart, productsId);
|
|
21
|
+
const bundleCount = Math.floor(totalQuantity / requiredQuantity);
|
|
22
|
+
if (bundleCount === 0) {
|
|
23
|
+
return { applicable: false, discountAmount: 0, affectedItems: [] };
|
|
24
|
+
}
|
|
25
|
+
// Calculate original price for bundled items
|
|
26
|
+
const originalPrice = this.calculateOriginalPrice(applicableItems);
|
|
27
|
+
// Calculate discounted price
|
|
28
|
+
const cartTotalQuantity = this.getTotalQuantity(context.cart, productsId);
|
|
29
|
+
const discountedPrice = this.calculateDiscountedPrice(originalPrice, bundlePrice, bundleCount, requiredQuantity, cartTotalQuantity);
|
|
30
|
+
const totalDiscount = originalPrice - discountedPrice;
|
|
31
|
+
// Distribute discount proportionally among items
|
|
32
|
+
const affectedItems = this.distributeDiscount(applicableItems, totalDiscount, originalPrice);
|
|
33
|
+
return {
|
|
34
|
+
applicable: true,
|
|
35
|
+
discountAmount: Number(totalDiscount.toFixed(2)),
|
|
36
|
+
affectedItems,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
isDiscountActive(discount) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
return discount.active && discount.startDate <= now && discount.endDate >= now;
|
|
42
|
+
}
|
|
43
|
+
getTotalQuantity(cart, productIds) {
|
|
44
|
+
return cart
|
|
45
|
+
.filter((item) => productIds.includes(item.product.id))
|
|
46
|
+
.reduce((sum, item) => sum + item.amount, 0);
|
|
47
|
+
}
|
|
48
|
+
calculateOriginalPrice(items) {
|
|
49
|
+
return items.reduce((sum, item) => sum + item.product.price * item.amount, 0);
|
|
50
|
+
}
|
|
51
|
+
calculateDiscountedPrice(originalPrice, bundlePrice, bundleCount, requiredQuantity, totalQuantity) {
|
|
52
|
+
// For bundle discounts, we pay the bundle price for each complete bundle
|
|
53
|
+
// and full price for any remaining items
|
|
54
|
+
const bundledItemsPrice = bundlePrice * bundleCount;
|
|
55
|
+
// For bundle discounts, we pay bundle price for complete bundles
|
|
56
|
+
// and full price for remaining items
|
|
57
|
+
// Since we're dealing with the total original price, we need to calculate
|
|
58
|
+
// how much of the original price represents the bundled items vs remaining items
|
|
59
|
+
const itemsInBundles = bundleCount * requiredQuantity;
|
|
60
|
+
const remainingItems = Math.max(0, totalQuantity - itemsInBundles);
|
|
61
|
+
// Calculate the proportion of original price that represents remaining items
|
|
62
|
+
const remainingItemsPrice = (remainingItems / totalQuantity) * originalPrice;
|
|
63
|
+
return bundledItemsPrice + remainingItemsPrice;
|
|
64
|
+
}
|
|
65
|
+
distributeDiscount(items, totalDiscount, originalPrice) {
|
|
66
|
+
const discountRatio = totalDiscount / originalPrice;
|
|
67
|
+
return items.map((item) => {
|
|
68
|
+
const itemDiscount = item.product.price * item.amount * discountRatio;
|
|
69
|
+
return {
|
|
70
|
+
productId: item.product.id,
|
|
71
|
+
quantity: item.amount,
|
|
72
|
+
originalPrice: Number(item.product.price.toFixed(2)),
|
|
73
|
+
discountedPrice: Number((item.product.price - itemDiscount / item.amount).toFixed(2)),
|
|
74
|
+
discountAmount: Number(itemDiscount.toFixed(2)),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BundleStrategy.test.d.ts","sourceRoot":"","sources":["../../../../../../lib/entities/Discount/strategies/__tests__/BundleStrategy.test.ts"],"names":[],"mappings":""}
|