washday-sdk 1.6.64 → 1.6.66

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/index.js CHANGED
@@ -154,6 +154,7 @@ const WashdayClient = function WashdayClient(apiToken, env = 'PROD', clientId, c
154
154
  sendOrderUncollectedCustomerNotification: ordersEndpoints.postModule.sendOrderUncollectedCustomerNotification,
155
155
  recordFailedServiceAttempt: ordersEndpoints.postModule.recordFailedServiceAttempt,
156
156
  deletePaymentLineById: ordersEndpoints.deleteModule.deletePaymentLineById,
157
+ deleteOrderEvidence: ordersEndpoints.deleteModule.deleteOrderEvidence,
157
158
  getListCustomersApp: ordersEndpoints.getModule.getListCustomersApp,
158
159
  getByIdCustomersApp: ordersEndpoints.getModule.getByIdCustomersApp,
159
160
  setOrderAcceptedBySequence: ordersEndpoints.putModule.setOrderAcceptedBySequence,
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  const GET_SET_ORDER_PAYMENTLINES = (orderId) => `/api/v2/order/${orderId}/paymentLines`;
11
+ const GET_SET_ORDER = 'api/v2/order';
11
12
  const GET_SET_ORDER_CUSTOMERS_APP = 'api/v2/washdayapp/orders';
12
13
  export const deletePaymentLineById = function (orderId, id) {
13
14
  return __awaiter(this, void 0, void 0, function* () {
@@ -23,6 +24,20 @@ export const deletePaymentLineById = function (orderId, id) {
23
24
  }
24
25
  });
25
26
  };
27
+ export const deleteOrderEvidence = function (orderId, evidenceId) {
28
+ return __awaiter(this, void 0, void 0, function* () {
29
+ try {
30
+ const config = {
31
+ headers: { Authorization: `Bearer ${this.apiToken}` }
32
+ };
33
+ return yield this.axiosInstance.delete(`${GET_SET_ORDER}/${orderId}/evidence/${evidenceId}`, config);
34
+ }
35
+ catch (error) {
36
+ console.error('Error fetching deleteOrderEvidence:', error);
37
+ throw error;
38
+ }
39
+ });
40
+ };
26
41
  export const cancelOrderByIdCustomersApp = function (id) {
27
42
  return __awaiter(this, void 0, void 0, function* () {
28
43
  try {
@@ -46,7 +46,7 @@ export const calculateOrderTotal = (order, selectedCustomer, storeSettings, hasS
46
46
  : calculateTotalTaxesIncluded;
47
47
  const productTableImports = productTableCalculator(order, selectedCustomer, storeSettings, storeDiscounts, appliedOrderDiscounts, discountCodeObj);
48
48
  // === PRODUCT LINE TOTALS ===
49
- const { totalQuantity, totalImportWithDiscount, totalImportWithoutDiscount, totalImporTaxes, totalDiscountAmount, totalSubtotalAmount } = getProductLineTotals(productTableImports);
49
+ const { totalQuantity, totalImportWithDiscount, totalImportWithoutDiscount, totalImporTaxes, totalDiscountAmount, totalSubtotalAmount, taxBreakdown } = getProductLineTotals(productTableImports);
50
50
  // === DISCOUNT CODE (monetario tipo NUMBER que se aplica una sola vez) ===
51
51
  let discountCodeAmount = 0;
52
52
  if (discountCodeObj &&
@@ -68,7 +68,7 @@ export const calculateOrderTotal = (order, selectedCustomer, storeSettings, hasS
68
68
  creditApplied -
69
69
  redeemPointsDiscount).toFixed(2);
70
70
  // === 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, total: orderTotal, creditApplied, redeemPointsApplied: redeemPointsDiscount, productTotalWithoutDiscount: totalImportWithoutDiscount, totalDiscountAmount: totalDiscountAmount + discountCodeAmount, shippingServiceTotal: shippingCost, appliedOrderDiscounts, subtotal: totalSubtotalAmount });
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 });
72
72
  }
73
73
  catch (error) {
74
74
  throw error;
@@ -1,5 +1,5 @@
1
1
  import { DiscountCodeTypes } from "../../enum";
2
- import { getProductTaxesPercentage } from "./helpers";
2
+ import { calculateProductTaxBreakdown, getProductTaxesPercentage } from "./helpers";
3
3
  const getNormalizedId = (value) => {
4
4
  if (value === null || value === undefined) {
5
5
  return "";
@@ -106,6 +106,11 @@ export const calculateTotalTaxesIncluded = (order, selectedCustomer, storeSettin
106
106
  const productLineTotalWithDiscount = +(productNetTotalWithDiscount * taxFactor).toFixed(2);
107
107
  // Calcular el total de impuestos como la diferencia entre bruto y neto con descuento
108
108
  const totalTaxesApplied = +(productLineTotalWithDiscount - productNetTotalWithDiscount).toFixed(2);
109
+ const taxBreakdown = calculateProductTaxBreakdown({
110
+ product: current,
111
+ store: storeSettings,
112
+ lineTaxAmount: totalTaxesApplied,
113
+ });
109
114
  const lineDiscount = +(discountAmountPerUnit * qty).toFixed(2);
110
115
  return {
111
116
  product: current,
@@ -113,6 +118,7 @@ export const calculateTotalTaxesIncluded = (order, selectedCustomer, storeSettin
113
118
  productLineImportTotal, // Total bruto sin descuento (incluye extra y IVA)
114
119
  productLineTotalWithDiscount, // Total bruto con descuento aplicado sobre el precio ajustado
115
120
  productLineTaxesTotal: totalTaxesApplied,
121
+ taxBreakdown,
116
122
  lineDiscountAmount: lineDiscount,
117
123
  productLineSubtotal: productNetTotalWithoutDiscount // Subtotal neto (sin IVA)
118
124
  };
@@ -1,5 +1,5 @@
1
1
  import { BuyAndGetConditionsTypes, DiscountCodeTypes } from "../../enum";
2
- import { getProductTaxesPercentage } from "./helpers";
2
+ import { calculateProductTaxBreakdown, getProductTaxesPercentage } from "./helpers";
3
3
  const getNormalizedId = (value) => {
4
4
  if (value === null || value === undefined) {
5
5
  return "";
@@ -91,6 +91,11 @@ export const calculateTotalTaxesOverPrice = (order, selectedCustomer, storeSetti
91
91
  const taxes = subtotal * taxPercentage;
92
92
  // Resultados finales:
93
93
  const totalTaxesApplied = +taxes.toFixed(2);
94
+ const taxBreakdown = calculateProductTaxBreakdown({
95
+ product: current,
96
+ store: storeSettings,
97
+ lineTaxAmount: totalTaxesApplied,
98
+ });
94
99
  const lineDiscount = +(productDiscount * qty).toFixed(2);
95
100
  // Precio total con descuento, reaplicando impuestos
96
101
  const prodLinePriceWithDiscount = +((subtotal * (1 + taxPercentage))).toFixed(2);
@@ -102,6 +107,7 @@ export const calculateTotalTaxesOverPrice = (order, selectedCustomer, storeSetti
102
107
  // Total final con descuento, con impuestos incluidos
103
108
  productLineTotalWithDiscount: prodLinePriceWithDiscount,
104
109
  productLineTaxesTotal: totalTaxesApplied,
110
+ taxBreakdown,
105
111
  lineDiscountAmount: lineDiscount,
106
112
  // Subtotal neto (sin impuestos) con descuento aplicado
107
113
  productLineSubtotal: subtotal
@@ -84,6 +84,103 @@ export const getProductTaxesPercentage = (productObj, store) => {
84
84
  const tax3 = getTaxValue(store.taxThree, productObj.taxExemptThree);
85
85
  return tax1 + tax2 + tax3;
86
86
  };
87
+ const TAX_SLOTS = [
88
+ { key: "taxOne", taxField: "taxOne", exemptField: "taxExemptOne" },
89
+ { key: "taxTwo", taxField: "taxTwo", exemptField: "taxExemptTwo" },
90
+ { key: "taxThree", taxField: "taxThree", exemptField: "taxExemptThree" },
91
+ ];
92
+ const round2 = (value) => Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100;
93
+ const getApplicableTaxesForProduct = (productObj, store) => {
94
+ return TAX_SLOTS.reduce((acc, slot) => {
95
+ var _a, _b;
96
+ if (productObj[slot.exemptField]) {
97
+ return acc;
98
+ }
99
+ const tax = store[slot.taxField];
100
+ const rate = Number((_a = tax === null || tax === void 0 ? void 0 : tax.value) !== null && _a !== void 0 ? _a : 0);
101
+ if (!Number.isFinite(rate) || rate <= 0) {
102
+ return acc;
103
+ }
104
+ acc.push({
105
+ key: slot.key,
106
+ name: String((_b = tax === null || tax === void 0 ? void 0 : tax.name) !== null && _b !== void 0 ? _b : slot.key),
107
+ rate,
108
+ weight: Math.round(rate * 100),
109
+ });
110
+ return acc;
111
+ }, []);
112
+ };
113
+ const allocateByWeights = (total, weights) => {
114
+ if (weights.length === 0)
115
+ return [];
116
+ if (total <= 0)
117
+ return new Array(weights.length).fill(0);
118
+ const safeWeights = weights.map((weight) => Math.max(0, Math.trunc(weight)));
119
+ const sum = safeWeights.reduce((acc, weight) => acc + weight, 0);
120
+ if (sum <= 0)
121
+ return new Array(weights.length).fill(0);
122
+ const raw = safeWeights.map((weight) => (total * weight) / sum);
123
+ const floors = raw.map((amount) => Math.floor(amount));
124
+ let remaining = total - floors.reduce((acc, amount) => acc + amount, 0);
125
+ const order = raw
126
+ .map((amount, index) => ({ index, frac: amount - Math.floor(amount) }))
127
+ .sort((a, b) => b.frac - a.frac);
128
+ const out = floors.slice();
129
+ for (let index = 0; index < order.length && remaining > 0; index += 1) {
130
+ out[order[index].index] += 1;
131
+ remaining -= 1;
132
+ }
133
+ return out;
134
+ };
135
+ export const aggregateTaxBreakdown = (items) => {
136
+ const byKey = new Map();
137
+ items.forEach((item) => {
138
+ if (!item || Number(item.amount || 0) <= 0) {
139
+ return;
140
+ }
141
+ const existing = byKey.get(item.key);
142
+ if (existing) {
143
+ existing.amount = round2(existing.amount + item.amount);
144
+ return;
145
+ }
146
+ byKey.set(item.key, Object.assign(Object.assign({}, item), { amount: round2(item.amount) }));
147
+ });
148
+ return TAX_SLOTS.map((slot) => byKey.get(slot.key)).filter((item) => Boolean(item && item.amount > 0));
149
+ };
150
+ export const normalizeTaxBreakdownToTotal = (items, total) => {
151
+ const target = round2(total);
152
+ if (target <= 0) {
153
+ return [];
154
+ }
155
+ const aggregated = aggregateTaxBreakdown(items);
156
+ if (aggregated.length === 0) {
157
+ return [];
158
+ }
159
+ const sum = round2(aggregated.reduce((acc, item) => acc + item.amount, 0));
160
+ const diff = round2(target - sum);
161
+ if (diff !== 0) {
162
+ const lastIndex = aggregated.length - 1;
163
+ aggregated[lastIndex] = Object.assign(Object.assign({}, aggregated[lastIndex]), { amount: round2(aggregated[lastIndex].amount + diff) });
164
+ }
165
+ return aggregated.filter((item) => item.amount > 0);
166
+ };
167
+ export const calculateProductTaxBreakdown = ({ product, store, lineTaxAmount, }) => {
168
+ const applicableTaxes = getApplicableTaxesForProduct(product, store);
169
+ const target = round2(lineTaxAmount);
170
+ if (target <= 0 || applicableTaxes.length === 0) {
171
+ return [];
172
+ }
173
+ const amounts = allocateByWeights(Math.round(target * 100), applicableTaxes.map((tax) => tax.weight)).map((amountCents) => round2(amountCents / 100));
174
+ return normalizeTaxBreakdownToTotal(applicableTaxes.map((tax, index) => {
175
+ var _a;
176
+ return ({
177
+ key: tax.key,
178
+ name: tax.name,
179
+ rate: tax.rate,
180
+ amount: (_a = amounts[index]) !== null && _a !== void 0 ? _a : 0,
181
+ });
182
+ }), target);
183
+ };
87
184
  export const getProductLineTotals = (productLinesTotals) => {
88
185
  const totalQuantity = +productLinesTotals.reduce((acum, line) => acum + line.qty, 0).toFixed(2);
89
186
  const totalImportWithDiscount = +productLinesTotals
@@ -101,13 +198,15 @@ export const getProductLineTotals = (productLinesTotals) => {
101
198
  const totalSubtotalAmount = +productLinesTotals
102
199
  .reduce((acum, line) => acum + line.productLineSubtotal, 0)
103
200
  .toFixed(2);
201
+ const taxBreakdown = normalizeTaxBreakdownToTotal(productLinesTotals.flatMap((line) => line.taxBreakdown || []), totalImporTaxes);
104
202
  return {
105
203
  totalQuantity,
106
204
  totalImportWithDiscount,
107
205
  totalImportWithoutDiscount,
108
206
  totalImporTaxes,
109
207
  totalDiscountAmount,
110
- totalSubtotalAmount
208
+ totalSubtotalAmount,
209
+ taxBreakdown
111
210
  };
112
211
  };
113
212
  export const getShippingCost = (discountObject, requireShippingService, store) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "washday-sdk",
3
- "version": "1.6.64",
3
+ "version": "1.6.66",
4
4
  "description": "Washday utilities functions and API",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
package/src/api/index.ts CHANGED
@@ -161,6 +161,7 @@ const WashdayClient: WashdayClientConstructor = function WashdayClient(this: Was
161
161
  sendOrderUncollectedCustomerNotification: ordersEndpoints.postModule.sendOrderUncollectedCustomerNotification,
162
162
  recordFailedServiceAttempt: ordersEndpoints.postModule.recordFailedServiceAttempt,
163
163
  deletePaymentLineById: ordersEndpoints.deleteModule.deletePaymentLineById,
164
+ deleteOrderEvidence: ordersEndpoints.deleteModule.deleteOrderEvidence,
164
165
  getListCustomersApp: ordersEndpoints.getModule.getListCustomersApp,
165
166
  getByIdCustomersApp: ordersEndpoints.getModule.getByIdCustomersApp,
166
167
  setOrderAcceptedBySequence: ordersEndpoints.putModule.setOrderAcceptedBySequence,
@@ -1,6 +1,7 @@
1
1
  import { WashdayClientInstance } from "../../interfaces/Api";
2
2
  import axiosInstance from "../axiosInstance";
3
3
  const GET_SET_ORDER_PAYMENTLINES = (orderId: string) => `/api/v2/order/${orderId}/paymentLines`;
4
+ const GET_SET_ORDER = 'api/v2/order';
4
5
  const GET_SET_ORDER_CUSTOMERS_APP = 'api/v2/washdayapp/orders';
5
6
 
6
7
  export const deletePaymentLineById = async function (this: WashdayClientInstance, orderId: string, id: string): Promise<any> {
@@ -15,6 +16,18 @@ export const deletePaymentLineById = async function (this: WashdayClientInstance
15
16
  }
16
17
  };
17
18
 
19
+ export const deleteOrderEvidence = async function (this: WashdayClientInstance, orderId: string, evidenceId: string): Promise<any> {
20
+ try {
21
+ const config = {
22
+ headers: { Authorization: `Bearer ${this.apiToken}` }
23
+ };
24
+ return await this.axiosInstance.delete(`${GET_SET_ORDER}/${orderId}/evidence/${evidenceId}`, config);
25
+ } catch (error) {
26
+ console.error('Error fetching deleteOrderEvidence:', error);
27
+ throw error;
28
+ }
29
+ };
30
+
18
31
 
19
32
  export const cancelOrderByIdCustomersApp = async function (this: WashdayClientInstance, id: string): Promise<any> {
20
33
  try {
package/src/index.ts CHANGED
@@ -8,6 +8,13 @@ export * from './interfaces/Customer';
8
8
  export * from './interfaces/DomainUrls';
9
9
  export * from './interfaces/GoogleMaps';
10
10
  export * from './interfaces/Attendance';
11
+ export type {
12
+ IOrderPricingSnapshot,
13
+ IOrderPricingSnapshotLine,
14
+ IOrderPricingSnapshotTotals,
15
+ ITaxBreakdownItem,
16
+ TaxBreakdownKey,
17
+ } from './interfaces/Order';
11
18
  export type { StartMPOAuthFlowRequest } from './api/integrations/post';
12
19
  export type {
13
20
  CreateCFDISubscriptionCheckoutSessionRequest,
@@ -146,6 +146,7 @@ export interface WashdayClientInstance {
146
146
  sendOrderUncollectedCustomerNotification: typeof ordersEndpoints.postModule.sendOrderUncollectedCustomerNotification,
147
147
  recordFailedServiceAttempt: typeof ordersEndpoints.postModule.recordFailedServiceAttempt,
148
148
  deletePaymentLineById: typeof ordersEndpoints.deleteModule.deletePaymentLineById;
149
+ deleteOrderEvidence: typeof ordersEndpoints.deleteModule.deleteOrderEvidence;
149
150
  getListCustomersApp: typeof ordersEndpoints.getModule.getListCustomersApp;
150
151
  getByIdCustomersApp: typeof ordersEndpoints.getModule.getByIdCustomersApp;
151
152
  setOrderAcceptedBySequence: typeof ordersEndpoints.putModule.setOrderAcceptedBySequence,
@@ -46,6 +46,71 @@ export interface IOrderPaymentLines {
46
46
  facturapiPaymentInvoiceID?: string | null,
47
47
  }
48
48
 
49
+ export interface IOrderEvidence {
50
+ _id?: any,
51
+ url: string,
52
+ cloudinaryPublicId?: string,
53
+ notes?: string,
54
+ type: 'pickup' | 'delivery' | 'signature' | 'other',
55
+ createdBy: IUser | string,
56
+ uploadedAt: Date,
57
+ }
58
+
59
+ export type TaxBreakdownKey = "taxOne" | "taxTwo" | "taxThree";
60
+
61
+ export interface ITaxBreakdownItem {
62
+ key: TaxBreakdownKey;
63
+ name: string;
64
+ rate: number;
65
+ amount: number;
66
+ }
67
+
68
+ export interface IOrderPricingSnapshotLine {
69
+ lineId: string;
70
+ quantity: number;
71
+ netBeforeDiscount: number;
72
+ netDiscount: number;
73
+ netAfterDiscount: number;
74
+ tax: number;
75
+ taxBreakdown?: ITaxBreakdownItem[];
76
+ grossBeforeDiscount: number;
77
+ gross: number;
78
+ }
79
+
80
+ export interface IOrderPricingSnapshotTotals {
81
+ totalQuantity: number;
82
+ productTotal: number;
83
+ productTotalWithoutDiscount: number;
84
+ taxesTotal: number;
85
+ taxBreakdown?: ITaxBreakdownItem[];
86
+ shippingServiceTotal: number;
87
+ orderLevelDiscountGross?: number;
88
+ discountTotalGrossLines?: number;
89
+ totalDiscountGross?: number;
90
+ subtotalBeforeDiscountNet: number;
91
+ subtotalNetAfterDiscount: number;
92
+ discountTotalNet: number;
93
+ creditApplied: number;
94
+ redeemPointsApplied: number;
95
+ total: number;
96
+ }
97
+
98
+ export interface IOrderPricingSnapshot {
99
+ version: string;
100
+ engine: string;
101
+ engineVersion?: string;
102
+ calculatedAt: Date;
103
+ currency: string;
104
+ taxesType: "included" | "over_price";
105
+ express: boolean;
106
+ discountCodeId?: string;
107
+ redeemPointsAsPaymentAmount: number;
108
+ totalsCents: IOrderPricingSnapshotTotals;
109
+ lines: IOrderPricingSnapshotLine[];
110
+ appliedOrderDiscounts: Record<string, number>;
111
+ meta?: Record<string, any>;
112
+ }
113
+
49
114
  export interface IOrderPhaseCompletion {
50
115
  source: RouteOrderCompletionSource,
51
116
  confirmedAt?: Date | null,
@@ -73,6 +138,7 @@ export interface IOrder {
73
138
  taxThree?: ITaxConfig,
74
139
  creditApplied: number,
75
140
  taxesTotal: number,
141
+ taxBreakdown?: ITaxBreakdownItem[],
76
142
  total: number,
77
143
  totalQuantity: number,
78
144
  productTotal: number,
@@ -109,7 +175,9 @@ export interface IOrder {
109
175
  appliedStoreDiscounts: Array<string> | null,
110
176
  appliedDiscountCodes: Array<string> | null,
111
177
  paymentLines: Array<IOrderPaymentLines> | Array<string> | any,
178
+ evidence?: Array<IOrderEvidence>,
112
179
  facturapiInvoiceID?: string | null,
180
+ pricingSnapshot?: IOrderPricingSnapshot | null,
113
181
  }
114
182
 
115
183
 
@@ -2,6 +2,7 @@ import { IStore } from "./Store"
2
2
  import { IStoreImage } from "./StoreImage"
3
3
  import { IUser } from "./User"
4
4
  import { OrderProductLineStatus } from "../enum"
5
+ import type { ITaxBreakdownItem } from "./Order"
5
6
 
6
7
  export interface ProductLineTotals {
7
8
  product: IOrderProduct,
@@ -9,6 +10,7 @@ export interface ProductLineTotals {
9
10
  productLineImportTotal: number,
10
11
  productLineTotalWithDiscount: number,
11
12
  productLineTaxesTotal: number,
13
+ taxBreakdown?: ITaxBreakdownItem[],
12
14
  lineDiscountAmount: number
13
15
  productLineSubtotal: number
14
16
  }
@@ -75,7 +75,8 @@ export const calculateOrderTotal = (
75
75
  totalImportWithoutDiscount,
76
76
  totalImporTaxes,
77
77
  totalDiscountAmount,
78
- totalSubtotalAmount
78
+ totalSubtotalAmount,
79
+ taxBreakdown
79
80
  } = getProductLineTotals(productTableImports);
80
81
 
81
82
  // === DISCOUNT CODE (monetario tipo NUMBER que se aplica una sola vez) ===
@@ -127,6 +128,7 @@ export const calculateOrderTotal = (
127
128
  customerDiscount: order.discountCode ? 0 : selectedCustomer?.customer?.discount,
128
129
  productTotal: totalImportWithDiscount,
129
130
  taxesTotal: totalImporTaxes,
131
+ taxBreakdown,
130
132
  total: orderTotal,
131
133
  creditApplied,
132
134
  redeemPointsApplied: redeemPointsDiscount, // ✅ NUEVO CAMPO
@@ -1,6 +1,6 @@
1
1
  import { DiscountCodeTypes } from "../../enum";
2
2
  import { IOrderProduct } from "../../interfaces/Product";
3
- import { getProductTaxesPercentage } from "./helpers";
3
+ import { calculateProductTaxBreakdown, getProductTaxesPercentage } from "./helpers";
4
4
 
5
5
  const getNormalizedId = (value: any): string => {
6
6
  if (value === null || value === undefined) {
@@ -124,6 +124,11 @@ export const calculateTotalTaxesIncluded = (
124
124
 
125
125
  // Calcular el total de impuestos como la diferencia entre bruto y neto con descuento
126
126
  const totalTaxesApplied = +(productLineTotalWithDiscount - productNetTotalWithDiscount).toFixed(2);
127
+ const taxBreakdown = calculateProductTaxBreakdown({
128
+ product: current,
129
+ store: storeSettings,
130
+ lineTaxAmount: totalTaxesApplied,
131
+ });
127
132
 
128
133
  const lineDiscount = +(discountAmountPerUnit * qty).toFixed(2);
129
134
 
@@ -133,6 +138,7 @@ export const calculateTotalTaxesIncluded = (
133
138
  productLineImportTotal, // Total bruto sin descuento (incluye extra y IVA)
134
139
  productLineTotalWithDiscount, // Total bruto con descuento aplicado sobre el precio ajustado
135
140
  productLineTaxesTotal: totalTaxesApplied,
141
+ taxBreakdown,
136
142
  lineDiscountAmount: lineDiscount,
137
143
  productLineSubtotal: productNetTotalWithoutDiscount // Subtotal neto (sin IVA)
138
144
  };
@@ -1,5 +1,5 @@
1
1
  import { BuyAndGetConditionsTypes, DiscountCodeTypes } from "../../enum";
2
- import { getProductTaxesPercentage } from "./helpers";
2
+ import { calculateProductTaxBreakdown, getProductTaxesPercentage } from "./helpers";
3
3
 
4
4
  const getNormalizedId = (value: any): string => {
5
5
  if (value === null || value === undefined) {
@@ -107,6 +107,11 @@ export const calculateTotalTaxesOverPrice = (
107
107
 
108
108
  // Resultados finales:
109
109
  const totalTaxesApplied = +taxes.toFixed(2);
110
+ const taxBreakdown = calculateProductTaxBreakdown({
111
+ product: current,
112
+ store: storeSettings,
113
+ lineTaxAmount: totalTaxesApplied,
114
+ });
110
115
  const lineDiscount = +(productDiscount * qty).toFixed(2);
111
116
 
112
117
  // Precio total con descuento, reaplicando impuestos
@@ -120,6 +125,7 @@ export const calculateTotalTaxesOverPrice = (
120
125
  // Total final con descuento, con impuestos incluidos
121
126
  productLineTotalWithDiscount: prodLinePriceWithDiscount,
122
127
  productLineTaxesTotal: totalTaxesApplied,
128
+ taxBreakdown,
123
129
  lineDiscountAmount: lineDiscount,
124
130
  // Subtotal neto (sin impuestos) con descuento aplicado
125
131
  productLineSubtotal: subtotal
@@ -2,6 +2,7 @@ import { BuyAndGetConditionsTypes, DiscountCodeTypes } from "../../enum";
2
2
  import { ICustomer } from "../../interfaces/Customer";
3
3
  import { IOrderProduct, ProductLineTotals } from "../../interfaces/Product";
4
4
  import { IStore, ITaxConfig } from "../../interfaces/Store";
5
+ import type { ITaxBreakdownItem, TaxBreakdownKey } from "../../interfaces/Order";
5
6
 
6
7
  const getNormalizedId = (value: any): string => {
7
8
  if (value === null || value === undefined) {
@@ -105,6 +106,154 @@ export const getProductTaxesPercentage = (productObj: IOrderProduct, store: ISto
105
106
  return tax1 + tax2 + tax3;
106
107
  };
107
108
 
109
+ const TAX_SLOTS: Array<{
110
+ key: TaxBreakdownKey;
111
+ taxField: "taxOne" | "taxTwo" | "taxThree";
112
+ exemptField: "taxExemptOne" | "taxExemptTwo" | "taxExemptThree";
113
+ }> = [
114
+ { key: "taxOne", taxField: "taxOne", exemptField: "taxExemptOne" },
115
+ { key: "taxTwo", taxField: "taxTwo", exemptField: "taxExemptTwo" },
116
+ { key: "taxThree", taxField: "taxThree", exemptField: "taxExemptThree" },
117
+ ];
118
+
119
+ type ApplicableTax = {
120
+ key: TaxBreakdownKey;
121
+ name: string;
122
+ rate: number;
123
+ weight: number;
124
+ };
125
+
126
+ const round2 = (value: number): number =>
127
+ Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100;
128
+
129
+ const getApplicableTaxesForProduct = (
130
+ productObj: IOrderProduct,
131
+ store: IStore
132
+ ): ApplicableTax[] => {
133
+ return TAX_SLOTS.reduce<ApplicableTax[]>((acc, slot) => {
134
+ if (productObj[slot.exemptField]) {
135
+ return acc;
136
+ }
137
+
138
+ const tax = store[slot.taxField] as ITaxConfig | undefined;
139
+ const rate = Number(tax?.value ?? 0);
140
+ if (!Number.isFinite(rate) || rate <= 0) {
141
+ return acc;
142
+ }
143
+
144
+ acc.push({
145
+ key: slot.key,
146
+ name: String(tax?.name ?? slot.key),
147
+ rate,
148
+ weight: Math.round(rate * 100),
149
+ });
150
+ return acc;
151
+ }, []);
152
+ };
153
+
154
+ const allocateByWeights = (total: number, weights: number[]): number[] => {
155
+ if (weights.length === 0) return [];
156
+ if (total <= 0) return new Array(weights.length).fill(0);
157
+
158
+ const safeWeights = weights.map((weight) => Math.max(0, Math.trunc(weight)));
159
+ const sum = safeWeights.reduce((acc, weight) => acc + weight, 0);
160
+ if (sum <= 0) return new Array(weights.length).fill(0);
161
+
162
+ const raw = safeWeights.map((weight) => (total * weight) / sum);
163
+ const floors = raw.map((amount) => Math.floor(amount));
164
+ let remaining = total - floors.reduce((acc, amount) => acc + amount, 0);
165
+ const order = raw
166
+ .map((amount, index) => ({ index, frac: amount - Math.floor(amount) }))
167
+ .sort((a, b) => b.frac - a.frac);
168
+
169
+ const out = floors.slice();
170
+ for (let index = 0; index < order.length && remaining > 0; index += 1) {
171
+ out[order[index].index] += 1;
172
+ remaining -= 1;
173
+ }
174
+ return out;
175
+ };
176
+
177
+ export const aggregateTaxBreakdown = (
178
+ items: ITaxBreakdownItem[]
179
+ ): ITaxBreakdownItem[] => {
180
+ const byKey = new Map<TaxBreakdownKey, ITaxBreakdownItem>();
181
+
182
+ items.forEach((item) => {
183
+ if (!item || Number(item.amount || 0) <= 0) {
184
+ return;
185
+ }
186
+ const existing = byKey.get(item.key);
187
+ if (existing) {
188
+ existing.amount = round2(existing.amount + item.amount);
189
+ return;
190
+ }
191
+ byKey.set(item.key, { ...item, amount: round2(item.amount) });
192
+ });
193
+
194
+ return TAX_SLOTS.map((slot) => byKey.get(slot.key)).filter(
195
+ (item): item is ITaxBreakdownItem => Boolean(item && item.amount > 0)
196
+ );
197
+ };
198
+
199
+ export const normalizeTaxBreakdownToTotal = (
200
+ items: ITaxBreakdownItem[],
201
+ total: number
202
+ ): ITaxBreakdownItem[] => {
203
+ const target = round2(total);
204
+ if (target <= 0) {
205
+ return [];
206
+ }
207
+
208
+ const aggregated = aggregateTaxBreakdown(items);
209
+ if (aggregated.length === 0) {
210
+ return [];
211
+ }
212
+
213
+ const sum = round2(aggregated.reduce((acc, item) => acc + item.amount, 0));
214
+ const diff = round2(target - sum);
215
+ if (diff !== 0) {
216
+ const lastIndex = aggregated.length - 1;
217
+ aggregated[lastIndex] = {
218
+ ...aggregated[lastIndex],
219
+ amount: round2(aggregated[lastIndex].amount + diff),
220
+ };
221
+ }
222
+
223
+ return aggregated.filter((item) => item.amount > 0);
224
+ };
225
+
226
+ export const calculateProductTaxBreakdown = ({
227
+ product,
228
+ store,
229
+ lineTaxAmount,
230
+ }: {
231
+ product: IOrderProduct;
232
+ store: IStore;
233
+ lineTaxAmount: number;
234
+ }): ITaxBreakdownItem[] => {
235
+ const applicableTaxes = getApplicableTaxesForProduct(product, store);
236
+ const target = round2(lineTaxAmount);
237
+ if (target <= 0 || applicableTaxes.length === 0) {
238
+ return [];
239
+ }
240
+
241
+ const amounts = allocateByWeights(
242
+ Math.round(target * 100),
243
+ applicableTaxes.map((tax) => tax.weight)
244
+ ).map((amountCents) => round2(amountCents / 100));
245
+
246
+ return normalizeTaxBreakdownToTotal(
247
+ applicableTaxes.map((tax, index) => ({
248
+ key: tax.key,
249
+ name: tax.name,
250
+ rate: tax.rate,
251
+ amount: amounts[index] ?? 0,
252
+ })),
253
+ target
254
+ );
255
+ };
256
+
108
257
  export const getProductLineTotals = (productLinesTotals: ProductLineTotals[]) => {
109
258
  const totalQuantity = +productLinesTotals.reduce((acum, line) => acum + line.qty, 0).toFixed(2);
110
259
  const totalImportWithDiscount = +productLinesTotals
@@ -122,6 +271,10 @@ export const getProductLineTotals = (productLinesTotals: ProductLineTotals[]) =>
122
271
  const totalSubtotalAmount = +productLinesTotals
123
272
  .reduce((acum, line) => acum + line.productLineSubtotal, 0)
124
273
  .toFixed(2);
274
+ const taxBreakdown = normalizeTaxBreakdownToTotal(
275
+ productLinesTotals.flatMap((line) => line.taxBreakdown || []),
276
+ totalImporTaxes
277
+ );
125
278
 
126
279
  return {
127
280
  totalQuantity,
@@ -129,7 +282,8 @@ export const getProductLineTotals = (productLinesTotals: ProductLineTotals[]) =>
129
282
  totalImportWithoutDiscount,
130
283
  totalImporTaxes,
131
284
  totalDiscountAmount,
132
- totalSubtotalAmount
285
+ totalSubtotalAmount,
286
+ taxBreakdown
133
287
  };
134
288
  };
135
289
 
@@ -0,0 +1,147 @@
1
+ import { utils } from "../src";
2
+
3
+ const selectedCustomer = {
4
+ customer: {
5
+ credit: 0,
6
+ discount: 0,
7
+ },
8
+ };
9
+
10
+ describe("calculateOrderTotal taxBreakdown", () => {
11
+ it("over_price adds taxBreakdown without changing existing totals", () => {
12
+ const result = utils.calculateOrderTotal(
13
+ {
14
+ express: false,
15
+ products: [
16
+ { _id: "p1", qty: 1, quantity: 1, price: 100, expressPrice: 100, extraAmount: 0 },
17
+ {
18
+ _id: "p2",
19
+ qty: 1,
20
+ quantity: 1,
21
+ price: 100,
22
+ expressPrice: 100,
23
+ extraAmount: 0,
24
+ taxExemptOne: true,
25
+ },
26
+ ],
27
+ buyAndGetProducts: [],
28
+ discountCode: null,
29
+ },
30
+ selectedCustomer as any,
31
+ {
32
+ taxesType: "over_price",
33
+ taxOne: { name: "IVA", value: 16 },
34
+ taxTwo: { name: "Local", value: 3 },
35
+ orderPageConfig: { shippingServiceCost: 0 },
36
+ } as any,
37
+ false,
38
+ [],
39
+ null,
40
+ 0
41
+ );
42
+
43
+ expect(result.taxesTotal).toBe(22);
44
+ expect(result.total).toBe(222);
45
+ expect(result.taxBreakdown).toEqual([
46
+ { key: "taxOne", name: "IVA", rate: 16, amount: 16 },
47
+ { key: "taxTwo", name: "Local", rate: 3, amount: 6 },
48
+ ]);
49
+ });
50
+
51
+ it("included distributes extracted tax across all configured taxes", () => {
52
+ const result = utils.calculateOrderTotal(
53
+ {
54
+ express: false,
55
+ products: [{ _id: "p", qty: 1, quantity: 1, price: 120, expressPrice: 120, extraAmount: 0 }],
56
+ buyAndGetProducts: [],
57
+ discountCode: null,
58
+ },
59
+ selectedCustomer as any,
60
+ {
61
+ taxesType: "included",
62
+ taxOne: { name: "IVA", value: 16 },
63
+ taxTwo: { name: "Local", value: 3 },
64
+ taxThree: { name: "Eco", value: 1 },
65
+ orderPageConfig: { shippingServiceCost: 0 },
66
+ } as any,
67
+ false,
68
+ [],
69
+ null,
70
+ 0
71
+ );
72
+
73
+ expect(result.taxesTotal).toBe(20);
74
+ expect(result.total).toBe(120);
75
+ expect(result.taxBreakdown).toEqual([
76
+ { key: "taxOne", name: "IVA", rate: 16, amount: 16 },
77
+ { key: "taxTwo", name: "Local", rate: 3, amount: 3 },
78
+ { key: "taxThree", name: "Eco", rate: 1, amount: 1 },
79
+ ]);
80
+ });
81
+
82
+ it("returns empty taxBreakdown when no tax amount remains after a free item promo", () => {
83
+ const result = utils.calculateOrderTotal(
84
+ {
85
+ express: false,
86
+ products: [
87
+ {
88
+ _id: "p",
89
+ qty: 1,
90
+ quantity: 1,
91
+ price: 100,
92
+ expressPrice: 100,
93
+ extraAmount: 0,
94
+ isBuyAndGetProduct: true,
95
+ },
96
+ ],
97
+ buyAndGetProducts: [],
98
+ discountCode: "discount-1",
99
+ },
100
+ selectedCustomer as any,
101
+ {
102
+ taxesType: "over_price",
103
+ taxOne: { name: "IVA", value: 16 },
104
+ orderPageConfig: { shippingServiceCost: 0 },
105
+ } as any,
106
+ false,
107
+ [],
108
+ {
109
+ type: "buyXGetY",
110
+ buyAndGetConditions: [{ getDiscountType: "free", discountValue: 0 }],
111
+ },
112
+ 0
113
+ );
114
+
115
+ expect(result.taxesTotal).toBe(0);
116
+ expect(result.taxBreakdown).toEqual([]);
117
+ });
118
+
119
+ it("over_price preserves one-cent rounded total tax across low-value multi-tax lines", () => {
120
+ const result = utils.calculateOrderTotal(
121
+ {
122
+ express: false,
123
+ products: [{ _id: "p", qty: 1, quantity: 1, price: 0.17, expressPrice: 0.17, extraAmount: 0 }],
124
+ buyAndGetProducts: [],
125
+ discountCode: null,
126
+ },
127
+ selectedCustomer as any,
128
+ {
129
+ taxesType: "over_price",
130
+ taxOne: { name: "T1", value: 1 },
131
+ taxTwo: { name: "T2", value: 1 },
132
+ taxThree: { name: "T3", value: 1 },
133
+ orderPageConfig: { shippingServiceCost: 0 },
134
+ } as any,
135
+ false,
136
+ [],
137
+ null,
138
+ 0
139
+ );
140
+
141
+ const breakdownTotal = result.taxBreakdown.reduce((sum: number, item: any) => sum + item.amount, 0);
142
+
143
+ expect(result.taxesTotal).toBe(0.01);
144
+ expect(result.taxBreakdown.length).toBeGreaterThan(0);
145
+ expect(+breakdownTotal.toFixed(2)).toBe(result.taxesTotal);
146
+ });
147
+ });
@@ -0,0 +1,37 @@
1
+ import { deleteOrderEvidence } from "../src/api/order/delete";
2
+
3
+ describe("orders.deleteOrderEvidence", () => {
4
+ it("deletes order evidence with the expected URL and auth header", async () => {
5
+ const deleteRequest = jest.fn().mockResolvedValue({
6
+ data: {
7
+ data: {
8
+ deleted: true,
9
+ evidenceId: "evidence-1",
10
+ cloudinaryResult: "ok",
11
+ },
12
+ },
13
+ });
14
+ const client = {
15
+ apiToken: "token-123",
16
+ axiosInstance: { delete: deleteRequest },
17
+ } as any;
18
+
19
+ const result = await deleteOrderEvidence.call(client, "order-1", "evidence-1");
20
+
21
+ expect(deleteRequest).toHaveBeenCalledWith(
22
+ "api/v2/order/order-1/evidence/evidence-1",
23
+ {
24
+ headers: { Authorization: "Bearer token-123" },
25
+ }
26
+ );
27
+ expect(result).toEqual({
28
+ data: {
29
+ data: {
30
+ deleted: true,
31
+ evidenceId: "evidence-1",
32
+ cloudinaryResult: "ok",
33
+ },
34
+ },
35
+ });
36
+ });
37
+ });