washday-sdk 1.6.67 → 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/api/order/post.js +1 -0
- 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/post.ts +2 -0
- package/src/api/order/put.ts +9 -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/test/orders.orderIdFallbackBody.test.ts +49 -0
- package/test/sdk.publishWorkflow.test.ts +27 -0
- package/.github/workflows/bump-version-pr.yml +0 -58
- package/.github/workflows/publish-on-merge.yml +0 -35
- /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);
|
package/dist/api/order/post.js
CHANGED
|
@@ -136,6 +136,7 @@ export const payAndCollect = function (params) {
|
|
|
136
136
|
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
137
137
|
};
|
|
138
138
|
return yield this.axiosInstance.post(PAY_AND_COLLECT(params.sequence, params.storeId), {
|
|
139
|
+
orderId: params.orderId,
|
|
139
140
|
paymentMethod: params.paymentMethod,
|
|
140
141
|
cashierBoxId: params.cashierBoxId,
|
|
141
142
|
amount: params.amount,
|
|
@@ -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/post.ts
CHANGED
|
@@ -138,6 +138,7 @@ export const setOrderUncollected = async function (this: WashdayClientInstance,
|
|
|
138
138
|
};
|
|
139
139
|
|
|
140
140
|
export const payAndCollect = async function (this: WashdayClientInstance, params: {
|
|
141
|
+
orderId?: string;
|
|
141
142
|
sequence: string;
|
|
142
143
|
storeId: string;
|
|
143
144
|
paymentMethod: string;
|
|
@@ -156,6 +157,7 @@ export const payAndCollect = async function (this: WashdayClientInstance, params
|
|
|
156
157
|
return await this.axiosInstance.post(
|
|
157
158
|
PAY_AND_COLLECT(params.sequence, params.storeId),
|
|
158
159
|
{
|
|
160
|
+
orderId: params.orderId,
|
|
159
161
|
paymentMethod: params.paymentMethod,
|
|
160
162
|
cashierBoxId: params.cashierBoxId,
|
|
161
163
|
amount: params.amount,
|
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> {
|
|
@@ -36,6 +43,7 @@ export const updatePaymentLineById = async function (this: WashdayClientInstance
|
|
|
36
43
|
};
|
|
37
44
|
|
|
38
45
|
export const setOrderCancelledBySequence = async function (this: WashdayClientInstance, sequence: string, storeId: string, data: {
|
|
46
|
+
orderId?: string,
|
|
39
47
|
cancelledDateTime: string
|
|
40
48
|
}): Promise<any> {
|
|
41
49
|
try {
|
|
@@ -82,6 +90,7 @@ export const setOrderCleanedBySequence = async function (this: WashdayClientInst
|
|
|
82
90
|
};
|
|
83
91
|
|
|
84
92
|
export const setOrderCollectedBySequence = async function (this: WashdayClientInstance, sequence: string, storeId: string, data: {
|
|
93
|
+
orderId?: string,
|
|
85
94
|
collectedDateTime: string;
|
|
86
95
|
collectWithAmountDue?: boolean,
|
|
87
96
|
pinUserId?: string
|
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
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { payAndCollect } from "../src/api/order/post";
|
|
2
|
+
import { setOrderCancelledBySequence } from "../src/api/order/put";
|
|
3
|
+
|
|
4
|
+
describe("orders orderId body fallback", () => {
|
|
5
|
+
it("sends orderId in payAndCollect body while keeping sequence/store route", async () => {
|
|
6
|
+
const post = jest.fn().mockResolvedValue({ data: { ok: true } });
|
|
7
|
+
const client = {
|
|
8
|
+
apiToken: "token-123",
|
|
9
|
+
axiosInstance: { post },
|
|
10
|
+
} as any;
|
|
11
|
+
|
|
12
|
+
await payAndCollect.call(client, {
|
|
13
|
+
orderId: "order-1",
|
|
14
|
+
sequence: "4",
|
|
15
|
+
storeId: "store-1",
|
|
16
|
+
paymentMethod: "card",
|
|
17
|
+
amount: 25,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(post).toHaveBeenCalledWith(
|
|
21
|
+
"/api/v2/order/4/store-1/pay-and-collect",
|
|
22
|
+
expect.objectContaining({ orderId: "order-1" }),
|
|
23
|
+
{
|
|
24
|
+
headers: { Authorization: "Bearer token-123" },
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("sends orderId in setOrderCancelledBySequence body", async () => {
|
|
30
|
+
const put = jest.fn().mockResolvedValue({ data: { data: true } });
|
|
31
|
+
const client = {
|
|
32
|
+
apiToken: "token-123",
|
|
33
|
+
axiosInstance: { put },
|
|
34
|
+
} as any;
|
|
35
|
+
|
|
36
|
+
await setOrderCancelledBySequence.call(client, "4", "store-1", {
|
|
37
|
+
orderId: "order-1",
|
|
38
|
+
cancelledDateTime: "2026-06-05T00:00:00.000Z",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(put).toHaveBeenCalledWith(
|
|
42
|
+
"api/order/4/store-1/cancelled",
|
|
43
|
+
expect.objectContaining({ orderId: "order-1" }),
|
|
44
|
+
{
|
|
45
|
+
headers: { Authorization: "Bearer token-123" },
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const repoRoot = path.resolve(__dirname, "../../..");
|
|
5
|
+
const workflowPath = path.join(repoRoot, ".github/workflows/sdk-publish.yml");
|
|
6
|
+
|
|
7
|
+
describe("sdk publish workflow", () => {
|
|
8
|
+
it("is manually runnable and bumps patch before publishing", () => {
|
|
9
|
+
expect(fs.existsSync(workflowPath)).toBe(true);
|
|
10
|
+
|
|
11
|
+
const workflow = fs.readFileSync(workflowPath, "utf8");
|
|
12
|
+
|
|
13
|
+
expect(workflow).toContain("workflow_dispatch:");
|
|
14
|
+
expect(workflow).not.toContain("push:");
|
|
15
|
+
expect(workflow).toContain("contents: write");
|
|
16
|
+
expect(workflow).toContain("id-token: write");
|
|
17
|
+
expect(workflow).toContain("working-directory: packages/sdk");
|
|
18
|
+
expect(workflow).toContain("registry-url: https://registry.npmjs.org");
|
|
19
|
+
expect(workflow).toContain('npm view "$PACKAGE_NAME@$CURRENT_VERSION" version');
|
|
20
|
+
expect(workflow).toContain("npm version patch --no-git-tag-version");
|
|
21
|
+
expect(workflow).toContain("git commit -m \"chore(sdk): bump patch version for publish\"");
|
|
22
|
+
expect(workflow).toContain("git push origin HEAD:main");
|
|
23
|
+
expect(workflow).toContain("npm publish --access public --no-provenance");
|
|
24
|
+
expect(workflow).not.toContain("NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}");
|
|
25
|
+
expect(workflow).not.toContain('NPM_CONFIG_PROVENANCE: "false"');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
name: Bump Version on PR
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
types: [opened, synchronize, reopened]
|
|
6
|
-
branches:
|
|
7
|
-
- main
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: write
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
bump-version:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
if: github.event.pull_request.base.ref == 'main'
|
|
16
|
-
steps:
|
|
17
|
-
- name: Checkout repository
|
|
18
|
-
uses: actions/checkout@v4
|
|
19
|
-
with:
|
|
20
|
-
ref: ${{ github.head_ref }}
|
|
21
|
-
token: ${{ secrets.GITHUB_TOKEN }}
|
|
22
|
-
|
|
23
|
-
- name: Use Node.js 24
|
|
24
|
-
uses: actions/setup-node@v4
|
|
25
|
-
with:
|
|
26
|
-
node-version: 24
|
|
27
|
-
|
|
28
|
-
- name: Configure git identity
|
|
29
|
-
run: |
|
|
30
|
-
git config --global user.name "washday-bot"
|
|
31
|
-
git config --global user.email "ci@washday.dev"
|
|
32
|
-
|
|
33
|
-
- name: Fetch main branch
|
|
34
|
-
run: git fetch origin main:main
|
|
35
|
-
|
|
36
|
-
- name: Check if version already bumped
|
|
37
|
-
id: check_version
|
|
38
|
-
run: |
|
|
39
|
-
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
|
40
|
-
git show main:package.json > /tmp/main-package.json
|
|
41
|
-
MAIN_VERSION=$(node -p "require('/tmp/main-package.json').version")
|
|
42
|
-
echo "Current PR version: $CURRENT_VERSION"
|
|
43
|
-
echo "Main branch version: $MAIN_VERSION"
|
|
44
|
-
if [ "$CURRENT_VERSION" != "$MAIN_VERSION" ]; then
|
|
45
|
-
echo "already_bumped=true" >> $GITHUB_OUTPUT
|
|
46
|
-
echo "✅ Version already bumped in this PR"
|
|
47
|
-
else
|
|
48
|
-
echo "already_bumped=false" >> $GITHUB_OUTPUT
|
|
49
|
-
echo "⚠️ Version needs bumping"
|
|
50
|
-
fi
|
|
51
|
-
|
|
52
|
-
- name: Bump version patch
|
|
53
|
-
if: steps.check_version.outputs.already_bumped == 'false'
|
|
54
|
-
run: |
|
|
55
|
-
npm version patch --no-git-tag-version
|
|
56
|
-
git add package.json package-lock.json
|
|
57
|
-
git commit -m "chore: bump version [skip ci]"
|
|
58
|
-
git push origin ${{ github.head_ref }}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
name: Publish SDK on Merge
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: read
|
|
10
|
-
id-token: write # Required for OIDC
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
publish:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
environment: publish
|
|
16
|
-
steps:
|
|
17
|
-
- name: Checkout
|
|
18
|
-
uses: actions/checkout@v4
|
|
19
|
-
|
|
20
|
-
- name: Use Node.js 24
|
|
21
|
-
uses: actions/setup-node@v4
|
|
22
|
-
with:
|
|
23
|
-
node-version: 24
|
|
24
|
-
|
|
25
|
-
- name: Install dependencies
|
|
26
|
-
run: npm ci
|
|
27
|
-
|
|
28
|
-
- name: Clear NODE_AUTH_TOKEN for OIDC
|
|
29
|
-
run: |
|
|
30
|
-
unset NODE_AUTH_TOKEN || true
|
|
31
|
-
|
|
32
|
-
- name: Build and publish
|
|
33
|
-
run: npm run build && npm publish --access public --no-provenance --verbose
|
|
34
|
-
env:
|
|
35
|
-
NODE_AUTH_TOKEN: ""
|
/package/{CLAUDE.md → AGENTS.md}
RENAMED
|
File without changes
|