@jsdev_ninja/core 0.12.5 → 0.12.7

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.
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- declare const StoreSchema: z.ZodObject<{
2
+ export declare const clientTypesSchema: z.ZodEnum<["individual", "company"]>;
3
+ export declare const StoreSchema: z.ZodObject<{
3
4
  id: z.ZodString;
4
5
  companyId: z.ZodString;
5
6
  name: z.ZodString;
@@ -43,5 +44,4 @@ declare const StoreSchema: z.ZodObject<{
43
44
  deliveryPrice?: number | undefined;
44
45
  }>;
45
46
  export type TStore = z.infer<typeof StoreSchema>;
46
- export {};
47
47
  //# sourceMappingURL=Store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Store.d.ts","sourceRoot":"","sources":["../../lib/entities/Store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAcf,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC"}
1
+ {"version":3,"file":"Store.d.ts","sourceRoot":"","sources":["../../lib/entities/Store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,iBAAiB,sCAAoC,CAAC;AAEnE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EActB,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
- const clientTypesSchema = z.enum(["individual", "company"]);
3
- const StoreSchema = z.object({
2
+ export const clientTypesSchema = z.enum(["individual", "company"]);
3
+ export const StoreSchema = z.object({
4
4
  id: z.string(),
5
5
  companyId: z.string(),
6
6
  name: z.string(),
@@ -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
+ });