washday-sdk 1.6.69 → 1.6.70
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/api/discounts/get.js +19 -4
- package/dist/utils/orders/calculateOrderTotal.js +7 -3
- package/dist/utils/orders/helpers.js +21 -0
- package/package.json +1 -1
- package/src/api/discounts/get.ts +30 -4
- package/src/api/discounts/put.ts +3 -0
- package/src/api/order/put.ts +7 -0
- package/src/interfaces/Store.ts +1 -0
- package/src/utils/orders/calculateOrderTotal.ts +7 -1
- package/src/utils/orders/helpers.ts +24 -0
- package/test/orders.discountCodeOnceOnOrderAndMinimum.test.ts +291 -0
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -75,13 +75,27 @@ export const getAutomaticDiscountById = function (storeId, discountId) {
|
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
};
|
|
78
|
-
|
|
78
|
+
const buildVerifyQueryString = (params) => {
|
|
79
|
+
if (!params)
|
|
80
|
+
return '';
|
|
81
|
+
const entries = [];
|
|
82
|
+
if (params.currentDate)
|
|
83
|
+
entries.push(`currentDate=${encodeURIComponent(params.currentDate)}`);
|
|
84
|
+
if (params.orderSubtotal !== undefined &&
|
|
85
|
+
params.orderSubtotal !== null &&
|
|
86
|
+
Number.isFinite(Number(params.orderSubtotal))) {
|
|
87
|
+
entries.push(`orderSubtotal=${encodeURIComponent(String(params.orderSubtotal))}`);
|
|
88
|
+
}
|
|
89
|
+
return entries.length ? `?${entries.join('&')}` : '';
|
|
90
|
+
};
|
|
91
|
+
export const verifyDiscountCode = function (storeId, code, params) {
|
|
79
92
|
return __awaiter(this, void 0, void 0, function* () {
|
|
80
93
|
try {
|
|
81
94
|
const config = {
|
|
82
95
|
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
83
96
|
};
|
|
84
|
-
|
|
97
|
+
const qs = buildVerifyQueryString(params);
|
|
98
|
+
return yield this.axiosInstance.get(`${GET_SET_DISCOUNT_CODES}/${storeId}/${code}/verify${qs}`, config);
|
|
85
99
|
}
|
|
86
100
|
catch (error) {
|
|
87
101
|
console.error('Error fetching verifyDiscountCode:', error);
|
|
@@ -89,13 +103,14 @@ export const verifyDiscountCode = function (storeId, code) {
|
|
|
89
103
|
}
|
|
90
104
|
});
|
|
91
105
|
};
|
|
92
|
-
export const verifyDiscountCodeCustomersApp = function (storeId, code) {
|
|
106
|
+
export const verifyDiscountCodeCustomersApp = function (storeId, code, params) {
|
|
93
107
|
return __awaiter(this, void 0, void 0, function* () {
|
|
94
108
|
try {
|
|
95
109
|
const config = {
|
|
96
110
|
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
97
111
|
};
|
|
98
|
-
|
|
112
|
+
const qs = buildVerifyQueryString(params);
|
|
113
|
+
return yield this.axiosInstance.get(`${GET_DISCOUNT_CODES_CUSTOMER_APP}/${storeId}/${code}/verify${qs}`, config);
|
|
99
114
|
}
|
|
100
115
|
catch (error) {
|
|
101
116
|
console.error('Error fetching verifyDiscountCodeCustomersApp:', error);
|
|
@@ -38,7 +38,7 @@ const hasMatchingDiscountTarget = (targets = [], product) => {
|
|
|
38
38
|
};
|
|
39
39
|
export const calculateOrderTotal = (order, selectedCustomer, storeSettings, hasShippingCost, storeDiscounts = [], discountCodeObj, redeemPointsDiscount = 0 // 💡 NUEVO parámetro
|
|
40
40
|
) => {
|
|
41
|
-
var _a;
|
|
41
|
+
var _a, _b, _c;
|
|
42
42
|
try {
|
|
43
43
|
const appliedOrderDiscounts = {};
|
|
44
44
|
const productTableCalculator = (storeSettings === null || storeSettings === void 0 ? void 0 : storeSettings.taxesType) === 'over_price'
|
|
@@ -54,7 +54,11 @@ export const calculateOrderTotal = (order, selectedCustomer, storeSettings, hasS
|
|
|
54
54
|
discountCodeObj.applyOnceOnOrder) {
|
|
55
55
|
const includesProducts = discountCodeObj.applyToAllProducts ||
|
|
56
56
|
order.products.some((curr) => hasMatchingDiscountTarget(discountCodeObj.products, curr));
|
|
57
|
-
|
|
57
|
+
const minimumOrderSubtotal = Number((_a = discountCodeObj.minimumOrderSubtotal) !== null && _a !== void 0 ? _a : 0);
|
|
58
|
+
const meetsMinimum = !Number.isFinite(minimumOrderSubtotal) ||
|
|
59
|
+
minimumOrderSubtotal <= 0 ||
|
|
60
|
+
Number((_b = totalImportWithoutDiscount !== null && totalImportWithoutDiscount !== void 0 ? totalImportWithoutDiscount : totalSubtotalAmount) !== null && _b !== void 0 ? _b : 0) >= minimumOrderSubtotal;
|
|
61
|
+
discountCodeAmount = includesProducts && meetsMinimum ? discountCodeObj.value : 0;
|
|
58
62
|
}
|
|
59
63
|
// === SHIPPING COST ===
|
|
60
64
|
const shippingCost = getShippingCost(discountCodeObj, hasShippingCost, storeSettings);
|
|
@@ -68,7 +72,7 @@ export const calculateOrderTotal = (order, selectedCustomer, storeSettings, hasS
|
|
|
68
72
|
creditApplied -
|
|
69
73
|
redeemPointsDiscount).toFixed(2);
|
|
70
74
|
// === RETURN FINAL OBJECT ===
|
|
71
|
-
return Object.assign(Object.assign({}, order), { totalQuantity, customerDiscount: order.discountCode ? 0 : (
|
|
75
|
+
return Object.assign(Object.assign({}, order), { totalQuantity, customerDiscount: order.discountCode ? 0 : (_c = selectedCustomer === null || selectedCustomer === void 0 ? void 0 : selectedCustomer.customer) === null || _c === void 0 ? void 0 : _c.discount, productTotal: totalImportWithDiscount, taxesTotal: totalImporTaxes, taxBreakdown, total: orderTotal, creditApplied, redeemPointsApplied: redeemPointsDiscount, productTotalWithoutDiscount: totalImportWithoutDiscount, totalDiscountAmount: totalDiscountAmount + discountCodeAmount, shippingServiceTotal: shippingCost, appliedOrderDiscounts, subtotal: totalSubtotalAmount });
|
|
72
76
|
}
|
|
73
77
|
catch (error) {
|
|
74
78
|
throw error;
|
|
@@ -225,7 +225,28 @@ export const getCreditApplied = (selectedCustomer, orderTotal) => {
|
|
|
225
225
|
}
|
|
226
226
|
return Math.min(customerCredit, orderTotal);
|
|
227
227
|
};
|
|
228
|
+
const productsMeetMinimumSubtotal = (productsArr, isExpress, minimumOrderSubtotal) => {
|
|
229
|
+
if (!Number.isFinite(minimumOrderSubtotal) || minimumOrderSubtotal <= 0)
|
|
230
|
+
return true;
|
|
231
|
+
const subtotal = (productsArr || []).reduce((sum, prod) => {
|
|
232
|
+
var _a, _b, _c, _d, _e;
|
|
233
|
+
if (!prod || prod.isBuyAndGetProduct || prod.isFreeItem)
|
|
234
|
+
return sum;
|
|
235
|
+
const price = Number((_b = (_a = (isExpress ? prod.expressPrice : prod.price)) !== null && _a !== void 0 ? _a : prod.price) !== null && _b !== void 0 ? _b : 0);
|
|
236
|
+
const qty = Number((_d = (_c = prod.quantity) !== null && _c !== void 0 ? _c : prod.qty) !== null && _d !== void 0 ? _d : 0);
|
|
237
|
+
const extra = Number((_e = prod.extraAmount) !== null && _e !== void 0 ? _e : 0);
|
|
238
|
+
if (!Number.isFinite(price) || !Number.isFinite(qty))
|
|
239
|
+
return sum;
|
|
240
|
+
return sum + price * qty + (Number.isFinite(extra) ? extra : 0);
|
|
241
|
+
}, 0);
|
|
242
|
+
return subtotal >= minimumOrderSubtotal;
|
|
243
|
+
};
|
|
228
244
|
export const applyDiscountToProducts = (discountCode, productsArr, isExpress, storeDiscounts = [], selectedCustomer = { discount: 0 }) => {
|
|
245
|
+
var _a;
|
|
246
|
+
if (discountCode &&
|
|
247
|
+
!productsMeetMinimumSubtotal(productsArr, isExpress, Number((_a = discountCode.minimumOrderSubtotal) !== null && _a !== void 0 ? _a : 0))) {
|
|
248
|
+
discountCode = null;
|
|
249
|
+
}
|
|
229
250
|
if (!discountCode) {
|
|
230
251
|
// Caso sin discountCode: se aplican descuentos automáticos solo sobre el precio del producto
|
|
231
252
|
return {
|
package/package.json
CHANGED
package/src/api/discounts/get.ts
CHANGED
|
@@ -63,24 +63,50 @@ export const getAutomaticDiscountById = async function (this: WashdayClientInsta
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
const buildVerifyQueryString = (params?: { orderSubtotal?: number; currentDate?: string }): string => {
|
|
67
|
+
if (!params) return '';
|
|
68
|
+
const entries: string[] = [];
|
|
69
|
+
if (params.currentDate) entries.push(`currentDate=${encodeURIComponent(params.currentDate)}`);
|
|
70
|
+
if (
|
|
71
|
+
params.orderSubtotal !== undefined &&
|
|
72
|
+
params.orderSubtotal !== null &&
|
|
73
|
+
Number.isFinite(Number(params.orderSubtotal))
|
|
74
|
+
) {
|
|
75
|
+
entries.push(`orderSubtotal=${encodeURIComponent(String(params.orderSubtotal))}`);
|
|
76
|
+
}
|
|
77
|
+
return entries.length ? `?${entries.join('&')}` : '';
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const verifyDiscountCode = async function (
|
|
81
|
+
this: WashdayClientInstance,
|
|
82
|
+
storeId: string,
|
|
83
|
+
code: string,
|
|
84
|
+
params?: { orderSubtotal?: number; currentDate?: string },
|
|
85
|
+
): Promise<any> {
|
|
67
86
|
try {
|
|
68
87
|
const config = {
|
|
69
88
|
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
70
89
|
};
|
|
71
|
-
|
|
90
|
+
const qs = buildVerifyQueryString(params);
|
|
91
|
+
return await this.axiosInstance.get(`${GET_SET_DISCOUNT_CODES}/${storeId}/${code}/verify${qs}`, config);
|
|
72
92
|
} catch (error) {
|
|
73
93
|
console.error('Error fetching verifyDiscountCode:', error);
|
|
74
94
|
throw error;
|
|
75
95
|
}
|
|
76
96
|
};
|
|
77
97
|
|
|
78
|
-
export const verifyDiscountCodeCustomersApp = async function (
|
|
98
|
+
export const verifyDiscountCodeCustomersApp = async function (
|
|
99
|
+
this: WashdayClientInstance,
|
|
100
|
+
storeId: string,
|
|
101
|
+
code: string,
|
|
102
|
+
params?: { orderSubtotal?: number; currentDate?: string },
|
|
103
|
+
): Promise<any> {
|
|
79
104
|
try {
|
|
80
105
|
const config = {
|
|
81
106
|
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
82
107
|
};
|
|
83
|
-
|
|
108
|
+
const qs = buildVerifyQueryString(params);
|
|
109
|
+
return await this.axiosInstance.get(`${GET_DISCOUNT_CODES_CUSTOMER_APP}/${storeId}/${code}/verify${qs}`, config);
|
|
84
110
|
} catch (error) {
|
|
85
111
|
console.error('Error fetching verifyDiscountCodeCustomersApp:', error);
|
|
86
112
|
throw error;
|
package/src/api/discounts/put.ts
CHANGED
|
@@ -52,6 +52,9 @@ export const updateDiscountCodeById = async function (this: WashdayClientInstanc
|
|
|
52
52
|
code?: string;
|
|
53
53
|
fromDate?: string;
|
|
54
54
|
toDate?: string;
|
|
55
|
+
applyOnceOnOrder?: boolean;
|
|
56
|
+
applyToAllProducts?: boolean;
|
|
57
|
+
minimumOrderSubtotal?: number;
|
|
55
58
|
}): Promise<any> {
|
|
56
59
|
try {
|
|
57
60
|
const config = {
|
package/src/api/order/put.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { AxiosResponse } from "axios";
|
|
2
2
|
import { WashdayClientInstance } from "../../interfaces/Api";
|
|
3
3
|
import { IUpdateOrderByIdDto } from "../../interfaces/Order";
|
|
4
|
+
import { PaymentMethodsEnum } from "../../enum";
|
|
4
5
|
import axiosInstance from "../axiosInstance";
|
|
5
6
|
import { generateQueryParamsStr } from "../../utils/apiUtils";
|
|
6
7
|
const GET_SET_ORDER = 'api/v2/order';
|
|
7
8
|
const GET_SET_ORDER_OLD = 'api/order';
|
|
8
9
|
const GET_SET_ORDER_PAYMENTLINES = (orderId: string) => `/api/v2/order/${orderId}/paymentLines`;
|
|
9
10
|
|
|
11
|
+
type EditablePaymentMethod =
|
|
12
|
+
| PaymentMethodsEnum.Cash
|
|
13
|
+
| PaymentMethodsEnum.Card
|
|
14
|
+
| PaymentMethodsEnum.Transfer;
|
|
15
|
+
|
|
10
16
|
export const updateById = async function (this: WashdayClientInstance, id: string, data: IUpdateOrderByIdDto): Promise<any> {
|
|
11
17
|
try {
|
|
12
18
|
const config = {
|
|
@@ -21,6 +27,7 @@ export const updateById = async function (this: WashdayClientInstance, id: strin
|
|
|
21
27
|
|
|
22
28
|
export const updatePaymentLineById = async function (this: WashdayClientInstance, orderId: string, id: string, data: {
|
|
23
29
|
amountPaid: number
|
|
30
|
+
paymentMethod?: EditablePaymentMethod
|
|
24
31
|
updatedDate: Date
|
|
25
32
|
pinUserId?: string
|
|
26
33
|
}): Promise<any> {
|
package/src/interfaces/Store.ts
CHANGED
|
@@ -92,7 +92,13 @@ export const calculateOrderTotal = (
|
|
|
92
92
|
hasMatchingDiscountTarget(discountCodeObj.products, curr)
|
|
93
93
|
);
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
const minimumOrderSubtotal = Number(discountCodeObj.minimumOrderSubtotal ?? 0);
|
|
96
|
+
const meetsMinimum =
|
|
97
|
+
!Number.isFinite(minimumOrderSubtotal) ||
|
|
98
|
+
minimumOrderSubtotal <= 0 ||
|
|
99
|
+
Number(totalImportWithoutDiscount ?? totalSubtotalAmount ?? 0) >= minimumOrderSubtotal;
|
|
100
|
+
|
|
101
|
+
discountCodeAmount = includesProducts && meetsMinimum ? discountCodeObj.value : 0;
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
// === SHIPPING COST ===
|
|
@@ -307,6 +307,23 @@ export const getCreditApplied = (selectedCustomer: { customer: ICustomer } | und
|
|
|
307
307
|
return Math.min(customerCredit, orderTotal);
|
|
308
308
|
};
|
|
309
309
|
|
|
310
|
+
const productsMeetMinimumSubtotal = (
|
|
311
|
+
productsArr: any[] | undefined,
|
|
312
|
+
isExpress: boolean,
|
|
313
|
+
minimumOrderSubtotal: number,
|
|
314
|
+
): boolean => {
|
|
315
|
+
if (!Number.isFinite(minimumOrderSubtotal) || minimumOrderSubtotal <= 0) return true;
|
|
316
|
+
const subtotal = (productsArr || []).reduce((sum: number, prod: any) => {
|
|
317
|
+
if (!prod || prod.isBuyAndGetProduct || prod.isFreeItem) return sum;
|
|
318
|
+
const price = Number((isExpress ? prod.expressPrice : prod.price) ?? prod.price ?? 0);
|
|
319
|
+
const qty = Number(prod.quantity ?? prod.qty ?? 0);
|
|
320
|
+
const extra = Number(prod.extraAmount ?? 0);
|
|
321
|
+
if (!Number.isFinite(price) || !Number.isFinite(qty)) return sum;
|
|
322
|
+
return sum + price * qty + (Number.isFinite(extra) ? extra : 0);
|
|
323
|
+
}, 0);
|
|
324
|
+
return subtotal >= minimumOrderSubtotal;
|
|
325
|
+
};
|
|
326
|
+
|
|
310
327
|
export const applyDiscountToProducts = (
|
|
311
328
|
discountCode: {
|
|
312
329
|
freeProductSetting: any;
|
|
@@ -315,6 +332,7 @@ export const applyDiscountToProducts = (
|
|
|
315
332
|
type: string;
|
|
316
333
|
applyToAllProducts: boolean;
|
|
317
334
|
applyOnceOnOrder: boolean;
|
|
335
|
+
minimumOrderSubtotal?: number;
|
|
318
336
|
buyAndGetConditions: any,
|
|
319
337
|
},
|
|
320
338
|
productsArr: any,
|
|
@@ -322,6 +340,12 @@ export const applyDiscountToProducts = (
|
|
|
322
340
|
storeDiscounts: any = [],
|
|
323
341
|
selectedCustomer = { discount: 0 }
|
|
324
342
|
) => {
|
|
343
|
+
if (
|
|
344
|
+
discountCode &&
|
|
345
|
+
!productsMeetMinimumSubtotal(productsArr, isExpress, Number(discountCode.minimumOrderSubtotal ?? 0))
|
|
346
|
+
) {
|
|
347
|
+
discountCode = null as any;
|
|
348
|
+
}
|
|
325
349
|
if (!discountCode) {
|
|
326
350
|
// Caso sin discountCode: se aplican descuentos automáticos solo sobre el precio del producto
|
|
327
351
|
return {
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { utils } from "../src";
|
|
2
|
+
|
|
3
|
+
const baseSelectedCustomer = {
|
|
4
|
+
customer: {
|
|
5
|
+
credit: 0,
|
|
6
|
+
discount: 0,
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const baseStore = {
|
|
11
|
+
taxesType: "over_price" as const,
|
|
12
|
+
taxOne: { name: "IVA", value: 16 },
|
|
13
|
+
taxTwo: null,
|
|
14
|
+
taxThree: null,
|
|
15
|
+
orderPageConfig: { shippingServiceCost: 0 },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const buildOrder = (overrides: any = {}) => ({
|
|
19
|
+
express: false,
|
|
20
|
+
products: [
|
|
21
|
+
{ _id: "p1", qty: 1, quantity: 1, price: 100, expressPrice: 100, extraAmount: 0 },
|
|
22
|
+
{ _id: "p2", qty: 1, quantity: 1, price: 150, expressPrice: 150, extraAmount: 0 },
|
|
23
|
+
],
|
|
24
|
+
buyAndGetProducts: [],
|
|
25
|
+
discountCode: null,
|
|
26
|
+
taxesType: "over_price",
|
|
27
|
+
...overrides,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("calculateOrderTotal — fixed-amount coupons", () => {
|
|
31
|
+
it("applyOnceOnOrder=true subtracts the coupon value once, regardless of quantity", () => {
|
|
32
|
+
const order = buildOrder({
|
|
33
|
+
products: [
|
|
34
|
+
{ _id: "p1", qty: 5, quantity: 5, price: 100, expressPrice: 100, extraAmount: 0 },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
const discountCode = {
|
|
38
|
+
type: "number",
|
|
39
|
+
value: 20,
|
|
40
|
+
applyToAllProducts: true,
|
|
41
|
+
products: [],
|
|
42
|
+
applyOnceOnOrder: true,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const result = utils.calculateOrderTotal(
|
|
46
|
+
order,
|
|
47
|
+
baseSelectedCustomer as any,
|
|
48
|
+
baseStore as any,
|
|
49
|
+
false,
|
|
50
|
+
[],
|
|
51
|
+
discountCode,
|
|
52
|
+
0
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// baseline (no coupon): 5*100=500 subtotal, +16% taxes over_price = 580.
|
|
56
|
+
// once-on-order applies a flat $20 reduction to the order total only.
|
|
57
|
+
expect(result.total).toBe(560);
|
|
58
|
+
// discount is order-level, not per-line, so totalDiscountAmount keeps the once-on-order $20.
|
|
59
|
+
expect(result.totalDiscountAmount).toBe(20);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("minimumOrderSubtotal blocks the once-on-order coupon when subtotal is below the minimum", () => {
|
|
63
|
+
const order = buildOrder();
|
|
64
|
+
const discountCode = {
|
|
65
|
+
type: "number",
|
|
66
|
+
value: 50,
|
|
67
|
+
applyToAllProducts: true,
|
|
68
|
+
products: [],
|
|
69
|
+
applyOnceOnOrder: true,
|
|
70
|
+
minimumOrderSubtotal: 300,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = utils.calculateOrderTotal(
|
|
74
|
+
order,
|
|
75
|
+
baseSelectedCustomer as any,
|
|
76
|
+
baseStore as any,
|
|
77
|
+
false,
|
|
78
|
+
[],
|
|
79
|
+
discountCode,
|
|
80
|
+
0
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// 250 < 300 → no discount applied, totals match the no-coupon baseline (250 + 40 taxes).
|
|
84
|
+
expect(result.totalDiscountAmount).toBe(0);
|
|
85
|
+
expect(result.total).toBe(290);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("minimumOrderSubtotal allows the once-on-order coupon at exactly the minimum", () => {
|
|
89
|
+
const order = buildOrder();
|
|
90
|
+
const discountCode = {
|
|
91
|
+
type: "number",
|
|
92
|
+
value: 50,
|
|
93
|
+
applyToAllProducts: true,
|
|
94
|
+
products: [],
|
|
95
|
+
applyOnceOnOrder: true,
|
|
96
|
+
minimumOrderSubtotal: 250,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const result = utils.calculateOrderTotal(
|
|
100
|
+
order,
|
|
101
|
+
baseSelectedCustomer as any,
|
|
102
|
+
baseStore as any,
|
|
103
|
+
false,
|
|
104
|
+
[],
|
|
105
|
+
discountCode,
|
|
106
|
+
0
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 250 == 250 → discount applied. Baseline 290, minus $50 = 240.
|
|
110
|
+
expect(result.total).toBe(240);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("zero/undefined minimumOrderSubtotal keeps current behavior (backwards compatible)", () => {
|
|
114
|
+
const order = buildOrder();
|
|
115
|
+
const discountCode = {
|
|
116
|
+
type: "number",
|
|
117
|
+
value: 50,
|
|
118
|
+
applyToAllProducts: true,
|
|
119
|
+
products: [],
|
|
120
|
+
applyOnceOnOrder: true,
|
|
121
|
+
minimumOrderSubtotal: 0,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = utils.calculateOrderTotal(
|
|
125
|
+
order,
|
|
126
|
+
baseSelectedCustomer as any,
|
|
127
|
+
baseStore as any,
|
|
128
|
+
false,
|
|
129
|
+
[],
|
|
130
|
+
discountCode,
|
|
131
|
+
0
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(result.total).toBe(240);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("applyDiscountToProducts — minimumOrderSubtotal gate", () => {
|
|
139
|
+
const sharedProducts = [
|
|
140
|
+
{ _id: "p1", price: 100, expressPrice: 100, qty: 1, quantity: 1 },
|
|
141
|
+
{ _id: "p2", price: 150, expressPrice: 150, qty: 1, quantity: 1 },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
it("applies per-product fixed discount when no minimum is configured", () => {
|
|
145
|
+
const { newOrderProds } = utils.applyDiscountToProducts(
|
|
146
|
+
{
|
|
147
|
+
type: "number",
|
|
148
|
+
value: 10,
|
|
149
|
+
applyToAllProducts: true,
|
|
150
|
+
products: [],
|
|
151
|
+
applyOnceOnOrder: false,
|
|
152
|
+
buyAndGetConditions: null,
|
|
153
|
+
freeProductSetting: null,
|
|
154
|
+
} as any,
|
|
155
|
+
sharedProducts,
|
|
156
|
+
false,
|
|
157
|
+
[],
|
|
158
|
+
{ discount: 0 }
|
|
159
|
+
);
|
|
160
|
+
expect(newOrderProds.every((p: any) => p.discountAmount === 10)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("skips the discount when subtotal is below minimumOrderSubtotal", () => {
|
|
164
|
+
const { newOrderProds } = utils.applyDiscountToProducts(
|
|
165
|
+
{
|
|
166
|
+
type: "number",
|
|
167
|
+
value: 10,
|
|
168
|
+
applyToAllProducts: true,
|
|
169
|
+
products: [],
|
|
170
|
+
applyOnceOnOrder: false,
|
|
171
|
+
minimumOrderSubtotal: 999,
|
|
172
|
+
buyAndGetConditions: null,
|
|
173
|
+
freeProductSetting: null,
|
|
174
|
+
} as any,
|
|
175
|
+
sharedProducts,
|
|
176
|
+
false,
|
|
177
|
+
[],
|
|
178
|
+
{ discount: 0 }
|
|
179
|
+
);
|
|
180
|
+
expect(newOrderProds.every((p: any) => (p.discountAmount ?? 0) === 0)).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("applies the discount when subtotal meets the minimum", () => {
|
|
184
|
+
const { newOrderProds } = utils.applyDiscountToProducts(
|
|
185
|
+
{
|
|
186
|
+
type: "number",
|
|
187
|
+
value: 10,
|
|
188
|
+
applyToAllProducts: true,
|
|
189
|
+
products: [],
|
|
190
|
+
applyOnceOnOrder: false,
|
|
191
|
+
minimumOrderSubtotal: 200,
|
|
192
|
+
buyAndGetConditions: null,
|
|
193
|
+
freeProductSetting: null,
|
|
194
|
+
} as any,
|
|
195
|
+
sharedProducts,
|
|
196
|
+
false,
|
|
197
|
+
[],
|
|
198
|
+
{ discount: 0 }
|
|
199
|
+
);
|
|
200
|
+
expect(newOrderProds.every((p: any) => p.discountAmount === 10)).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("uses expressPrice when isExpress=true for the minimum check", () => {
|
|
204
|
+
const expressProducts = [
|
|
205
|
+
{ _id: "p1", price: 100, expressPrice: 160, qty: 1, quantity: 1 },
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
// Non-express: subtotal=100, below minimum → discount skipped
|
|
209
|
+
const nonExpress = utils.applyDiscountToProducts(
|
|
210
|
+
{
|
|
211
|
+
type: "number",
|
|
212
|
+
value: 10,
|
|
213
|
+
applyToAllProducts: true,
|
|
214
|
+
products: [],
|
|
215
|
+
applyOnceOnOrder: false,
|
|
216
|
+
minimumOrderSubtotal: 150,
|
|
217
|
+
buyAndGetConditions: null,
|
|
218
|
+
freeProductSetting: null,
|
|
219
|
+
} as any,
|
|
220
|
+
expressProducts,
|
|
221
|
+
false,
|
|
222
|
+
[],
|
|
223
|
+
{ discount: 0 }
|
|
224
|
+
);
|
|
225
|
+
expect((nonExpress.newOrderProds[0] as any).discountAmount ?? 0).toBe(0);
|
|
226
|
+
|
|
227
|
+
// Express: subtotal=160, meets minimum → discount applied
|
|
228
|
+
const express = utils.applyDiscountToProducts(
|
|
229
|
+
{
|
|
230
|
+
type: "number",
|
|
231
|
+
value: 10,
|
|
232
|
+
applyToAllProducts: true,
|
|
233
|
+
products: [],
|
|
234
|
+
applyOnceOnOrder: false,
|
|
235
|
+
minimumOrderSubtotal: 150,
|
|
236
|
+
buyAndGetConditions: null,
|
|
237
|
+
freeProductSetting: null,
|
|
238
|
+
} as any,
|
|
239
|
+
expressProducts,
|
|
240
|
+
true,
|
|
241
|
+
[],
|
|
242
|
+
{ discount: 0 }
|
|
243
|
+
);
|
|
244
|
+
expect((express.newOrderProds[0] as any).discountAmount).toBe(10);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("includes extraAmount when checking the minimum subtotal", () => {
|
|
248
|
+
const productsWithExtra = [
|
|
249
|
+
{ _id: "p1", price: 100, expressPrice: 100, qty: 1, quantity: 1, extraAmount: 25 },
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
// Base price * qty = 100, + extraAmount 25 = 125 → minimum 120 should clear
|
|
253
|
+
const meetsWithExtra = utils.applyDiscountToProducts(
|
|
254
|
+
{
|
|
255
|
+
type: "number",
|
|
256
|
+
value: 5,
|
|
257
|
+
applyToAllProducts: true,
|
|
258
|
+
products: [],
|
|
259
|
+
applyOnceOnOrder: false,
|
|
260
|
+
minimumOrderSubtotal: 120,
|
|
261
|
+
buyAndGetConditions: null,
|
|
262
|
+
freeProductSetting: null,
|
|
263
|
+
} as any,
|
|
264
|
+
productsWithExtra,
|
|
265
|
+
false,
|
|
266
|
+
[],
|
|
267
|
+
{ discount: 0 }
|
|
268
|
+
);
|
|
269
|
+
expect((meetsWithExtra.newOrderProds[0] as any).discountAmount).toBe(5);
|
|
270
|
+
|
|
271
|
+
// Without extra (extraAmount removed), 100 < 120 → skipped
|
|
272
|
+
const productsNoExtra = [{ _id: "p1", price: 100, expressPrice: 100, qty: 1, quantity: 1 }];
|
|
273
|
+
const failsWithoutExtra = utils.applyDiscountToProducts(
|
|
274
|
+
{
|
|
275
|
+
type: "number",
|
|
276
|
+
value: 5,
|
|
277
|
+
applyToAllProducts: true,
|
|
278
|
+
products: [],
|
|
279
|
+
applyOnceOnOrder: false,
|
|
280
|
+
minimumOrderSubtotal: 120,
|
|
281
|
+
buyAndGetConditions: null,
|
|
282
|
+
freeProductSetting: null,
|
|
283
|
+
} as any,
|
|
284
|
+
productsNoExtra,
|
|
285
|
+
false,
|
|
286
|
+
[],
|
|
287
|
+
{ discount: 0 }
|
|
288
|
+
);
|
|
289
|
+
expect((failsWithoutExtra.newOrderProds[0] as any).discountAmount ?? 0).toBe(0);
|
|
290
|
+
});
|
|
291
|
+
});
|
/package/{CLAUDE.md → AGENTS.md}
RENAMED
|
File without changes
|