@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.
Files changed (68) hide show
  1. package/dist/core.cjs.js +1 -1
  2. package/dist/core.cjs.js.map +1 -1
  3. package/dist/core.es.js +1001 -853
  4. package/dist/core.es.js.map +1 -1
  5. package/dist/core.umd.js +1 -1
  6. package/dist/core.umd.js.map +1 -1
  7. package/dist/entities/Discount/__tests__/engine.test.d.ts +2 -0
  8. package/dist/entities/Discount/__tests__/engine.test.d.ts.map +1 -0
  9. package/dist/entities/Discount/__tests__/engine.test.js +298 -0
  10. package/dist/entities/Discount/__tests__/factory.test.d.ts +2 -0
  11. package/dist/entities/Discount/__tests__/factory.test.d.ts.map +1 -0
  12. package/dist/entities/Discount/__tests__/factory.test.js +75 -0
  13. package/dist/entities/Discount/__tests__/integration.test.d.ts +2 -0
  14. package/dist/entities/Discount/__tests__/integration.test.d.ts.map +1 -0
  15. package/dist/entities/Discount/__tests__/integration.test.js +115 -0
  16. package/dist/entities/Discount/__tests__/simple.test.d.ts +2 -0
  17. package/dist/entities/Discount/__tests__/simple.test.d.ts.map +1 -0
  18. package/dist/entities/Discount/__tests__/simple.test.js +186 -0
  19. package/dist/entities/Discount/__tests__/utils.test.d.ts +2 -0
  20. package/dist/entities/Discount/__tests__/utils.test.d.ts.map +1 -0
  21. package/dist/entities/Discount/__tests__/utils.test.js +55 -0
  22. package/dist/entities/Discount/engine.d.ts +29 -0
  23. package/dist/entities/Discount/engine.d.ts.map +1 -0
  24. package/dist/entities/Discount/engine.js +80 -0
  25. package/dist/entities/Discount/factory.d.ts +10 -0
  26. package/dist/entities/Discount/factory.d.ts.map +1 -0
  27. package/dist/entities/Discount/factory.js +18 -0
  28. package/dist/entities/Discount/index.d.ts +9 -0
  29. package/dist/entities/Discount/index.d.ts.map +1 -0
  30. package/dist/entities/Discount/index.js +8 -0
  31. package/dist/entities/Discount/strategies/BundleStrategy.d.ts +12 -0
  32. package/dist/entities/Discount/strategies/BundleStrategy.d.ts.map +1 -0
  33. package/dist/entities/Discount/strategies/BundleStrategy.js +80 -0
  34. package/dist/entities/Discount/strategies/__tests__/BundleStrategy.test.d.ts +2 -0
  35. package/dist/entities/Discount/strategies/__tests__/BundleStrategy.test.d.ts.map +1 -0
  36. package/dist/entities/Discount/strategies/__tests__/BundleStrategy.test.js +265 -0
  37. package/dist/entities/Discount/strategy.d.ts +6 -0
  38. package/dist/entities/Discount/strategy.d.ts.map +1 -0
  39. package/dist/entities/Discount/strategy.js +1 -0
  40. package/dist/entities/Discount/types.d.ts +148 -0
  41. package/dist/entities/Discount/types.d.ts.map +1 -0
  42. package/dist/entities/Discount/types.js +27 -0
  43. package/dist/entities/Discount/utils.d.ts +29 -0
  44. package/dist/entities/Discount/utils.d.ts.map +1 -0
  45. package/dist/entities/Discount/utils.js +39 -0
  46. package/dist/tsconfig.app.tsbuildinfo +1 -1
  47. package/dist/utils/index.d.ts.map +1 -1
  48. package/dist/utils/index.js +16 -60
  49. package/lib/entities/Discount/__tests__/engine.test.ts +337 -0
  50. package/lib/entities/Discount/__tests__/factory.test.ts +91 -0
  51. package/lib/entities/Discount/__tests__/integration.test.ts +132 -0
  52. package/lib/entities/Discount/__tests__/simple.test.ts +215 -0
  53. package/lib/entities/Discount/__tests__/utils.test.ts +69 -0
  54. package/lib/entities/Discount/engine.ts +129 -0
  55. package/lib/entities/Discount/factory.ts +25 -0
  56. package/lib/entities/Discount/index.ts +10 -0
  57. package/lib/entities/Discount/strategies/BundleStrategy.ts +123 -0
  58. package/lib/entities/Discount/strategies/__tests__/BundleStrategy.test.ts +295 -0
  59. package/lib/entities/Discount/strategy.ts +6 -0
  60. package/lib/entities/Discount/types.ts +68 -0
  61. package/lib/entities/Discount/utils.ts +42 -0
  62. package/lib/utils/index.ts +19 -80
  63. package/package.json +10 -5
  64. package/vitest.config.ts +10 -0
  65. package/dist/entities/Discount.d.ts +0 -71
  66. package/dist/entities/Discount.d.ts.map +0 -1
  67. package/dist/entities/Discount.js +0 -20
  68. package/lib/entities/Discount.ts +0 -23
