@jsdev_ninja/core 0.12.6 → 0.12.8
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 +1001 -853
- 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/__tests__/engine.test.d.ts +2 -0
- package/dist/entities/Discount/__tests__/engine.test.d.ts.map +1 -0
- package/dist/entities/Discount/__tests__/engine.test.js +298 -0
- package/dist/entities/Discount/__tests__/factory.test.d.ts +2 -0
- package/dist/entities/Discount/__tests__/factory.test.d.ts.map +1 -0
- package/dist/entities/Discount/__tests__/factory.test.js +75 -0
- package/dist/entities/Discount/__tests__/integration.test.d.ts +2 -0
- package/dist/entities/Discount/__tests__/integration.test.d.ts.map +1 -0
- package/dist/entities/Discount/__tests__/integration.test.js +115 -0
- package/dist/entities/Discount/__tests__/simple.test.d.ts +2 -0
- package/dist/entities/Discount/__tests__/simple.test.d.ts.map +1 -0
- package/dist/entities/Discount/__tests__/simple.test.js +186 -0
- package/dist/entities/Discount/__tests__/utils.test.d.ts +2 -0
- package/dist/entities/Discount/__tests__/utils.test.d.ts.map +1 -0
- package/dist/entities/Discount/__tests__/utils.test.js +55 -0
- package/dist/entities/Discount/engine.d.ts +29 -0
- package/dist/entities/Discount/engine.d.ts.map +1 -0
- package/dist/entities/Discount/engine.js +80 -0
- package/dist/entities/Discount/factory.d.ts +10 -0
- package/dist/entities/Discount/factory.d.ts.map +1 -0
- package/dist/entities/Discount/factory.js +18 -0
- package/dist/entities/Discount/index.d.ts +9 -0
- package/dist/entities/Discount/index.d.ts.map +1 -0
- package/dist/entities/Discount/index.js +8 -0
- package/dist/entities/Discount/strategies/BundleStrategy.d.ts +12 -0
- package/dist/entities/Discount/strategies/BundleStrategy.d.ts.map +1 -0
- package/dist/entities/Discount/strategies/BundleStrategy.js +80 -0
- package/dist/entities/Discount/strategies/__tests__/BundleStrategy.test.d.ts +2 -0
- package/dist/entities/Discount/strategies/__tests__/BundleStrategy.test.d.ts.map +1 -0
- package/dist/entities/Discount/strategies/__tests__/BundleStrategy.test.js +265 -0
- package/dist/entities/Discount/strategy.d.ts +6 -0
- package/dist/entities/Discount/strategy.d.ts.map +1 -0
- package/dist/entities/Discount/strategy.js +1 -0
- package/dist/entities/Discount/types.d.ts +148 -0
- package/dist/entities/Discount/types.d.ts.map +1 -0
- package/dist/entities/Discount/types.js +27 -0
- package/dist/entities/Discount/utils.d.ts +29 -0
- package/dist/entities/Discount/utils.d.ts.map +1 -0
- package/dist/entities/Discount/utils.js +39 -0
- package/dist/tsconfig.app.tsbuildinfo +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +16 -60
- package/lib/entities/Discount/__tests__/engine.test.ts +337 -0
- package/lib/entities/Discount/__tests__/factory.test.ts +91 -0
- package/lib/entities/Discount/__tests__/integration.test.ts +132 -0
- package/lib/entities/Discount/__tests__/simple.test.ts +215 -0
- package/lib/entities/Discount/__tests__/utils.test.ts +69 -0
- package/lib/entities/Discount/engine.ts +129 -0
- package/lib/entities/Discount/factory.ts +25 -0
- package/lib/entities/Discount/index.ts +10 -0
- package/lib/entities/Discount/strategies/BundleStrategy.ts +123 -0
- package/lib/entities/Discount/strategies/__tests__/BundleStrategy.test.ts +295 -0
- package/lib/entities/Discount/strategy.ts +6 -0
- package/lib/entities/Discount/types.ts +68 -0
- package/lib/entities/Discount/utils.ts +42 -0
- package/lib/utils/index.ts +19 -80
- package/package.json +10 -5
- package/vitest.config.ts +10 -0
- package/dist/entities/Discount.d.ts +0 -71
- package/dist/entities/Discount.d.ts.map +0 -1
- package/dist/entities/Discount.js +0 -20
- package/lib/entities/Discount.ts +0 -23
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { BundleDiscountStrategy } from "../BundleStrategy";
|
|
3
|
+
describe("BundleDiscountStrategy", () => {
|
|
4
|
+
let strategy;
|
|
5
|
+
let mockContext;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
strategy = new BundleDiscountStrategy();
|
|
8
|
+
mockContext = {
|
|
9
|
+
cart: [
|
|
10
|
+
{
|
|
11
|
+
amount: 2,
|
|
12
|
+
product: {
|
|
13
|
+
id: "product1",
|
|
14
|
+
price: 10,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
amount: 1,
|
|
19
|
+
product: {
|
|
20
|
+
id: "product2",
|
|
21
|
+
price: 15,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
user: undefined,
|
|
26
|
+
appliedDiscounts: [],
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
describe("canApply", () => {
|
|
30
|
+
it("should return true for valid bundle discount", () => {
|
|
31
|
+
const discount = {
|
|
32
|
+
type: "Discount",
|
|
33
|
+
storeId: "store1",
|
|
34
|
+
companyId: "company1",
|
|
35
|
+
id: "bundle1",
|
|
36
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
37
|
+
active: true,
|
|
38
|
+
startDate: Date.now() - 1000,
|
|
39
|
+
endDate: Date.now() + 1000,
|
|
40
|
+
variant: {
|
|
41
|
+
variantType: "bundle",
|
|
42
|
+
productsId: ["product1", "product2"],
|
|
43
|
+
requiredQuantity: 3,
|
|
44
|
+
bundlePrice: 25,
|
|
45
|
+
},
|
|
46
|
+
conditions: {
|
|
47
|
+
stackable: false,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
expect(strategy.canApply(discount, mockContext)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it("should return true for bundle variant type", () => {
|
|
53
|
+
const discount = {
|
|
54
|
+
...mockDiscount,
|
|
55
|
+
variant: {
|
|
56
|
+
variantType: "bundle",
|
|
57
|
+
productsId: ["product1", "product2"],
|
|
58
|
+
requiredQuantity: 3,
|
|
59
|
+
bundlePrice: 25,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
// This should work since it's a bundle type and we have 3 items total
|
|
63
|
+
expect(strategy.canApply(discount, mockContext)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it("should return false when not enough items", () => {
|
|
66
|
+
const discount = {
|
|
67
|
+
type: "Discount",
|
|
68
|
+
storeId: "store1",
|
|
69
|
+
companyId: "company1",
|
|
70
|
+
id: "bundle1",
|
|
71
|
+
name: [{ lang: "he", value: "Buy 5 for $40" }],
|
|
72
|
+
active: true,
|
|
73
|
+
startDate: Date.now() - 1000,
|
|
74
|
+
endDate: Date.now() + 1000,
|
|
75
|
+
variant: {
|
|
76
|
+
variantType: "bundle",
|
|
77
|
+
productsId: ["product1", "product2"],
|
|
78
|
+
requiredQuantity: 5, // Need 5, but cart only has 3
|
|
79
|
+
bundlePrice: 40,
|
|
80
|
+
},
|
|
81
|
+
conditions: {
|
|
82
|
+
stackable: false,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
expect(strategy.canApply(discount, mockContext)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
it("should return false when discount is inactive", () => {
|
|
88
|
+
const discount = {
|
|
89
|
+
type: "Discount",
|
|
90
|
+
storeId: "store1",
|
|
91
|
+
companyId: "company1",
|
|
92
|
+
id: "bundle1",
|
|
93
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
94
|
+
active: false, // Inactive
|
|
95
|
+
startDate: Date.now() - 1000,
|
|
96
|
+
endDate: Date.now() + 1000,
|
|
97
|
+
variant: {
|
|
98
|
+
variantType: "bundle",
|
|
99
|
+
productsId: ["product1", "product2"],
|
|
100
|
+
requiredQuantity: 3,
|
|
101
|
+
bundlePrice: 25,
|
|
102
|
+
},
|
|
103
|
+
conditions: {
|
|
104
|
+
stackable: false,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
expect(strategy.canApply(discount, mockContext)).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
it("should return false when discount is expired", () => {
|
|
110
|
+
const discount = {
|
|
111
|
+
type: "Discount",
|
|
112
|
+
storeId: "store1",
|
|
113
|
+
companyId: "company1",
|
|
114
|
+
id: "bundle1",
|
|
115
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
116
|
+
active: true,
|
|
117
|
+
startDate: Date.now() - 2000,
|
|
118
|
+
endDate: Date.now() - 1000, // Expired
|
|
119
|
+
variant: {
|
|
120
|
+
variantType: "bundle",
|
|
121
|
+
productsId: ["product1", "product2"],
|
|
122
|
+
requiredQuantity: 3,
|
|
123
|
+
bundlePrice: 25,
|
|
124
|
+
},
|
|
125
|
+
conditions: {
|
|
126
|
+
stackable: false,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
expect(strategy.canApply(discount, mockContext)).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe("calculate", () => {
|
|
133
|
+
it("should calculate bundle discount correctly", () => {
|
|
134
|
+
const discount = {
|
|
135
|
+
type: "Discount",
|
|
136
|
+
storeId: "store1",
|
|
137
|
+
companyId: "company1",
|
|
138
|
+
id: "bundle1",
|
|
139
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
140
|
+
active: true,
|
|
141
|
+
startDate: Date.now() - 1000,
|
|
142
|
+
endDate: Date.now() + 1000,
|
|
143
|
+
variant: {
|
|
144
|
+
variantType: "bundle",
|
|
145
|
+
productsId: ["product1", "product2"],
|
|
146
|
+
requiredQuantity: 3,
|
|
147
|
+
bundlePrice: 25,
|
|
148
|
+
},
|
|
149
|
+
conditions: {
|
|
150
|
+
stackable: false,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const result = strategy.calculate(discount, mockContext);
|
|
154
|
+
expect(result.applicable).toBe(true);
|
|
155
|
+
expect(result.discountAmount).toBe(10); // (2×10 + 1×15) - 25 = 35 - 25 = 10
|
|
156
|
+
expect(result.affectedItems).toHaveLength(2);
|
|
157
|
+
// Check product1 discount
|
|
158
|
+
const product1Item = result.affectedItems.find(item => item.productId === "product1");
|
|
159
|
+
expect(product1Item).toBeDefined();
|
|
160
|
+
expect(product1Item.discountAmount).toBeCloseTo(5.71, 2); // Proportional to 20/35
|
|
161
|
+
// Check product2 discount
|
|
162
|
+
const product2Item = result.affectedItems.find(item => item.productId === "product2");
|
|
163
|
+
expect(product2Item).toBeDefined();
|
|
164
|
+
expect(product2Item.discountAmount).toBeCloseTo(4.29, 2); // Proportional to 15/35
|
|
165
|
+
});
|
|
166
|
+
it("should handle multiple bundles", () => {
|
|
167
|
+
const contextWithMoreItems = {
|
|
168
|
+
cart: [
|
|
169
|
+
{
|
|
170
|
+
amount: 6, // 6 items
|
|
171
|
+
product: {
|
|
172
|
+
id: "product1",
|
|
173
|
+
price: 10,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
user: undefined,
|
|
178
|
+
appliedDiscounts: [],
|
|
179
|
+
};
|
|
180
|
+
const discount = {
|
|
181
|
+
type: "Discount",
|
|
182
|
+
storeId: "store1",
|
|
183
|
+
companyId: "company1",
|
|
184
|
+
id: "bundle1",
|
|
185
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
186
|
+
active: true,
|
|
187
|
+
startDate: Date.now() - 1000,
|
|
188
|
+
endDate: Date.now() + 1000,
|
|
189
|
+
variant: {
|
|
190
|
+
variantType: "bundle",
|
|
191
|
+
productsId: ["product1"],
|
|
192
|
+
requiredQuantity: 3,
|
|
193
|
+
bundlePrice: 25,
|
|
194
|
+
},
|
|
195
|
+
conditions: {
|
|
196
|
+
stackable: false,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
const result = strategy.calculate(discount, contextWithMoreItems);
|
|
200
|
+
expect(result.applicable).toBe(true);
|
|
201
|
+
expect(result.discountAmount).toBe(10); // (6×10) - (2×25) = 60 - 50 = 10
|
|
202
|
+
expect(result.affectedItems).toHaveLength(1);
|
|
203
|
+
expect(result.affectedItems[0].discountAmount).toBe(10);
|
|
204
|
+
});
|
|
205
|
+
it("should return not applicable for invalid bundle", () => {
|
|
206
|
+
const discount = {
|
|
207
|
+
...mockDiscount,
|
|
208
|
+
variant: {
|
|
209
|
+
variantType: "bundle",
|
|
210
|
+
productsId: ["product1"],
|
|
211
|
+
requiredQuantity: 10, // Too many required
|
|
212
|
+
bundlePrice: 25,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
const result = strategy.calculate(discount, mockContext);
|
|
216
|
+
expect(result.applicable).toBe(false);
|
|
217
|
+
expect(result.discountAmount).toBe(0);
|
|
218
|
+
expect(result.affectedItems).toHaveLength(0);
|
|
219
|
+
});
|
|
220
|
+
it("should return not applicable when not enough items", () => {
|
|
221
|
+
const discount = {
|
|
222
|
+
type: "Discount",
|
|
223
|
+
storeId: "store1",
|
|
224
|
+
companyId: "company1",
|
|
225
|
+
id: "bundle1",
|
|
226
|
+
name: [{ lang: "he", value: "Buy 5 for $40" }],
|
|
227
|
+
active: true,
|
|
228
|
+
startDate: Date.now() - 1000,
|
|
229
|
+
endDate: Date.now() + 1000,
|
|
230
|
+
variant: {
|
|
231
|
+
variantType: "bundle",
|
|
232
|
+
productsId: ["product1", "product2"],
|
|
233
|
+
requiredQuantity: 5,
|
|
234
|
+
bundlePrice: 40,
|
|
235
|
+
},
|
|
236
|
+
conditions: {
|
|
237
|
+
stackable: false,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
const result = strategy.calculate(discount, mockContext);
|
|
241
|
+
expect(result.applicable).toBe(false);
|
|
242
|
+
expect(result.discountAmount).toBe(0);
|
|
243
|
+
expect(result.affectedItems).toHaveLength(0);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
const mockDiscount = {
|
|
248
|
+
type: "Discount",
|
|
249
|
+
storeId: "store1",
|
|
250
|
+
companyId: "company1",
|
|
251
|
+
id: "bundle1",
|
|
252
|
+
name: [{ lang: "he", value: "Buy 3 for $25" }],
|
|
253
|
+
active: true,
|
|
254
|
+
startDate: Date.now() - 1000,
|
|
255
|
+
endDate: Date.now() + 1000,
|
|
256
|
+
variant: {
|
|
257
|
+
variantType: "bundle",
|
|
258
|
+
productsId: ["product1", "product2"],
|
|
259
|
+
requiredQuantity: 3,
|
|
260
|
+
bundlePrice: 25,
|
|
261
|
+
},
|
|
262
|
+
conditions: {
|
|
263
|
+
stackable: false,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { TDiscount, DiscountResult, DiscountContext } from "./types";
|
|
2
|
+
export interface DiscountStrategy {
|
|
3
|
+
canApply(discount: TDiscount, context: DiscountContext): boolean;
|
|
4
|
+
calculate(discount: TDiscount, context: DiscountContext): DiscountResult;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../../lib/entities/Discount/strategy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC;IACjE,SAAS,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CAAC;CAC1E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const DiscountConditionsSchema: z.ZodOptional<z.ZodObject<{
|
|
3
|
+
minSpend: z.ZodOptional<z.ZodNumber>;
|
|
4
|
+
stackable: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
stackable: boolean;
|
|
7
|
+
minSpend?: number | undefined;
|
|
8
|
+
}, {
|
|
9
|
+
minSpend?: number | undefined;
|
|
10
|
+
stackable?: boolean | undefined;
|
|
11
|
+
}>>;
|
|
12
|
+
export declare const DiscountVariantSchema: z.ZodDiscriminatedUnion<"variantType", [z.ZodObject<{
|
|
13
|
+
variantType: z.ZodLiteral<"bundle">;
|
|
14
|
+
productsId: z.ZodArray<z.ZodString, "many">;
|
|
15
|
+
requiredQuantity: z.ZodNumber;
|
|
16
|
+
bundlePrice: z.ZodNumber;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
variantType: "bundle";
|
|
19
|
+
productsId: string[];
|
|
20
|
+
requiredQuantity: number;
|
|
21
|
+
bundlePrice: number;
|
|
22
|
+
}, {
|
|
23
|
+
variantType: "bundle";
|
|
24
|
+
productsId: string[];
|
|
25
|
+
requiredQuantity: number;
|
|
26
|
+
bundlePrice: number;
|
|
27
|
+
}>]>;
|
|
28
|
+
export declare const DiscountSchema: z.ZodObject<{
|
|
29
|
+
type: z.ZodLiteral<"Discount">;
|
|
30
|
+
storeId: z.ZodString;
|
|
31
|
+
companyId: z.ZodString;
|
|
32
|
+
id: z.ZodString;
|
|
33
|
+
name: z.ZodArray<z.ZodObject<{
|
|
34
|
+
lang: z.ZodEnum<["he"]>;
|
|
35
|
+
value: z.ZodString;
|
|
36
|
+
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
value: string;
|
|
38
|
+
lang: "he";
|
|
39
|
+
}, {
|
|
40
|
+
value: string;
|
|
41
|
+
lang: "he";
|
|
42
|
+
}>, "many">;
|
|
43
|
+
active: z.ZodBoolean;
|
|
44
|
+
startDate: z.ZodNumber;
|
|
45
|
+
endDate: z.ZodNumber;
|
|
46
|
+
variant: z.ZodDiscriminatedUnion<"variantType", [z.ZodObject<{
|
|
47
|
+
variantType: z.ZodLiteral<"bundle">;
|
|
48
|
+
productsId: z.ZodArray<z.ZodString, "many">;
|
|
49
|
+
requiredQuantity: z.ZodNumber;
|
|
50
|
+
bundlePrice: z.ZodNumber;
|
|
51
|
+
}, "strip", z.ZodTypeAny, {
|
|
52
|
+
variantType: "bundle";
|
|
53
|
+
productsId: string[];
|
|
54
|
+
requiredQuantity: number;
|
|
55
|
+
bundlePrice: number;
|
|
56
|
+
}, {
|
|
57
|
+
variantType: "bundle";
|
|
58
|
+
productsId: string[];
|
|
59
|
+
requiredQuantity: number;
|
|
60
|
+
bundlePrice: number;
|
|
61
|
+
}>]>;
|
|
62
|
+
conditions: z.ZodOptional<z.ZodObject<{
|
|
63
|
+
minSpend: z.ZodOptional<z.ZodNumber>;
|
|
64
|
+
stackable: z.ZodDefault<z.ZodBoolean>;
|
|
65
|
+
}, "strip", z.ZodTypeAny, {
|
|
66
|
+
stackable: boolean;
|
|
67
|
+
minSpend?: number | undefined;
|
|
68
|
+
}, {
|
|
69
|
+
minSpend?: number | undefined;
|
|
70
|
+
stackable?: boolean | undefined;
|
|
71
|
+
}>>;
|
|
72
|
+
}, "strip", z.ZodTypeAny, {
|
|
73
|
+
type: "Discount";
|
|
74
|
+
id: string;
|
|
75
|
+
companyId: string;
|
|
76
|
+
storeId: string;
|
|
77
|
+
name: {
|
|
78
|
+
value: string;
|
|
79
|
+
lang: "he";
|
|
80
|
+
}[];
|
|
81
|
+
active: boolean;
|
|
82
|
+
startDate: number;
|
|
83
|
+
endDate: number;
|
|
84
|
+
variant: {
|
|
85
|
+
variantType: "bundle";
|
|
86
|
+
productsId: string[];
|
|
87
|
+
requiredQuantity: number;
|
|
88
|
+
bundlePrice: number;
|
|
89
|
+
};
|
|
90
|
+
conditions?: {
|
|
91
|
+
stackable: boolean;
|
|
92
|
+
minSpend?: number | undefined;
|
|
93
|
+
} | undefined;
|
|
94
|
+
}, {
|
|
95
|
+
type: "Discount";
|
|
96
|
+
id: string;
|
|
97
|
+
companyId: string;
|
|
98
|
+
storeId: string;
|
|
99
|
+
name: {
|
|
100
|
+
value: string;
|
|
101
|
+
lang: "he";
|
|
102
|
+
}[];
|
|
103
|
+
active: boolean;
|
|
104
|
+
startDate: number;
|
|
105
|
+
endDate: number;
|
|
106
|
+
variant: {
|
|
107
|
+
variantType: "bundle";
|
|
108
|
+
productsId: string[];
|
|
109
|
+
requiredQuantity: number;
|
|
110
|
+
bundlePrice: number;
|
|
111
|
+
};
|
|
112
|
+
conditions?: {
|
|
113
|
+
minSpend?: number | undefined;
|
|
114
|
+
stackable?: boolean | undefined;
|
|
115
|
+
} | undefined;
|
|
116
|
+
}>;
|
|
117
|
+
export type TDiscount = z.infer<typeof DiscountSchema>;
|
|
118
|
+
export type DiscountVariant = z.infer<typeof DiscountVariantSchema>;
|
|
119
|
+
export type DiscountConditions = z.infer<typeof DiscountConditionsSchema>;
|
|
120
|
+
export interface DiscountResult {
|
|
121
|
+
applicable: boolean;
|
|
122
|
+
discountAmount: number;
|
|
123
|
+
affectedItems: Array<{
|
|
124
|
+
productId: string;
|
|
125
|
+
quantity: number;
|
|
126
|
+
originalPrice: number;
|
|
127
|
+
discountedPrice: number;
|
|
128
|
+
discountAmount: number;
|
|
129
|
+
}>;
|
|
130
|
+
}
|
|
131
|
+
export interface AppliedDiscount {
|
|
132
|
+
discountId: string;
|
|
133
|
+
discountName: string;
|
|
134
|
+
discountAmount: number;
|
|
135
|
+
affectedItems: DiscountResult["affectedItems"];
|
|
136
|
+
}
|
|
137
|
+
export interface DiscountContext {
|
|
138
|
+
cart: Array<{
|
|
139
|
+
amount: number;
|
|
140
|
+
product: {
|
|
141
|
+
id: string;
|
|
142
|
+
price: number;
|
|
143
|
+
};
|
|
144
|
+
}>;
|
|
145
|
+
user?: Record<string, unknown>;
|
|
146
|
+
appliedDiscounts: AppliedDiscount[];
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +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;;;;;;;;;GAGxB,CAAC;AAGd,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;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,KAAK,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,KAAK,CAAC;QACV,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE;YACP,EAAE,EAAE,MAAM,CAAC;YACX,KAAK,EAAE,MAAM,CAAC;SACf,CAAC;KACH,CAAC,CAAC;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,gBAAgB,EAAE,eAAe,EAAE,CAAC;CACrC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Simple discount conditions
|
|
3
|
+
export const DiscountConditionsSchema = z.object({
|
|
4
|
+
minSpend: z.number().positive().optional(),
|
|
5
|
+
stackable: z.boolean().default(false),
|
|
6
|
+
}).optional();
|
|
7
|
+
// Bundle discount - buy X items for Y price
|
|
8
|
+
export const DiscountVariantSchema = z.discriminatedUnion("variantType", [
|
|
9
|
+
z.object({
|
|
10
|
+
variantType: z.literal("bundle"),
|
|
11
|
+
productsId: z.array(z.string()).min(1), // Which products are included
|
|
12
|
+
requiredQuantity: z.number().positive(), // How many items needed (e.g., 3)
|
|
13
|
+
bundlePrice: z.number().positive(), // Total price for the bundle (e.g., $25)
|
|
14
|
+
}),
|
|
15
|
+
]);
|
|
16
|
+
export const DiscountSchema = z.object({
|
|
17
|
+
type: z.literal("Discount"),
|
|
18
|
+
storeId: z.string().min(1),
|
|
19
|
+
companyId: z.string().min(1),
|
|
20
|
+
id: z.string().min(1),
|
|
21
|
+
name: z.array(z.object({ lang: z.enum(["he"]), value: z.string() })),
|
|
22
|
+
active: z.boolean(),
|
|
23
|
+
startDate: z.number(),
|
|
24
|
+
endDate: z.number(),
|
|
25
|
+
variant: DiscountVariantSchema,
|
|
26
|
+
conditions: DiscountConditionsSchema,
|
|
27
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for discount calculations
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Formats a number to 2 decimal places and returns as a number
|
|
6
|
+
* @param value - The number to format
|
|
7
|
+
* @returns The formatted number with 2 decimal places
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatCurrency(value: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Formats a number to 2 decimal places and returns as a string
|
|
12
|
+
* @param value - The number to format
|
|
13
|
+
* @returns The formatted string with 2 decimal places
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatCurrencyString(value: number): string;
|
|
16
|
+
/**
|
|
17
|
+
* Ensures a monetary value is never negative
|
|
18
|
+
* @param value - The value to check
|
|
19
|
+
* @returns The value, or 0 if negative
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureNonNegative(value: number): number;
|
|
22
|
+
/**
|
|
23
|
+
* Calculates the percentage discount
|
|
24
|
+
* @param originalPrice - The original price
|
|
25
|
+
* @param discountedPrice - The discounted price
|
|
26
|
+
* @returns The percentage discount (0-100)
|
|
27
|
+
*/
|
|
28
|
+
export declare function calculatePercentageDiscount(originalPrice: number, discountedPrice: number): number;
|
|
29
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../lib/entities/Discount/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,aAAa,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAIlG"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for discount calculations
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Formats a number to 2 decimal places and returns as a number
|
|
6
|
+
* @param value - The number to format
|
|
7
|
+
* @returns The formatted number with 2 decimal places
|
|
8
|
+
*/
|
|
9
|
+
export function formatCurrency(value) {
|
|
10
|
+
return Number(value.toFixed(2));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Formats a number to 2 decimal places and returns as a string
|
|
14
|
+
* @param value - The number to format
|
|
15
|
+
* @returns The formatted string with 2 decimal places
|
|
16
|
+
*/
|
|
17
|
+
export function formatCurrencyString(value) {
|
|
18
|
+
return value.toFixed(2);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Ensures a monetary value is never negative
|
|
22
|
+
* @param value - The value to check
|
|
23
|
+
* @returns The value, or 0 if negative
|
|
24
|
+
*/
|
|
25
|
+
export function ensureNonNegative(value) {
|
|
26
|
+
return Math.max(0, formatCurrency(value));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Calculates the percentage discount
|
|
30
|
+
* @param originalPrice - The original price
|
|
31
|
+
* @param discountedPrice - The discounted price
|
|
32
|
+
* @returns The percentage discount (0-100)
|
|
33
|
+
*/
|
|
34
|
+
export function calculatePercentageDiscount(originalPrice, discountedPrice) {
|
|
35
|
+
if (originalPrice <= 0)
|
|
36
|
+
return 0;
|
|
37
|
+
const discount = originalPrice - discountedPrice;
|
|
38
|
+
return formatCurrency((discount / originalPrice) * 100);
|
|
39
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../lib/index.tsx","../lib/entities/address.ts","../lib/entities/atoms.ts","../lib/entities/cart.ts","../lib/entities/category.ts","../lib/entities/company.ts","../lib/entities/
|
|
1
|
+
{"root":["../lib/index.tsx","../lib/entities/address.ts","../lib/entities/atoms.ts","../lib/entities/cart.ts","../lib/entities/category.ts","../lib/entities/company.ts","../lib/entities/favoriteproduct.ts","../lib/entities/locale.ts","../lib/entities/order.ts","../lib/entities/product.ts","../lib/entities/profile.ts","../lib/entities/store.ts","../lib/entities/index.ts","../lib/entities/discount/engine.ts","../lib/entities/discount/factory.ts","../lib/entities/discount/index.ts","../lib/entities/discount/strategy.ts","../lib/entities/discount/types.ts","../lib/entities/discount/utils.ts","../lib/entities/discount/__tests__/engine.test.ts","../lib/entities/discount/__tests__/factory.test.ts","../lib/entities/discount/__tests__/integration.test.ts","../lib/entities/discount/__tests__/simple.test.ts","../lib/entities/discount/__tests__/utils.test.ts","../lib/entities/discount/strategies/bundlestrategy.ts","../lib/entities/discount/strategies/__tests__/bundlestrategy.test.ts","../lib/firebase-api/app.ts","../lib/firebase-api/index.ts","../lib/utils/index.ts"],"version":"5.7.3"}
|
|
@@ -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;
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4EA"}
|
package/dist/utils/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DiscountEngine } from "../entities/Discount/engine";
|
|
1
2
|
const CONFIG = {
|
|
2
3
|
VAT: 18,
|
|
3
4
|
};
|
|
@@ -21,75 +22,30 @@ function getPriceAfterDiscount(product) {
|
|
|
21
22
|
}
|
|
22
23
|
return product.price;
|
|
23
24
|
}
|
|
24
|
-
// mark product that used in discount
|
|
25
|
-
// get final price for product and discount id
|
|
26
|
-
// filter better discount
|
|
27
25
|
// main
|
|
28
26
|
export function getCartCost({ cart, discounts, store, }) {
|
|
29
27
|
const { isVatIncludedInPrice } = store;
|
|
30
|
-
|
|
28
|
+
// Convert cart items to the format expected by the discount engine
|
|
29
|
+
const cartForEngine = cart.map(item => ({
|
|
30
|
+
amount: item.amount,
|
|
31
|
+
product: {
|
|
32
|
+
id: item.product.id,
|
|
33
|
+
price: item.product.price,
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
// Apply discounts using the new discount engine
|
|
37
|
+
const discountResult = DiscountEngine.calculateDiscounts(cartForEngine, discounts);
|
|
38
|
+
// Map the results back to the original format with additional product info
|
|
39
|
+
const result = cart.map((item, index) => {
|
|
40
|
+
const engineItem = discountResult.items[index];
|
|
31
41
|
return {
|
|
32
42
|
amount: item.amount,
|
|
33
43
|
product: { ...item.product },
|
|
34
44
|
originalPrice: item.product.price,
|
|
35
|
-
finalPrice: getPriceAfterDiscount(item.product),
|
|
36
|
-
finalDiscount: calculateDiscount(item.product),
|
|
45
|
+
finalPrice: engineItem ? engineItem.finalPrice : getPriceAfterDiscount(item.product),
|
|
46
|
+
finalDiscount: engineItem ? engineItem.finalDiscount : calculateDiscount(item.product),
|
|
37
47
|
};
|
|
38
48
|
});
|
|
39
|
-
const activeDiscounts = discounts.filter((discount) => {
|
|
40
|
-
if (discount.variant.variantType === "bundle") {
|
|
41
|
-
const productsTotal = cart?.reduce((total, item) => {
|
|
42
|
-
if (discount.variant.productsId.includes(item.product.id)) {
|
|
43
|
-
total += item.amount;
|
|
44
|
-
return total;
|
|
45
|
-
}
|
|
46
|
-
return total;
|
|
47
|
-
}, 0) ?? 0;
|
|
48
|
-
if (productsTotal >= discount.variant.requiredQuantity) {
|
|
49
|
-
// const times = Math.floor(productsTotal / discount.variant.requiredQuantity);
|
|
50
|
-
// console.log("yes", times, discount.variant.discountPrice);
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
});
|
|
56
|
-
console.log("activeDiscounts", activeDiscounts);
|
|
57
|
-
activeDiscounts.forEach((discount) => {
|
|
58
|
-
if (discount.variant.variantType === "bundle") {
|
|
59
|
-
// get all products in cart
|
|
60
|
-
const products = cart.filter((item) => discount.variant.productsId.includes(item.product.id));
|
|
61
|
-
const productsTotal = products?.reduce((total, item) => {
|
|
62
|
-
if (discount.variant.productsId.includes(item.product.id)) {
|
|
63
|
-
total += item.amount;
|
|
64
|
-
return total;
|
|
65
|
-
}
|
|
66
|
-
return total;
|
|
67
|
-
}, 0) ?? 0;
|
|
68
|
-
const times = Math.floor(productsTotal / discount.variant.requiredQuantity);
|
|
69
|
-
const price = getPriceAfterDiscount(products[0]?.product);
|
|
70
|
-
const _discount = calculateDiscount(products[0]?.product);
|
|
71
|
-
console.log("price", price, _discount);
|
|
72
|
-
const discountPrice = Number((discount.variant.discountPrice / discount.variant.requiredQuantity).toFixed(2)) * 1;
|
|
73
|
-
console.log("discountPrice", discountPrice);
|
|
74
|
-
const totalDiscount = (price * discount.variant.requiredQuantity - discount.variant.discountPrice) * times;
|
|
75
|
-
const originalPrice = productsTotal * price;
|
|
76
|
-
const discountPriceFinal = originalPrice - totalDiscount;
|
|
77
|
-
const averagePrice = Number((discountPriceFinal / productsTotal).toFixed(2));
|
|
78
|
-
result = result.map((item) => {
|
|
79
|
-
if (discount.variant.productsId.includes(item.product.id)) {
|
|
80
|
-
return {
|
|
81
|
-
...item,
|
|
82
|
-
finalPrice: averagePrice,
|
|
83
|
-
originalPrice: item.product.price,
|
|
84
|
-
discountPrice: price,
|
|
85
|
-
finalDiscount: item.finalDiscount + (item.product.price - averagePrice),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
return item;
|
|
89
|
-
});
|
|
90
|
-
// find average price
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
49
|
const cartDetails = result.reduce((acc, item) => {
|
|
94
50
|
const { product, amount, finalPrice, finalDiscount } = item;
|
|
95
51
|
let productVatValue = 0;
|