washday-sdk 1.6.69 → 1.6.71
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 +24 -15
- package/dist/utils/orders/calculateTotalTaxesIncluded.js +22 -4
- package/dist/utils/orders/calculateTotalTaxesOverPrice.js +19 -3
- package/dist/utils/orders/helpers.js +104 -13
- 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 +40 -20
- package/src/utils/orders/calculateTotalTaxesIncluded.ts +25 -4
- package/src/utils/orders/calculateTotalTaxesOverPrice.ts +19 -3
- package/src/utils/orders/helpers.ts +114 -12
- package/test/orders.allocator.test.ts +58 -0
- package/test/orders.discountCodeNumberPreTax.test.ts +217 -0
- package/test/orders.discountCodeOnceOnOrderAndMinimum.test.ts +293 -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);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DiscountCodeTypes } from "../../enum";
|
|
2
2
|
import { calculateTotalTaxesIncluded } from "./calculateTotalTaxesIncluded";
|
|
3
3
|
import { calculateTotalTaxesOverPrice } from "./calculateTotalTaxesOverPrice";
|
|
4
|
-
import { getCreditApplied, getProductLineTotals, getShippingCost } from "./helpers";
|
|
4
|
+
import { applyDiscountToProducts, getCreditApplied, getProductLineTotals, getShippingCost, } from "./helpers";
|
|
5
5
|
const getNormalizedId = (value) => {
|
|
6
6
|
if (value === null || value === undefined) {
|
|
7
7
|
return "";
|
|
@@ -38,37 +38,46 @@ 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
|
+
// For NUMBER coupons (fixed-amount) we must bake `discountAmount` per
|
|
45
|
+
// line so the tax calculators can reduce the taxable base BEFORE
|
|
46
|
+
// computing tax. `applyDiscountToProducts` handles both per-unit
|
|
47
|
+
// (`applyOnceOnOrder=false`) and proportional allocation
|
|
48
|
+
// (`applyOnceOnOrder=true`). For other coupon types, the tax calc derives
|
|
49
|
+
// the discount via `discPercentageInteger` so we can skip the pre-pass.
|
|
50
|
+
let processedOrder = order;
|
|
51
|
+
if (discountCodeObj && discountCodeObj.type === DiscountCodeTypes.NUMBER) {
|
|
52
|
+
const { newOrderProds, buyAndGetProds } = applyDiscountToProducts(discountCodeObj, (_a = order.products) !== null && _a !== void 0 ? _a : [], Boolean(order.express), storeDiscounts, selectedCustomer === null || selectedCustomer === void 0 ? void 0 : selectedCustomer.customer);
|
|
53
|
+
processedOrder = Object.assign(Object.assign({}, order), { products: newOrderProds, buyAndGetProducts: buyAndGetProds && buyAndGetProds.length > 0
|
|
54
|
+
? buyAndGetProds
|
|
55
|
+
: (_b = order.buyAndGetProducts) !== null && _b !== void 0 ? _b : [] });
|
|
56
|
+
}
|
|
44
57
|
const productTableCalculator = (storeSettings === null || storeSettings === void 0 ? void 0 : storeSettings.taxesType) === 'over_price'
|
|
45
58
|
? calculateTotalTaxesOverPrice
|
|
46
59
|
: calculateTotalTaxesIncluded;
|
|
47
|
-
const productTableImports = productTableCalculator(
|
|
60
|
+
const productTableImports = productTableCalculator(processedOrder, selectedCustomer, storeSettings, storeDiscounts, appliedOrderDiscounts, discountCodeObj);
|
|
48
61
|
// === PRODUCT LINE TOTALS ===
|
|
49
62
|
const { totalQuantity, totalImportWithDiscount, totalImportWithoutDiscount, totalImporTaxes, totalDiscountAmount, totalSubtotalAmount, taxBreakdown } = getProductLineTotals(productTableImports);
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
order.products.some((curr) => hasMatchingDiscountTarget(discountCodeObj.products, curr));
|
|
57
|
-
discountCodeAmount = includesProducts ? discountCodeObj.value : 0;
|
|
58
|
-
}
|
|
63
|
+
// NOTE: the old post-tax "discountCodeAmount" subtraction was removed.
|
|
64
|
+
// NUMBER coupons (both `applyOnceOnOrder=true` and `=false`) are now
|
|
65
|
+
// applied pre-tax via per-line `discountAmount` baked above, so
|
|
66
|
+
// `totalImportWithDiscount` already reflects the coupon AND its tax
|
|
67
|
+
// reduction effect. `totalDiscountAmount` from the line totals is the
|
|
68
|
+
// nominal coupon value (user-facing "Descuento").
|
|
59
69
|
// === SHIPPING COST ===
|
|
60
70
|
const shippingCost = getShippingCost(discountCodeObj, hasShippingCost, storeSettings);
|
|
61
71
|
// === TOTAL ANTES DE CRÉDITOS/PUNTOS ===
|
|
62
72
|
const orderTotalWithOutCredit = +(totalImportWithDiscount +
|
|
63
|
-
shippingCost
|
|
64
|
-
discountCodeAmount).toFixed(2);
|
|
73
|
+
shippingCost).toFixed(2);
|
|
65
74
|
// === APLICAR CRÉDITO Y PUNTOS REDIMIDOS ===
|
|
66
75
|
const creditApplied = getCreditApplied(selectedCustomer, orderTotalWithOutCredit);
|
|
67
76
|
const orderTotal = +(orderTotalWithOutCredit -
|
|
68
77
|
creditApplied -
|
|
69
78
|
redeemPointsDiscount).toFixed(2);
|
|
70
79
|
// === RETURN FINAL OBJECT ===
|
|
71
|
-
return Object.assign(Object.assign({}, order), { totalQuantity, customerDiscount: order.discountCode ? 0 : (
|
|
80
|
+
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, shippingServiceTotal: shippingCost, appliedOrderDiscounts, subtotal: totalSubtotalAmount });
|
|
72
81
|
}
|
|
73
82
|
catch (error) {
|
|
74
83
|
throw error;
|
|
@@ -92,10 +92,23 @@ export const calculateTotalTaxesIncluded = (order, selectedCustomer, storeSettin
|
|
|
92
92
|
const unitExtraNet = qty > 0 ? extraAmountNet / qty : 0;
|
|
93
93
|
// Precio neto ajustado: precio base + extra (por unidad)
|
|
94
94
|
const adjustedUnitNetPrice = unitNetPrice + unitExtraNet;
|
|
95
|
-
// Calcular el descuento
|
|
96
|
-
|
|
95
|
+
// Calcular el descuento por unidad.
|
|
96
|
+
// - PERCENTAGE/BUY_X_GET_Y/FREE_ITEM: derive from `discPercentageInteger`
|
|
97
|
+
// (already applied as percentage on adjusted net unit price).
|
|
98
|
+
// - NUMBER (fixed): `current.discountAmount` is the per-line GROSS
|
|
99
|
+
// discount in pesos, baked by `applyDiscountToProducts`. Convert to
|
|
100
|
+
// per-unit net so the existing pipeline computes tax post-discount.
|
|
101
|
+
let discountAmountPerUnit = discPercentageInteger ? adjustedUnitNetPrice * discPercentageInteger : 0;
|
|
102
|
+
const isFixedNumberCoupon = !!discountCodeObj &&
|
|
103
|
+
discountCodeObj.type === DiscountCodeTypes.NUMBER &&
|
|
104
|
+
Number(current.discountAmount) > 0;
|
|
105
|
+
if (isFixedNumberCoupon && qty > 0) {
|
|
106
|
+
const perUnitGrossDiscount = Number(current.discountAmount) / qty;
|
|
107
|
+
const perUnitNetDiscount = perUnitGrossDiscount / taxFactor;
|
|
108
|
+
discountAmountPerUnit = Math.min(adjustedUnitNetPrice, perUnitNetDiscount);
|
|
109
|
+
}
|
|
97
110
|
// Precio neto con descuento aplicado por unidad
|
|
98
|
-
const discountedUnitNetPrice = adjustedUnitNetPrice - discountAmountPerUnit;
|
|
111
|
+
const discountedUnitNetPrice = Math.max(0, adjustedUnitNetPrice - discountAmountPerUnit);
|
|
99
112
|
// === Totales de la línea ===
|
|
100
113
|
// Total neto sin descuento: precio base total + extra (ya prorrateado en el total)
|
|
101
114
|
const productNetTotalWithoutDiscount = (unitNetPrice * qty) + extraAmountNet;
|
|
@@ -111,7 +124,12 @@ export const calculateTotalTaxesIncluded = (order, selectedCustomer, storeSettin
|
|
|
111
124
|
store: storeSettings,
|
|
112
125
|
lineTaxAmount: totalTaxesApplied,
|
|
113
126
|
});
|
|
114
|
-
|
|
127
|
+
// For NUMBER coupons the user-facing "Descuento" is the gross slice
|
|
128
|
+
// (which is what was baked into `current.discountAmount`). For other
|
|
129
|
+
// discount types, line discount = per-unit net × qty (existing).
|
|
130
|
+
const lineDiscount = isFixedNumberCoupon
|
|
131
|
+
? Number(current.discountAmount)
|
|
132
|
+
: +(discountAmountPerUnit * qty).toFixed(2);
|
|
115
133
|
return {
|
|
116
134
|
product: current,
|
|
117
135
|
qty,
|
|
@@ -68,6 +68,12 @@ export const calculateTotalTaxesOverPrice = (order, selectedCustomer, storeSetti
|
|
|
68
68
|
: 0;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
else if (discountCodeObj.type === DiscountCodeTypes.NUMBER) {
|
|
72
|
+
// Fixed-amount discount baked by `applyDiscountToProducts` as
|
|
73
|
+
// `current.discountAmount` (line-total, NET pesos in over_price).
|
|
74
|
+
// Per-unit treatment: derive `productDiscount = discountAmount / qty`
|
|
75
|
+
// so the existing pipeline below subtracts pre-tax.
|
|
76
|
+
}
|
|
71
77
|
else if (discountCodeObj.type === DiscountCodeTypes.BUY_X_GET_Y) {
|
|
72
78
|
const condition = discountCodeObj.buyAndGetConditions[0];
|
|
73
79
|
if (condition.getDiscountType === BuyAndGetConditionsTypes.PERCENTAGE && current.isBuyAndGetProduct) {
|
|
@@ -81,9 +87,19 @@ export const calculateTotalTaxesOverPrice = (order, selectedCustomer, storeSetti
|
|
|
81
87
|
discPercentageInteger = 1;
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
|
-
// Calcular el descuento por unidad sobre el precio ajustado
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
// Calcular el descuento por unidad sobre el precio ajustado.
|
|
91
|
+
// - PERCENTAGE/BUY_X_GET_Y/FREE_ITEM: derive from `discPercentageInteger`.
|
|
92
|
+
// - NUMBER (fixed): `current.discountAmount` is the per-line NET discount
|
|
93
|
+
// in pesos, baked by `applyDiscountToProducts`. Convert to per-unit.
|
|
94
|
+
let productDiscount = adjustedPrice * discPercentageInteger;
|
|
95
|
+
const isFixedNumberCoupon = !!discountCodeObj &&
|
|
96
|
+
discountCodeObj.type === DiscountCodeTypes.NUMBER &&
|
|
97
|
+
Number(current.discountAmount) > 0;
|
|
98
|
+
if (isFixedNumberCoupon && qty > 0) {
|
|
99
|
+
const perUnitDiscount = Number(current.discountAmount) / qty;
|
|
100
|
+
productDiscount = Math.min(adjustedPrice, perUnitDiscount);
|
|
101
|
+
}
|
|
102
|
+
const discountedAdjustedPrice = Math.max(0, adjustedPrice - productDiscount);
|
|
87
103
|
// Subtotal para la línea (precio descontado por unidad * cantidad) SIN impuestos
|
|
88
104
|
const subtotal = discountedAdjustedPrice * qty;
|
|
89
105
|
// Calcular impuestos sobre el subtotal
|
|
@@ -110,7 +110,7 @@ const getApplicableTaxesForProduct = (productObj, store) => {
|
|
|
110
110
|
return acc;
|
|
111
111
|
}, []);
|
|
112
112
|
};
|
|
113
|
-
const allocateByWeights = (total, weights) => {
|
|
113
|
+
export const allocateByWeights = (total, weights) => {
|
|
114
114
|
if (weights.length === 0)
|
|
115
115
|
return [];
|
|
116
116
|
if (total <= 0)
|
|
@@ -132,6 +132,43 @@ const allocateByWeights = (total, weights) => {
|
|
|
132
132
|
}
|
|
133
133
|
return out;
|
|
134
134
|
};
|
|
135
|
+
// Allocate a fixed order-level discount across eligible line bases (in cents).
|
|
136
|
+
// Each entry is capped at its corresponding base. Overflow caused by the cap
|
|
137
|
+
// is rerouted to the next-largest base. The result sums to
|
|
138
|
+
// min(totalCents, sum(baseCents)).
|
|
139
|
+
export const allocateFixedDiscountAcrossLines = (totalCents, baseCents) => {
|
|
140
|
+
const len = baseCents.length;
|
|
141
|
+
if (len === 0)
|
|
142
|
+
return [];
|
|
143
|
+
const safeBases = baseCents.map((b) => Math.max(0, Math.trunc(b)));
|
|
144
|
+
const sumBases = safeBases.reduce((a, b) => a + b, 0);
|
|
145
|
+
if (sumBases <= 0 || totalCents <= 0) {
|
|
146
|
+
return new Array(len).fill(0);
|
|
147
|
+
}
|
|
148
|
+
const cappedTotal = Math.min(Math.trunc(totalCents), sumBases);
|
|
149
|
+
const slices = allocateByWeights(cappedTotal, safeBases);
|
|
150
|
+
let overflow = 0;
|
|
151
|
+
for (let i = 0; i < len; i += 1) {
|
|
152
|
+
if (slices[i] > safeBases[i]) {
|
|
153
|
+
overflow += slices[i] - safeBases[i];
|
|
154
|
+
slices[i] = safeBases[i];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (overflow === 0)
|
|
158
|
+
return slices;
|
|
159
|
+
const headroomOrder = safeBases
|
|
160
|
+
.map((b, i) => ({ i, headroom: b - slices[i] }))
|
|
161
|
+
.filter((entry) => entry.headroom > 0)
|
|
162
|
+
.sort((a, b) => b.headroom - a.headroom);
|
|
163
|
+
for (const entry of headroomOrder) {
|
|
164
|
+
if (overflow <= 0)
|
|
165
|
+
break;
|
|
166
|
+
const take = Math.min(entry.headroom, overflow);
|
|
167
|
+
slices[entry.i] += take;
|
|
168
|
+
overflow -= take;
|
|
169
|
+
}
|
|
170
|
+
return slices;
|
|
171
|
+
};
|
|
135
172
|
export const aggregateTaxBreakdown = (items) => {
|
|
136
173
|
const byKey = new Map();
|
|
137
174
|
items.forEach((item) => {
|
|
@@ -225,7 +262,28 @@ export const getCreditApplied = (selectedCustomer, orderTotal) => {
|
|
|
225
262
|
}
|
|
226
263
|
return Math.min(customerCredit, orderTotal);
|
|
227
264
|
};
|
|
265
|
+
const productsMeetMinimumSubtotal = (productsArr, isExpress, minimumOrderSubtotal) => {
|
|
266
|
+
if (!Number.isFinite(minimumOrderSubtotal) || minimumOrderSubtotal <= 0)
|
|
267
|
+
return true;
|
|
268
|
+
const subtotal = (productsArr || []).reduce((sum, prod) => {
|
|
269
|
+
var _a, _b, _c, _d, _e;
|
|
270
|
+
if (!prod || prod.isBuyAndGetProduct || prod.isFreeItem)
|
|
271
|
+
return sum;
|
|
272
|
+
const price = Number((_b = (_a = (isExpress ? prod.expressPrice : prod.price)) !== null && _a !== void 0 ? _a : prod.price) !== null && _b !== void 0 ? _b : 0);
|
|
273
|
+
const qty = Number((_d = (_c = prod.quantity) !== null && _c !== void 0 ? _c : prod.qty) !== null && _d !== void 0 ? _d : 0);
|
|
274
|
+
const extra = Number((_e = prod.extraAmount) !== null && _e !== void 0 ? _e : 0);
|
|
275
|
+
if (!Number.isFinite(price) || !Number.isFinite(qty))
|
|
276
|
+
return sum;
|
|
277
|
+
return sum + price * qty + (Number.isFinite(extra) ? extra : 0);
|
|
278
|
+
}, 0);
|
|
279
|
+
return subtotal >= minimumOrderSubtotal;
|
|
280
|
+
};
|
|
228
281
|
export const applyDiscountToProducts = (discountCode, productsArr, isExpress, storeDiscounts = [], selectedCustomer = { discount: 0 }) => {
|
|
282
|
+
var _a;
|
|
283
|
+
if (discountCode &&
|
|
284
|
+
!productsMeetMinimumSubtotal(productsArr, isExpress, Number((_a = discountCode.minimumOrderSubtotal) !== null && _a !== void 0 ? _a : 0))) {
|
|
285
|
+
discountCode = null;
|
|
286
|
+
}
|
|
229
287
|
if (!discountCode) {
|
|
230
288
|
// Caso sin discountCode: se aplican descuentos automáticos solo sobre el precio del producto
|
|
231
289
|
return {
|
|
@@ -275,21 +333,54 @@ export const applyDiscountToProducts = (discountCode, productsArr, isExpress, st
|
|
|
275
333
|
}
|
|
276
334
|
}
|
|
277
335
|
if (discountCode && discountCode.type === DiscountCodeTypes.NUMBER) {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
336
|
+
const couponValue = Number(discountCode.value) || 0;
|
|
337
|
+
const eligibilityIds = discountCode.applyToAllProducts
|
|
338
|
+
? null
|
|
339
|
+
: getTargetProductIdSet(discountCode.products);
|
|
340
|
+
const isEligibleProd = (prod) => eligibilityIds === null || eligibilityIds.has(getProductIdentity(prod));
|
|
341
|
+
if (discountCode.applyOnceOnOrder) {
|
|
342
|
+
// applyOnceOnOrder=true: allocate the fixed amount proportionally
|
|
343
|
+
// across eligible lines so each line carries its share as a pre-tax
|
|
344
|
+
// discountAmount. The downstream tax calc reads this and reduces the
|
|
345
|
+
// taxable base before computing tax.
|
|
346
|
+
const eligibleEntries = [];
|
|
347
|
+
newOrderProds.forEach((prod, idx) => {
|
|
348
|
+
var _a, _b;
|
|
349
|
+
if (!isEligibleProd(prod))
|
|
350
|
+
return;
|
|
351
|
+
const prodPrice = isExpress
|
|
352
|
+
? (_a = prod.expressPrice) !== null && _a !== void 0 ? _a : prod.price
|
|
353
|
+
: prod.price;
|
|
354
|
+
const qty = Number((_b = prod.qty) !== null && _b !== void 0 ? _b : prod.quantity) || 0;
|
|
355
|
+
const extra = Number(prod.extraAmount) || 0;
|
|
356
|
+
const baseCents = Math.round(((Number(prodPrice) || 0) * qty + extra) * 100);
|
|
357
|
+
if (baseCents > 0) {
|
|
358
|
+
eligibleEntries.push({ idx, baseCents });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
if (eligibleEntries.length > 0) {
|
|
362
|
+
const couponCents = Math.max(0, Math.round(couponValue * 100));
|
|
363
|
+
const sliceCents = allocateFixedDiscountAcrossLines(couponCents, eligibleEntries.map((e) => e.baseCents));
|
|
364
|
+
const newProdsCopy = newOrderProds.slice();
|
|
365
|
+
eligibleEntries.forEach((e, i) => {
|
|
366
|
+
const slicePesos = +(sliceCents[i] / 100).toFixed(2);
|
|
367
|
+
newProdsCopy[e.idx] = Object.assign(Object.assign({}, newProdsCopy[e.idx]), { discountAmount: slicePesos });
|
|
290
368
|
});
|
|
369
|
+
newOrderProds = newProdsCopy;
|
|
291
370
|
}
|
|
292
371
|
}
|
|
372
|
+
else {
|
|
373
|
+
// applyOnceOnOrder=false: per-unit fixed discount. Total per eligible
|
|
374
|
+
// line is value * qty (was a flat `value` previously — silent bug).
|
|
375
|
+
newOrderProds = newOrderProds.map((prod) => {
|
|
376
|
+
var _a;
|
|
377
|
+
if (!isEligibleProd(prod))
|
|
378
|
+
return prod;
|
|
379
|
+
const qty = Number((_a = prod.qty) !== null && _a !== void 0 ? _a : prod.quantity) || 0;
|
|
380
|
+
const lineDiscount = +(couponValue * qty).toFixed(2);
|
|
381
|
+
return Object.assign(Object.assign({}, prod), { discountAmount: lineDiscount });
|
|
382
|
+
});
|
|
383
|
+
}
|
|
293
384
|
}
|
|
294
385
|
if (discountCode.buyAndGetConditions && discountCode.type === DiscountCodeTypes.BUY_X_GET_Y) {
|
|
295
386
|
const buyConditions = discountCode.buyAndGetConditions[0].buyConditions;
|
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
|
@@ -3,7 +3,12 @@ import { ICustomer } from "../../interfaces/Customer";
|
|
|
3
3
|
import { IStore } from "../../interfaces/Store";
|
|
4
4
|
import { calculateTotalTaxesIncluded } from "./calculateTotalTaxesIncluded";
|
|
5
5
|
import { calculateTotalTaxesOverPrice } from "./calculateTotalTaxesOverPrice";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
applyDiscountToProducts,
|
|
8
|
+
getCreditApplied,
|
|
9
|
+
getProductLineTotals,
|
|
10
|
+
getShippingCost,
|
|
11
|
+
} from "./helpers";
|
|
7
12
|
|
|
8
13
|
const getNormalizedId = (value: any): string => {
|
|
9
14
|
if (value === null || value === undefined) {
|
|
@@ -54,13 +59,38 @@ export const calculateOrderTotal = (
|
|
|
54
59
|
try {
|
|
55
60
|
const appliedOrderDiscounts: any = {};
|
|
56
61
|
|
|
62
|
+
// For NUMBER coupons (fixed-amount) we must bake `discountAmount` per
|
|
63
|
+
// line so the tax calculators can reduce the taxable base BEFORE
|
|
64
|
+
// computing tax. `applyDiscountToProducts` handles both per-unit
|
|
65
|
+
// (`applyOnceOnOrder=false`) and proportional allocation
|
|
66
|
+
// (`applyOnceOnOrder=true`). For other coupon types, the tax calc derives
|
|
67
|
+
// the discount via `discPercentageInteger` so we can skip the pre-pass.
|
|
68
|
+
let processedOrder = order;
|
|
69
|
+
if (discountCodeObj && discountCodeObj.type === DiscountCodeTypes.NUMBER) {
|
|
70
|
+
const { newOrderProds, buyAndGetProds } = applyDiscountToProducts(
|
|
71
|
+
discountCodeObj,
|
|
72
|
+
order.products ?? [],
|
|
73
|
+
Boolean(order.express),
|
|
74
|
+
storeDiscounts,
|
|
75
|
+
selectedCustomer?.customer
|
|
76
|
+
);
|
|
77
|
+
processedOrder = {
|
|
78
|
+
...order,
|
|
79
|
+
products: newOrderProds,
|
|
80
|
+
buyAndGetProducts:
|
|
81
|
+
buyAndGetProds && buyAndGetProds.length > 0
|
|
82
|
+
? buyAndGetProds
|
|
83
|
+
: order.buyAndGetProducts ?? [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
const productTableCalculator =
|
|
58
88
|
storeSettings?.taxesType === 'over_price'
|
|
59
89
|
? calculateTotalTaxesOverPrice
|
|
60
90
|
: calculateTotalTaxesIncluded;
|
|
61
91
|
|
|
62
92
|
const productTableImports = productTableCalculator(
|
|
63
|
-
|
|
93
|
+
processedOrder,
|
|
64
94
|
selectedCustomer,
|
|
65
95
|
storeSettings,
|
|
66
96
|
storeDiscounts,
|
|
@@ -79,21 +109,12 @@ export const calculateOrderTotal = (
|
|
|
79
109
|
taxBreakdown
|
|
80
110
|
} = getProductLineTotals(productTableImports);
|
|
81
111
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
) {
|
|
89
|
-
const includesProducts =
|
|
90
|
-
discountCodeObj.applyToAllProducts ||
|
|
91
|
-
order.products.some((curr: any) =>
|
|
92
|
-
hasMatchingDiscountTarget(discountCodeObj.products, curr)
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
discountCodeAmount = includesProducts ? discountCodeObj.value : 0;
|
|
96
|
-
}
|
|
112
|
+
// NOTE: the old post-tax "discountCodeAmount" subtraction was removed.
|
|
113
|
+
// NUMBER coupons (both `applyOnceOnOrder=true` and `=false`) are now
|
|
114
|
+
// applied pre-tax via per-line `discountAmount` baked above, so
|
|
115
|
+
// `totalImportWithDiscount` already reflects the coupon AND its tax
|
|
116
|
+
// reduction effect. `totalDiscountAmount` from the line totals is the
|
|
117
|
+
// nominal coupon value (user-facing "Descuento").
|
|
97
118
|
|
|
98
119
|
// === SHIPPING COST ===
|
|
99
120
|
const shippingCost = getShippingCost(
|
|
@@ -105,8 +126,7 @@ export const calculateOrderTotal = (
|
|
|
105
126
|
// === TOTAL ANTES DE CRÉDITOS/PUNTOS ===
|
|
106
127
|
const orderTotalWithOutCredit = +(
|
|
107
128
|
totalImportWithDiscount +
|
|
108
|
-
shippingCost
|
|
109
|
-
discountCodeAmount
|
|
129
|
+
shippingCost
|
|
110
130
|
).toFixed(2);
|
|
111
131
|
|
|
112
132
|
// === APLICAR CRÉDITO Y PUNTOS REDIMIDOS ===
|
|
@@ -133,7 +153,7 @@ export const calculateOrderTotal = (
|
|
|
133
153
|
creditApplied,
|
|
134
154
|
redeemPointsApplied: redeemPointsDiscount, // ✅ NUEVO CAMPO
|
|
135
155
|
productTotalWithoutDiscount: totalImportWithoutDiscount,
|
|
136
|
-
totalDiscountAmount: totalDiscountAmount
|
|
156
|
+
totalDiscountAmount: totalDiscountAmount,
|
|
137
157
|
shippingServiceTotal: shippingCost,
|
|
138
158
|
appliedOrderDiscounts,
|
|
139
159
|
subtotal: totalSubtotalAmount
|
|
@@ -107,10 +107,26 @@ export const calculateTotalTaxesIncluded = (
|
|
|
107
107
|
const unitExtraNet = qty > 0 ? extraAmountNet / qty : 0;
|
|
108
108
|
// Precio neto ajustado: precio base + extra (por unidad)
|
|
109
109
|
const adjustedUnitNetPrice = unitNetPrice + unitExtraNet;
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
|
|
111
|
+
// Calcular el descuento por unidad.
|
|
112
|
+
// - PERCENTAGE/BUY_X_GET_Y/FREE_ITEM: derive from `discPercentageInteger`
|
|
113
|
+
// (already applied as percentage on adjusted net unit price).
|
|
114
|
+
// - NUMBER (fixed): `current.discountAmount` is the per-line GROSS
|
|
115
|
+
// discount in pesos, baked by `applyDiscountToProducts`. Convert to
|
|
116
|
+
// per-unit net so the existing pipeline computes tax post-discount.
|
|
117
|
+
let discountAmountPerUnit = discPercentageInteger ? adjustedUnitNetPrice * discPercentageInteger : 0;
|
|
118
|
+
const isFixedNumberCoupon =
|
|
119
|
+
!!discountCodeObj &&
|
|
120
|
+
discountCodeObj.type === DiscountCodeTypes.NUMBER &&
|
|
121
|
+
Number(current.discountAmount) > 0;
|
|
122
|
+
if (isFixedNumberCoupon && qty > 0) {
|
|
123
|
+
const perUnitGrossDiscount = Number(current.discountAmount) / qty;
|
|
124
|
+
const perUnitNetDiscount = perUnitGrossDiscount / taxFactor;
|
|
125
|
+
discountAmountPerUnit = Math.min(adjustedUnitNetPrice, perUnitNetDiscount);
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
// Precio neto con descuento aplicado por unidad
|
|
113
|
-
const discountedUnitNetPrice = adjustedUnitNetPrice - discountAmountPerUnit;
|
|
129
|
+
const discountedUnitNetPrice = Math.max(0, adjustedUnitNetPrice - discountAmountPerUnit);
|
|
114
130
|
|
|
115
131
|
// === Totales de la línea ===
|
|
116
132
|
// Total neto sin descuento: precio base total + extra (ya prorrateado en el total)
|
|
@@ -130,7 +146,12 @@ export const calculateTotalTaxesIncluded = (
|
|
|
130
146
|
lineTaxAmount: totalTaxesApplied,
|
|
131
147
|
});
|
|
132
148
|
|
|
133
|
-
|
|
149
|
+
// For NUMBER coupons the user-facing "Descuento" is the gross slice
|
|
150
|
+
// (which is what was baked into `current.discountAmount`). For other
|
|
151
|
+
// discount types, line discount = per-unit net × qty (existing).
|
|
152
|
+
const lineDiscount = isFixedNumberCoupon
|
|
153
|
+
? Number(current.discountAmount)
|
|
154
|
+
: +(discountAmountPerUnit * qty).toFixed(2);
|
|
134
155
|
|
|
135
156
|
return {
|
|
136
157
|
product: current,
|
|
@@ -81,6 +81,11 @@ export const calculateTotalTaxesOverPrice = (
|
|
|
81
81
|
? +(discountCodeObj.value / 100).toFixed(2)
|
|
82
82
|
: 0;
|
|
83
83
|
}
|
|
84
|
+
} else if (discountCodeObj.type === DiscountCodeTypes.NUMBER) {
|
|
85
|
+
// Fixed-amount discount baked by `applyDiscountToProducts` as
|
|
86
|
+
// `current.discountAmount` (line-total, NET pesos in over_price).
|
|
87
|
+
// Per-unit treatment: derive `productDiscount = discountAmount / qty`
|
|
88
|
+
// so the existing pipeline below subtracts pre-tax.
|
|
84
89
|
} else if (discountCodeObj.type === DiscountCodeTypes.BUY_X_GET_Y) {
|
|
85
90
|
const condition = discountCodeObj.buyAndGetConditions[0];
|
|
86
91
|
if (condition.getDiscountType === BuyAndGetConditionsTypes.PERCENTAGE && current.isBuyAndGetProduct) {
|
|
@@ -94,9 +99,20 @@ export const calculateTotalTaxesOverPrice = (
|
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
// Calcular el descuento por unidad sobre el precio ajustado
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
// Calcular el descuento por unidad sobre el precio ajustado.
|
|
103
|
+
// - PERCENTAGE/BUY_X_GET_Y/FREE_ITEM: derive from `discPercentageInteger`.
|
|
104
|
+
// - NUMBER (fixed): `current.discountAmount` is the per-line NET discount
|
|
105
|
+
// in pesos, baked by `applyDiscountToProducts`. Convert to per-unit.
|
|
106
|
+
let productDiscount = adjustedPrice * discPercentageInteger;
|
|
107
|
+
const isFixedNumberCoupon =
|
|
108
|
+
!!discountCodeObj &&
|
|
109
|
+
discountCodeObj.type === DiscountCodeTypes.NUMBER &&
|
|
110
|
+
Number(current.discountAmount) > 0;
|
|
111
|
+
if (isFixedNumberCoupon && qty > 0) {
|
|
112
|
+
const perUnitDiscount = Number(current.discountAmount) / qty;
|
|
113
|
+
productDiscount = Math.min(adjustedPrice, perUnitDiscount);
|
|
114
|
+
}
|
|
115
|
+
const discountedAdjustedPrice = Math.max(0, adjustedPrice - productDiscount);
|
|
100
116
|
|
|
101
117
|
// Subtotal para la línea (precio descontado por unidad * cantidad) SIN impuestos
|
|
102
118
|
const subtotal = discountedAdjustedPrice * qty;
|