@@ -0,0 +1,337 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { DiscountEngine } from "../engine";
3
+ import { DiscountStrategyFactory } from "../factory";
4
+ import { BundleDiscountStrategy } from "../strategies/BundleStrategy";
5
+ import { TDiscount } from "../types";
6
+
7
+ describe("DiscountEngine", () => {
8
+ beforeEach(() => {
9
+ DiscountStrategyFactory.clearStrategies();
10
+ DiscountStrategyFactory.registerStrategy("bundle", new BundleDiscountStrategy());
11
+ });
12
+
13
+ afterEach(() => {
14
+ DiscountStrategyFactory.clearStrategies();
15
+ });
16
+
17
+ describe("calculateDiscounts", () => {
18
+ it("should apply bundle discount correctly", () => {
19
+ const cart = [
20
+ {
21
+ amount: 2,
22
+ product: {
23
+ id: "product1",
24
+ price: 10,
25
+ },
26
+ },
27
+ {
28
+ amount: 1,
29
+ product: {
30
+ id: "product2",
31
+ price: 15,
32
+ },
33
+ },
34
+ ];
35
+
36
+ const discounts: TDiscount[] = [
37
+ {
38
+ type: "Discount",
39
+ storeId: "store1",
40
+ companyId: "company1",
41
+ id: "bundle1",
42
+ name: [{ lang: "he", value: "Buy 3 for $25" }],
43
+ active: true,
44
+ startDate: Date.now() - 1000,
45
+ endDate: Date.now() + 1000,
46
+ variant: {
47
+ variantType: "bundle",
48
+ productsId: ["product1", "product2"],
49
+ requiredQuantity: 3,
50
+ bundlePrice: 25,
51
+ },
52
+ conditions: {
53
+ stackable: false,
54
+ },
55
+ },
56
+ ];
57
+
58
+ const result = DiscountEngine.calculateDiscounts(cart, discounts);
59
+
60
+ expect(result.totalDiscount).toBe(10);
61
+ expect(result.appliedDiscounts).toHaveLength(1);
62
+ expect(result.appliedDiscounts[0].discountId).toBe("bundle1");
63
+ expect(result.items).toHaveLength(2);
64
+
65
+ // Check final prices
66
+ const product1Item = result.items.find(item => item.product.id === "product1");
67
+ expect(product1Item?.finalPrice).toBeCloseTo(7.14, 2); // 10 - (5.71/2) = 7.145
68
+
69
+ const product2Item = result.items.find(item => item.product.id === "product2");
70
+ expect(product2Item?.finalPrice).toBeCloseTo(10.71, 2); // 15 - 4.29 = 10.71
71
+ });
72
+
73
+ it("should handle multiple discounts", () => {
74
+ const cart = [
75
+ {
76
+ amount: 4,
77
+ product: {
78
+ id: "product1",
79
+ price: 10,
80
+ },
81
+ },
82
+ ];
83
+
84
+ const discounts: TDiscount[] = [
85
+ {
86
+ type: "Discount",
87
+ storeId: "store1",
88
+ companyId: "company1",
89
+ id: "bundle1",
90
+ name: [{ lang: "he", value: "Buy 3 for $25" }],
91
+ active: true,
92
+ startDate: Date.now() - 1000,
93
+ endDate: Date.now() + 1000,
94
+ variant: {
95
+ variantType: "bundle",
96
+ productsId: ["product1"],
97
+ requiredQuantity: 3,
98
+ bundlePrice: 25,
99
+ },
100
+ conditions: {
101
+ stackable: true, // Allow stacking
102
+ },
103
+ },
104
+ {
105
+ type: "Discount",
106
+ storeId: "store1",
107
+ companyId: "company1",
108
+ id: "bundle2",
109
+ name: [{ lang: "he", value: "Buy 2 for $15" }],
110
+ active: true,
111
+ startDate: Date.now() - 1000,
112
+ endDate: Date.now() + 1000,
113
+ variant: {
114
+ variantType: "bundle",
115
+ productsId: ["product1"],
116
+ requiredQuantity: 2,
117
+ bundlePrice: 15,
118
+ },
119
+ conditions: {
120
+ stackable: true,
121
+ },
122
+ },
123
+ ];
124
+
125
+ const result = DiscountEngine.calculateDiscounts(cart, discounts);
126
+
127
+ expect(result.appliedDiscounts).toHaveLength(2);
128
+ expect(result.totalDiscount).toBeGreaterThan(0);
129
+ });
130
+
131
+ it("should filter out inactive discounts", () => {
132
+ const cart = [
133
+ {
134
+ amount: 3,
135
+ product: {
136
+ id: "product1",
137
+ price: 10,
138
+ },
139
+ },
140
+ ];
141
+
142
+ const discounts: TDiscount[] = [
143
+ {
144
+ type: "Discount",
145
+ storeId: "store1",
146
+ companyId: "company1",
147
+ id: "bundle1",
148
+ name: [{ lang: "he", value: "Buy 3 for $25" }],
149
+ active: false, // Inactive
150
+ startDate: Date.now() - 1000,
151
+ endDate: Date.now() + 1000,
152
+ variant: {
153
+ variantType: "bundle",
154
+ productsId: ["product1"],
155
+ requiredQuantity: 3,
156
+ bundlePrice: 25,
157
+ },
158
+ conditions: {
159
+ stackable: false,
160
+ },
161
+ },
162
+ ];
163
+
164
+ const result = DiscountEngine.calculateDiscounts(cart, discounts);
165
+
166
+ expect(result.totalDiscount).toBe(0);
167
+ expect(result.appliedDiscounts).toHaveLength(0);
168
+ });
169
+
170
+ it("should handle empty cart", () => {
171
+ const cart: Array<{
172
+ amount: number;
173
+ product: {
174
+ id: string;
175
+ price: number;
176
+ };
177
+ }> = [];
178
+ const discounts: TDiscount[] = [
179
+ {
180
+ type: "Discount",
181
+ storeId: "store1",
182
+ companyId: "company1",
183
+ id: "bundle1",
184
+ name: [{ lang: "he", value: "Buy 3 for $25" }],
185
+ active: true,
186
+ startDate: Date.now() - 1000,
187
+ endDate: Date.now() + 1000,
188
+ variant: {
189
+ variantType: "bundle",
190
+ productsId: ["product1"],
191
+ requiredQuantity: 3,
192
+ bundlePrice: 25,
193
+ },
194
+ conditions: {
195
+ stackable: false,
196
+ },
197
+ },
198
+ ];
199
+
200
+ const result = DiscountEngine.calculateDiscounts(cart, discounts);
201
+
202
+ expect(result.totalDiscount).toBe(0);
203
+ expect(result.appliedDiscounts).toHaveLength(0);
204
+ expect(result.items).toHaveLength(0);
205
+ });
206
+
207
+ it("should handle empty discounts", () => {
208
+ const cart = [
209
+ {
210
+ amount: 2,
211
+ product: {
212
+ id: "product1",
213
+ price: 10,
214
+ },
215
+ },
216
+ ];
217
+
218
+ const discounts: TDiscount[] = [];
219
+
220
+ const result = DiscountEngine.calculateDiscounts(cart, discounts);
221
+
222
+ expect(result.totalDiscount).toBe(0);
223
+ expect(result.appliedDiscounts).toHaveLength(0);
224
+ expect(result.items).toHaveLength(1);
225
+ expect(result.items[0].finalPrice).toBe(10); // No discount applied
226
+ });
227
+ });
228
+
229
+ describe("isDiscountActive", () => {
230
+ it("should return true for active discount", () => {
231
+ const discount: TDiscount = {
232
+ type: "Discount",
233
+ storeId: "store1",
234
+ companyId: "company1",
235
+ id: "bundle1",
236
+ name: [{ lang: "he", value: "Buy 3 for $25" }],
237
+ active: true,
238
+ startDate: Date.now() - 1000,
239
+ endDate: Date.now() + 1000,
240
+ variant: {
241
+ variantType: "bundle",
242
+ productsId: ["product1"],
243
+ requiredQuantity: 3,
244
+ bundlePrice: 25,
245
+ },
246
+ };
247
+
248
+ expect(DiscountEngine.isDiscountActive(discount)).toBe(true);
249
+ });
250
+
251
+ it("should return false for inactive discount", () => {
252
+ const discount: TDiscount = {
253
+ type: "Discount",
254
+ storeId: "store1",
255
+ companyId: "company1",
256
+ id: "bundle1",
257
+ name: [{ lang: "he", value: "Buy 3 for $25" }],
258
+ active: false,
259
+ startDate: Date.now() - 1000,
260
+ endDate: Date.now() + 1000,
261
+ variant: {
262
+ variantType: "bundle",
263
+ productsId: ["product1"],
264
+ requiredQuantity: 3,
265
+ bundlePrice: 25,
266
+ },
267
+ };
268
+
269
+ expect(DiscountEngine.isDiscountActive(discount)).toBe(false);
270
+ });
271
+
272
+ it("should return false for expired discount", () => {
273
+ const discount: TDiscount = {
274
+ type: "Discount",
275
+ storeId: "store1",
276
+ companyId: "company1",
277
+ id: "bundle1",
278
+ name: [{ lang: "he", value: "Buy 3 for $25" }],
279
+ active: true,
280
+ startDate: Date.now() - 2000,
281
+ endDate: Date.now() - 1000, // Expired
282
+ variant: {
283
+ variantType: "bundle",
284
+ productsId: ["product1"],
285
+ requiredQuantity: 3,
286
+ bundlePrice: 25,
287
+ },
288
+ };
289
+
290
+ expect(DiscountEngine.isDiscountActive(discount)).toBe(false);
291
+ });
292
+ });
293
+
294
+ describe("getActiveDiscounts", () => {
295
+ it("should filter active discounts", () => {
296
+ const discounts: TDiscount[] = [
297
+ {
298
+ type: "Discount",
299
+ storeId: "store1",
300
+ companyId: "company1",
301
+ id: "active1",
302
+ name: [{ lang: "he", value: "Active Discount" }],
303
+ active: true,
304
+ startDate: Date.now() - 1000,
305
+ endDate: Date.now() + 1000,
306
+ variant: {
307
+ variantType: "bundle",
308
+ productsId: ["product1"],
309
+ requiredQuantity: 3,
310
+ bundlePrice: 25,
311
+ },
312
+ },
313
+ {
314
+ type: "Discount",
315
+ storeId: "store1",
316
+ companyId: "company1",
317
+ id: "inactive1",
318
+ name: [{ lang: "he", value: "Inactive Discount" }],
319
+ active: false,
320
+ startDate: Date.now() - 1000,
321
+ endDate: Date.now() + 1000,
322
+ variant: {
323
+ variantType: "bundle",
324
+ productsId: ["product1"],
325
+ requiredQuantity: 3,
326
+ bundlePrice: 25,
327
+ },
328
+ },
329
+ ];
330
+
331
+ const activeDiscounts = DiscountEngine.getActiveDiscounts(discounts);
332
+
333
+ expect(activeDiscounts).toHaveLength(1);
334
+ expect(activeDiscounts[0].id).toBe("active1");
335
+ });
336
+ });
337
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { DiscountStrategyFactory } from "../factory";
3
+ import { DiscountStrategy } from "../strategy";
4
+ import { TDiscount } from "../types";
5
+
6
+ describe("DiscountStrategyFactory", () => {
7
+ beforeEach(() => {
8
+ DiscountStrategyFactory.clearStrategies();
9
+ });
10
+
11
+ afterEach(() => {
12
+ DiscountStrategyFactory.clearStrategies();
13
+ });
14
+
15
+ it("should register and get strategy", () => {
16
+ const mockStrategy: DiscountStrategy = {
17
+ canApply: () => false,
18
+ calculate: () => ({ applicable: false, discountAmount: 0, affectedItems: [] }),
19
+ };
20
+
21
+ DiscountStrategyFactory.registerStrategy("bundle", mockStrategy);
22
+
23
+ const discount: TDiscount = {
24
+ type: "Discount",
25
+ storeId: "store1",
26
+ companyId: "company1",
27
+ id: "test1",
28
+ name: [{ lang: "he", value: "Test Discount" }],
29
+ active: true,
30
+ startDate: Date.now() - 1000,
31
+ endDate: Date.now() + 1000,
32
+ variant: {
33
+ variantType: "bundle",
34
+ productsId: ["product1"],
35
+ requiredQuantity: 3,
36
+ bundlePrice: 25,
37
+ },
38
+ };
39
+
40
+ const result = DiscountStrategyFactory.getStrategy(discount);
41
+ expect(result).toBe(mockStrategy);
42
+ });
43
+
44
+ it("should return null for unregistered strategy", () => {
45
+ const discount: TDiscount = {
46
+ type: "Discount",
47
+ storeId: "store1",
48
+ companyId: "company1",
49
+ id: "test1",
50
+ name: [{ lang: "he", value: "Test Discount" }],
51
+ active: true,
52
+ startDate: Date.now() - 1000,
53
+ endDate: Date.now() + 1000,
54
+ variant: {
55
+ variantType: "bundle",
56
+ productsId: ["product1"],
57
+ requiredQuantity: 3,
58
+ bundlePrice: 25,
59
+ },
60
+ };
61
+
62
+ const result = DiscountStrategyFactory.getStrategy(discount);
63
+ expect(result).toBeNull();
64
+ });
65
+
66
+ it("should get registered types", () => {
67
+ const mockStrategy: DiscountStrategy = {
68
+ canApply: () => false,
69
+ calculate: () => ({ applicable: false, discountAmount: 0, affectedItems: [] }),
70
+ };
71
+
72
+ DiscountStrategyFactory.registerStrategy("bundle", mockStrategy);
73
+ DiscountStrategyFactory.registerStrategy("percentage", mockStrategy);
74
+
75
+ const types = DiscountStrategyFactory.getRegisteredTypes();
76
+ expect(types).toEqual(["bundle", "percentage"]);
77
+ });
78
+
79
+ it("should clear strategies", () => {
80
+ const mockStrategy: DiscountStrategy = {
81
+ canApply: () => false,
82
+ calculate: () => ({ applicable: false, discountAmount: 0, affectedItems: [] }),
83
+ };
84
+
85
+ DiscountStrategyFactory.registerStrategy("bundle", mockStrategy);
86
+ expect(DiscountStrategyFactory.getRegisteredTypes()).toHaveLength(1);
87
+
88
+ DiscountStrategyFactory.clearStrategies();
89
+ expect(DiscountStrategyFactory.getRegisteredTypes()).toHaveLength(0);
90
+ });
91
+ });
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { DiscountEngine } from "../engine";
3
+ import { DiscountStrategyFactory } from "../factory";
4
+ import { BundleDiscountStrategy } from "../strategies/BundleStrategy";
5
+ import { TDiscount } from "../types";
6
+
7
+ describe("Discount System Integration", () => {
8
+ beforeEach(() => {
9
+ DiscountStrategyFactory.clearStrategies();
10
+ DiscountStrategyFactory.registerStrategy("bundle", new BundleDiscountStrategy());
11
+ });
12
+
13
+ afterEach(() => {
14
+ DiscountStrategyFactory.clearStrategies();
15
+ });
16
+
17
+ it("should handle real-world bundle scenario", () => {
18
+ // Scenario: Buy 3 dairy products for $20
19
+ const cart = [
20
+ {
21
+ amount: 2, // 2 milk
22
+ product: {
23
+ id: "milk",
24
+ price: 8, // $8 each
25
+ },
26
+ },
27
+ {
28
+ amount: 1, // 1 yogurt
29
+ product: {
30
+ id: "yogurt",
31
+ price: 6, // $6 each
32
+ },
33
+ },
34
+ {
35
+ amount: 1, // 1 bread (not in bundle)
36
+ product: {
37
+ id: "bread",
38
+ price: 4, // $4 each
39
+ },
40
+ },
41
+ ];
42
+
43
+ const dairyBundle: TDiscount = {
44
+ type: "Discount",
45
+ storeId: "store1",
46
+ companyId: "company1",
47
+ id: "dairy-bundle",
48
+ name: [{ lang: "he", value: "3 Dairy Products for $20" }],
49
+ active: true,
50
+ startDate: Date.now() - 1000,
51
+ endDate: Date.now() + 1000,
52
+ variant: {
53
+ variantType: "bundle",
54
+ productsId: ["milk", "yogurt", "cheese"], // Dairy products
55
+ requiredQuantity: 3,
56
+ bundlePrice: 20,
57
+ },
58
+ conditions: {
59
+ stackable: false,
60
+ },
61
+ };
62
+
63
+ const result = DiscountEngine.calculateDiscounts(cart, [dairyBundle]);
64
+
65
+ // Original prices:
66
+ // milk: 2 × $8 = $16
67
+ // yogurt: 1 × $6 = $6
68
+ // bread: 1 × $4 = $4
69
+ // Total: $26
70
+
71
+ // Bundle applies to milk + yogurt (3 items for $20)
72
+ // Remaining: bread at $4
73
+ // New total: $24
74
+
75
+ expect(result.totalDiscount).toBe(2); // $26 - $24
76
+ expect(result.appliedDiscounts).toHaveLength(1);
77
+ expect(result.appliedDiscounts[0].discountId).toBe("dairy-bundle");
78
+
79
+ // Check individual item prices
80
+ const milkItem = result.items.find(item => item.product.id === "milk");
81
+ expect(milkItem?.finalPrice).toBeCloseTo(7.28, 2); // 8 - (1.45/2) = 7.275
82
+
83
+ const yogurtItem = result.items.find(item => item.product.id === "yogurt");
84
+ expect(yogurtItem?.finalPrice).toBeCloseTo(5.45, 2); // 6 - 0.55 = 5.45
85
+
86
+ const breadItem = result.items.find(item => item.product.id === "bread");
87
+ expect(breadItem?.finalPrice).toBe(4); // No discount
88
+ });
89
+
90
+ it("should handle BOGO scenario", () => {
91
+ // Scenario: Buy 2 Get 1 Free
92
+ const cart = [
93
+ {
94
+ amount: 3, // Buy 3 items
95
+ product: {
96
+ id: "product1",
97
+ price: 10, // $10 each
98
+ },
99
+ },
100
+ ];
101
+
102
+ const bogoDiscount: TDiscount = {
103
+ type: "Discount",
104
+ storeId: "store1",
105
+ companyId: "company1",
106
+ id: "bogo",
107
+ name: [{ lang: "he", value: "Buy 2 Get 1 Free" }],
108
+ active: true,
109
+ startDate: Date.now() - 1000,
110
+ endDate: Date.now() + 1000,
111
+ variant: {
112
+ variantType: "bundle",
113
+ productsId: ["product1"],
114
+ requiredQuantity: 3, // Buy 2 + get 1 free = 3 items
115
+ bundlePrice: 20, // Price of 2 items (10 × 2)
116
+ },
117
+ conditions: {
118
+ stackable: false,
119
+ },
120
+ };
121
+
122
+ const result = DiscountEngine.calculateDiscounts(cart, [bogoDiscount]);
123
+
124
+ // Original: 3 × $10 = $30
125
+ // Bundle: 3 items for $20
126
+ // Discount: $10
127
+
128
+ expect(result.totalDiscount).toBe(10);
129
+ expect(result.appliedDiscounts).toHaveLength(1);
130
+ expect(result.items[0].finalPrice).toBeCloseTo(6.67, 2); // $20 / 3
131
+ });
132
+ });