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.
@@ -75,13 +75,27 @@ export const getAutomaticDiscountById = function (storeId, discountId) {
75
75
  }
76
76
  });
77
77
  };
78
- export const verifyDiscountCode = function (storeId, code) {
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
- return yield this.axiosInstance.get(`${GET_SET_DISCOUNT_CODES}/${storeId}/${code}/verify`, config);
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
- return yield this.axiosInstance.get(`${GET_DISCOUNT_CODES_CUSTOMER_APP}/${storeId}/${code}/verify`, config);
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(order, selectedCustomer, storeSettings, storeDiscounts, appliedOrderDiscounts, discountCodeObj);
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
- // === DISCOUNT CODE (monetario tipo NUMBER que se aplica una sola vez) ===
51
- let discountCodeAmount = 0;
52
- if (discountCodeObj &&
53
- discountCodeObj.type === DiscountCodeTypes.NUMBER &&
54
- discountCodeObj.applyOnceOnOrder) {
55
- const includesProducts = discountCodeObj.applyToAllProducts ||
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 : (_a = selectedCustomer === null || selectedCustomer === void 0 ? void 0 : selectedCustomer.customer) === null || _a === void 0 ? void 0 : _a.discount, productTotal: totalImportWithDiscount, taxesTotal: totalImporTaxes, taxBreakdown, total: orderTotal, creditApplied, redeemPointsApplied: redeemPointsDiscount, productTotalWithoutDiscount: totalImportWithoutDiscount, totalDiscountAmount: totalDiscountAmount + discountCodeAmount, shippingServiceTotal: shippingCost, appliedOrderDiscounts, subtotal: totalSubtotalAmount });
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 sobre el precio ajustado
96
- const discountAmountPerUnit = discPercentageInteger ? adjustedUnitNetPrice * discPercentageInteger : 0;
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
- const lineDiscount = +(discountAmountPerUnit * qty).toFixed(2);
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
- const productDiscount = adjustedPrice * discPercentageInteger;
86
- const discountedAdjustedPrice = adjustedPrice - productDiscount;
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 discountAmount = discountCode.value;
279
- if (!discountCode.applyOnceOnOrder) {
280
- if (discountCode.applyToAllProducts) {
281
- newOrderProds = newOrderProds.map((prod) => (Object.assign(Object.assign({}, prod), { discountAmount: discountAmount })));
282
- }
283
- else {
284
- const discountProductIds = getTargetProductIdSet(discountCode.products);
285
- newOrderProds = newOrderProds.map((prod) => {
286
- if (!discountProductIds.has(getProductIdentity(prod))) {
287
- return prod;
288
- }
289
- return Object.assign(Object.assign({}, prod), { discountAmount });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "washday-sdk",
3
- "version": "1.6.69",
3
+ "version": "1.6.71",
4
4
  "description": "Washday utilities functions and API",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -63,24 +63,50 @@ export const getAutomaticDiscountById = async function (this: WashdayClientInsta
63
63
  }
64
64
  };
65
65
 
66
- export const verifyDiscountCode = async function (this: WashdayClientInstance, storeId: string, code: string,): Promise<any> {
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
- return await this.axiosInstance.get(`${GET_SET_DISCOUNT_CODES}/${storeId}/${code}/verify`, config);
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 (this: WashdayClientInstance, storeId: string, code: string,): Promise<any> {
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
- return await this.axiosInstance.get(`${GET_DISCOUNT_CODES_CUSTOMER_APP}/${storeId}/${code}/verify`, config);
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;
@@ -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 = {
@@ -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> {
@@ -422,6 +422,7 @@ export interface IDiscountCode {
422
422
  createdBy: IUser | string,
423
423
  isActive: boolean,
424
424
  applyOnceOnOrder: boolean,
425
+ minimumOrderSubtotal?: number,
425
426
  applyToAllProducts: boolean,
426
427
  hasUseLimit: boolean,
427
428
  useLimitQty: number,
@@ -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 { getCreditApplied, getProductLineTotals, getShippingCost } from "./helpers";
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
- order,
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
- // === DISCOUNT CODE (monetario tipo NUMBER que se aplica una sola vez) ===
83
- let discountCodeAmount = 0;
84
- if (
85
- discountCodeObj &&
86
- discountCodeObj.type === DiscountCodeTypes.NUMBER &&
87
- discountCodeObj.applyOnceOnOrder
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 + discountCodeAmount,
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
- // Calcular el descuento sobre el precio ajustado
111
- const discountAmountPerUnit = discPercentageInteger ? adjustedUnitNetPrice * discPercentageInteger : 0;
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
- const lineDiscount = +(discountAmountPerUnit * qty).toFixed(2);
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
- const productDiscount = adjustedPrice * discPercentageInteger;
99
- const discountedAdjustedPrice = adjustedPrice - productDiscount;
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;