washday-sdk 1.6.71 → 1.6.73

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
@@ -356,6 +356,7 @@ const WashdayClient = function WashdayClient(apiToken, env = 'PROD', clientId, c
356
356
  getSuppliesReport: reportsExportEndpoints.getModule.getSuppliesReport,
357
357
  getDiscountCodesReport: reportsExportEndpoints.getModule.getDiscountCodesReport,
358
358
  getStaffReport: reportsExportEndpoints.getModule.getStaffReport,
359
+ getStaffActivityDetails: reportsExportEndpoints.getModule.getStaffActivityDetails,
359
360
  getProductSalesReport: reportsExportEndpoints.getModule.getProductSalesReport,
360
361
  getProductSalesDrillDown: reportsExportEndpoints.getModule.getProductSalesDrillDown,
361
362
  getPaymentLinesReport: reportsExportEndpoints.getModule.getPaymentLinesReport,
@@ -145,6 +145,25 @@ export const getStaffReport = function (storeId, params) {
145
145
  }
146
146
  });
147
147
  };
148
+ export const getStaffActivityDetails = function (storeId, params) {
149
+ return __awaiter(this, void 0, void 0, function* () {
150
+ try {
151
+ const config = {
152
+ headers: { Authorization: `Bearer ${this.apiToken}` }
153
+ };
154
+ const queryParams = generateQueryParamsStr([
155
+ 'fromDate',
156
+ 'toDate',
157
+ 'employeeId',
158
+ ], params);
159
+ return yield this.axiosInstance.get(`${GET_SET_REPORTS}/${storeId}/staff/details?${queryParams}`, config);
160
+ }
161
+ catch (error) {
162
+ console.error('Error fetching getStaffActivityDetails:', error);
163
+ throw error;
164
+ }
165
+ });
166
+ };
148
167
  export const getProductSalesReport = function (storeId, params) {
149
168
  return __awaiter(this, void 0, void 0, function* () {
150
169
  try {
@@ -59,7 +59,7 @@ export const calculateOrderTotal = (order, selectedCustomer, storeSettings, hasS
59
59
  : calculateTotalTaxesIncluded;
60
60
  const productTableImports = productTableCalculator(processedOrder, selectedCustomer, storeSettings, storeDiscounts, appliedOrderDiscounts, discountCodeObj);
61
61
  // === PRODUCT LINE TOTALS ===
62
- const { totalQuantity, totalImportWithDiscount, totalImportWithoutDiscount, totalImporTaxes, totalDiscountAmount, totalSubtotalAmount, taxBreakdown } = getProductLineTotals(productTableImports);
62
+ const { totalQuantity, totalImportWithDiscount, totalImportWithoutDiscount, totalImporTaxes, totalDiscountAmount, totalSubtotalAmount, taxBreakdown, totalNetAfterDiscount, totalGrossBeforeDiscount, } = getProductLineTotals(productTableImports);
63
63
  // NOTE: the old post-tax "discountCodeAmount" subtraction was removed.
64
64
  // NUMBER coupons (both `applyOnceOnOrder=true` and `=false`) are now
65
65
  // applied pre-tax via per-line `discountAmount` baked above, so
@@ -76,8 +76,45 @@ export const calculateOrderTotal = (order, selectedCustomer, storeSettings, hasS
76
76
  const orderTotal = +(orderTotalWithOutCredit -
77
77
  creditApplied -
78
78
  redeemPointsDiscount).toFixed(2);
79
+ // === PRICING BREAKDOWN ===
80
+ // For included: totalImportWithoutDiscount=gross-before-discount, totalSubtotalAmount=net-before-discount
81
+ // For over_price: totalImportWithoutDiscount=net-before-discount, totalSubtotalAmount=net-after-discount
82
+ const taxMode = (storeSettings === null || storeSettings === void 0 ? void 0 : storeSettings.taxesType) === 'over_price' ? 'over_price' : 'included';
83
+ const subtotalBeforeDiscountNet = taxMode === 'included' ? totalSubtotalAmount : totalImportWithoutDiscount;
84
+ const subtotalAfterDiscountNet = taxMode === 'included' ? totalNetAfterDiscount : totalSubtotalAmount;
85
+ const subtotalBeforeDiscountGross = taxMode === 'included' ? totalImportWithoutDiscount : totalGrossBeforeDiscount;
86
+ const subtotalAfterDiscountGross = totalImportWithDiscount;
87
+ const pricingBreakdown = {
88
+ subtotalBeforeDiscountNet,
89
+ subtotalAfterDiscountNet,
90
+ subtotalBeforeDiscountGross,
91
+ subtotalAfterDiscountGross,
92
+ discountTotalNet: +(subtotalBeforeDiscountNet - subtotalAfterDiscountNet).toFixed(2),
93
+ discountTotalGross: +(subtotalBeforeDiscountGross - subtotalAfterDiscountGross).toFixed(2),
94
+ taxesTotal: totalImporTaxes,
95
+ shippingServiceTotal: shippingCost,
96
+ creditApplied,
97
+ redeemPointsApplied: redeemPointsDiscount,
98
+ totalBeforePaymentsGross: orderTotalWithOutCredit,
99
+ total: orderTotal,
100
+ };
101
+ // For included: non-NUMBER lineDiscountAmount is net (discountAmountPerUnit*qty), so
102
+ // totalDiscountAmount is mixed-semantic. Use discountTotalGross (gross-before minus gross-after)
103
+ // which is unambiguously user-facing in both modes.
104
+ const displayDiscount = taxMode === 'included' ? pricingBreakdown.discountTotalGross : pricingBreakdown.discountTotalNet;
105
+ const displayTotals = {
106
+ subtotal: totalImportWithoutDiscount, // display-safe: gross (included) or net (over_price) — both equal the price-tag amount
107
+ discount: displayDiscount,
108
+ taxesTotal: totalImporTaxes,
109
+ taxesLabel: taxMode === 'included' ? 'taxesIncluded' : 'taxesTotal',
110
+ shippingServiceTotal: shippingCost,
111
+ creditApplied,
112
+ redeemPointsApplied: redeemPointsDiscount,
113
+ total: orderTotal,
114
+ };
79
115
  // === RETURN FINAL OBJECT ===
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 });
116
+ 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, pricingBreakdown,
117
+ displayTotals });
81
118
  }
82
119
  catch (error) {
83
120
  throw error;
@@ -138,7 +138,8 @@ export const calculateTotalTaxesIncluded = (order, selectedCustomer, storeSettin
138
138
  productLineTaxesTotal: totalTaxesApplied,
139
139
  taxBreakdown,
140
140
  lineDiscountAmount: lineDiscount,
141
- productLineSubtotal: productNetTotalWithoutDiscount // Subtotal neto (sin IVA)
141
+ productLineSubtotal: productNetTotalWithoutDiscount, // Subtotal neto (sin IVA) — for included this is NET before discount
142
+ lineNetAfterDiscount: productNetTotalWithDiscount, // NET after discount — needed to derive pricingBreakdown
142
143
  };
143
144
  });
144
145
  return productTableImports;
@@ -115,18 +115,21 @@ export const calculateTotalTaxesOverPrice = (order, selectedCustomer, storeSetti
115
115
  const lineDiscount = +(productDiscount * qty).toFixed(2);
116
116
  // Precio total con descuento, reaplicando impuestos
117
117
  const prodLinePriceWithDiscount = +((subtotal * (1 + taxPercentage))).toFixed(2);
118
+ // GROSS before discount: net-before-discount * (1 + tax). Needed to derive pricingBreakdown.
119
+ const lineGrossBeforeDiscount = +((productBasePrice * qty + extraAmount) * (1 + taxPercentage)).toFixed(2);
118
120
  return {
119
121
  product: current,
120
122
  qty,
121
- // Total bruto original sin descuento: (precio base * cantidad) + extraAmount
123
+ // Total neto original sin descuento: (precio base * cantidad) + extraAmount
122
124
  productLineImportTotal: (productBasePrice * qty) + extraAmount,
123
125
  // Total final con descuento, con impuestos incluidos
124
126
  productLineTotalWithDiscount: prodLinePriceWithDiscount,
125
127
  productLineTaxesTotal: totalTaxesApplied,
126
128
  taxBreakdown,
127
129
  lineDiscountAmount: lineDiscount,
128
- // Subtotal neto (sin impuestos) con descuento aplicado
129
- productLineSubtotal: subtotal
130
+ // Subtotal neto (sin impuestos) con descuento aplicado — for over_price this is NET after discount
131
+ productLineSubtotal: subtotal,
132
+ lineGrossBeforeDiscount, // GROSS before discount — needed to derive pricingBreakdown
130
133
  };
131
134
  });
132
135
  return productTableImports;
@@ -236,6 +236,14 @@ export const getProductLineTotals = (productLinesTotals) => {
236
236
  .reduce((acum, line) => acum + line.productLineSubtotal, 0)
237
237
  .toFixed(2);
238
238
  const taxBreakdown = normalizeTaxBreakdownToTotal(productLinesTotals.flatMap((line) => line.taxBreakdown || []), totalImporTaxes);
239
+ // lineNetAfterDiscount is set by the included calculator (for over_price, productLineSubtotal IS net-after-discount)
240
+ // lineGrossBeforeDiscount is set by the over_price calculator (for included, productLineImportTotal IS gross-before-discount)
241
+ const totalNetAfterDiscount = +productLinesTotals
242
+ .reduce((acum, line) => { var _a; return acum + ((_a = line.lineNetAfterDiscount) !== null && _a !== void 0 ? _a : line.productLineSubtotal); }, 0)
243
+ .toFixed(2);
244
+ const totalGrossBeforeDiscount = +productLinesTotals
245
+ .reduce((acum, line) => { var _a; return acum + ((_a = line.lineGrossBeforeDiscount) !== null && _a !== void 0 ? _a : line.productLineImportTotal); }, 0)
246
+ .toFixed(2);
239
247
  return {
240
248
  totalQuantity,
241
249
  totalImportWithDiscount,
@@ -243,7 +251,9 @@ export const getProductLineTotals = (productLinesTotals) => {
243
251
  totalImporTaxes,
244
252
  totalDiscountAmount,
245
253
  totalSubtotalAmount,
246
- taxBreakdown
254
+ taxBreakdown,
255
+ totalNetAfterDiscount,
256
+ totalGrossBeforeDiscount,
247
257
  };
248
258
  };
249
259
  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.71",
3
+ "version": "1.6.73",
4
4
  "description": "Washday utilities functions and API",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
package/src/api/index.ts CHANGED
@@ -363,6 +363,7 @@ const WashdayClient: WashdayClientConstructor = function WashdayClient(this: Was
363
363
  getSuppliesReport: reportsExportEndpoints.getModule.getSuppliesReport,
364
364
  getDiscountCodesReport: reportsExportEndpoints.getModule.getDiscountCodesReport,
365
365
  getStaffReport: reportsExportEndpoints.getModule.getStaffReport,
366
+ getStaffActivityDetails: reportsExportEndpoints.getModule.getStaffActivityDetails,
366
367
  getProductSalesReport: reportsExportEndpoints.getModule.getProductSalesReport,
367
368
  getProductSalesDrillDown: reportsExportEndpoints.getModule.getProductSalesDrillDown,
368
369
  getPaymentLinesReport: reportsExportEndpoints.getModule.getPaymentLinesReport,
@@ -154,6 +154,27 @@ export const getStaffReport = async function (this: WashdayClientInstance, store
154
154
  }
155
155
  };
156
156
 
157
+ export const getStaffActivityDetails = async function (this: WashdayClientInstance, storeId: string, params: {
158
+ fromDate?: string
159
+ toDate?: string
160
+ employeeId?: string
161
+ }): Promise<any> {
162
+ try {
163
+ const config = {
164
+ headers: { Authorization: `Bearer ${this.apiToken}` }
165
+ };
166
+ const queryParams = generateQueryParamsStr([
167
+ 'fromDate',
168
+ 'toDate',
169
+ 'employeeId',
170
+ ], params);
171
+ return await this.axiosInstance.get(`${GET_SET_REPORTS}/${storeId}/staff/details?${queryParams}`, config);
172
+ } catch (error) {
173
+ console.error('Error fetching getStaffActivityDetails:', error);
174
+ throw error;
175
+ }
176
+ };
177
+
157
178
  export const getProductSalesReport = async function (this: WashdayClientInstance, storeId: string, params: {
158
179
  fromDate?: string
159
180
  toDate?: string
@@ -348,6 +348,7 @@ export interface WashdayClientInstance {
348
348
  getSuppliesReport: typeof reportsExportEndpoints.getModule.getSuppliesReport;
349
349
  getDiscountCodesReport: typeof reportsExportEndpoints.getModule.getDiscountCodesReport;
350
350
  getStaffReport: typeof reportsExportEndpoints.getModule.getStaffReport;
351
+ getStaffActivityDetails: typeof reportsExportEndpoints.getModule.getStaffActivityDetails;
351
352
  getProductSalesReport: typeof reportsExportEndpoints.getModule.getProductSalesReport;
352
353
  getProductSalesDrillDown: typeof reportsExportEndpoints.getModule.getProductSalesDrillDown;
353
354
  getPaymentLinesReport: typeof reportsExportEndpoints.getModule.getPaymentLinesReport;
@@ -13,6 +13,10 @@ export interface ProductLineTotals {
13
13
  taxBreakdown?: ITaxBreakdownItem[],
14
14
  lineDiscountAmount: number
15
15
  productLineSubtotal: number
16
+ // Additive semantic fields. Only the calculator that can compute these natively sets them;
17
+ // the other mode's fallback is encoded in getProductLineTotals.
18
+ lineNetAfterDiscount?: number // set by included calculator; for over_price productLineSubtotal already IS net-after-discount
19
+ lineGrossBeforeDiscount?: number // set by over_price calculator; for included productLineImportTotal already IS gross-before-discount
16
20
  }
17
21
 
18
22
  export interface IPriceOption {
@@ -926,5 +926,169 @@ describe("Calculate Order Total tests", () => {
926
926
  });
927
927
 
928
928
 
929
+ // ─── pricingBreakdown ───────────────────────────────────────────────────
930
+
931
+ it("pricingBreakdown: included, no discount — legacy fields unchanged, breakdown correct", () => {
932
+ ORDER_DEMO.store.taxesType = "included";
933
+ const { orderDto, customer, store, storeDiscounts, discountCodeObj } = ORDER_DEMO;
934
+ const result = calculateOrderTotal(orderDto, { customer }, store, false, storeDiscounts, discountCodeObj);
935
+
936
+ // Legacy fields must be preserved
937
+ expect(result.productTotal).toBe(54.22);
938
+ expect(result.productTotalWithoutDiscount).toBe(54.22);
939
+ expect(result.taxesTotal).toBe(8.68);
940
+ expect(result.subtotal).toBeCloseTo(46.74, 1); // net before discount for included
941
+
942
+ const bd = result.pricingBreakdown;
943
+ expect(bd.subtotalBeforeDiscountGross).toBe(54.22);
944
+ expect(bd.subtotalAfterDiscountGross).toBe(54.22);
945
+ expect(bd.discountTotalGross).toBe(0);
946
+ expect(bd.discountTotalNet).toBeCloseTo(0, 1);
947
+ expect(bd.taxesTotal).toBe(8.68);
948
+ expect(bd.totalBeforePaymentsGross).toBe(54.22);
949
+ expect(bd.total).toBeCloseTo(result.total, 2);
950
+
951
+ const dt = result.displayTotals;
952
+ expect(dt.subtotal).toBe(54.22);
953
+ expect(dt.taxesLabel).toBe("taxesIncluded");
954
+ expect(dt.total).toBeCloseTo(result.total, 2);
955
+ });
956
+
957
+ it("pricingBreakdown: over_price, no discount — legacy fields unchanged, breakdown correct", () => {
958
+ ORDER_DEMO.store.taxesType = "over_price";
959
+ const { orderDto, customer, store, storeDiscounts, discountCodeObj } = ORDER_DEMO;
960
+ const result = calculateOrderTotal(orderDto, { customer }, store, false, storeDiscounts, discountCodeObj);
961
+
962
+ // product: price=17, qty=3.7, tax=16% over_price
963
+ // net before discount = 17 * 3.7 = 62.9
964
+ // gross before discount = 62.9 * 1.16 = 72.964 ≈ 72.96
965
+ expect(result.productTotal).toBeCloseTo(72.96, 1);
966
+ expect(result.productTotalWithoutDiscount).toBeCloseTo(62.9, 2);
967
+
968
+ const bd = result.pricingBreakdown;
969
+ expect(bd.subtotalBeforeDiscountNet).toBeCloseTo(62.9, 2);
970
+ expect(bd.subtotalAfterDiscountNet).toBeCloseTo(62.9, 2);
971
+ expect(bd.subtotalBeforeDiscountGross).toBeCloseTo(72.96, 1);
972
+ expect(bd.subtotalAfterDiscountGross).toBeCloseTo(72.96, 1);
973
+ expect(bd.discountTotalNet).toBe(0);
974
+ expect(bd.discountTotalGross).toBeCloseTo(0, 1);
975
+
976
+ const dt = result.displayTotals;
977
+ expect(dt.subtotal).toBeCloseTo(62.9, 2); // net (= price tag for over_price)
978
+ expect(dt.taxesLabel).toBe("taxesTotal");
979
+ });
980
+
981
+ it("pricingBreakdown: included + NUMBER coupon $30 on $50 product (acceptance criteria issue#40)", () => {
982
+ // Setup: product price=$50 gross (tax=16% included), qty=1, NUMBER coupon=$30
983
+ ORDER_DEMO.store.taxesType = "included";
984
+ ORDER_DEMO.orderDto.products = [{
985
+ _id: "prod1",
986
+ name: "Test",
987
+ price: 50,
988
+ expressPrice: 50,
989
+ qty: 1,
990
+ quantity: 1,
991
+ extraAmount: 0,
992
+ taxExemptOne: false, taxExemptTwo: false, taxExemptThree: false,
993
+ discountAmount: 0,
994
+ }];
995
+ ORDER_DEMO.orderDto.buyAndGetProducts = [];
996
+ ORDER_DEMO.orderDto.discountCode = "THIRTY";
997
+ ORDER_DEMO.discountCodeObj = {
998
+ _id: "dc1",
999
+ type: "number",
1000
+ value: 30,
1001
+ applyToAllProducts: true,
1002
+ applyOnceOnOrder: true,
1003
+ products: [],
1004
+ freeProductSetting: [],
1005
+ buyAndGetConditions: [],
1006
+ };
1007
+
1008
+ const { orderDto, customer, store, storeDiscounts } = ORDER_DEMO;
1009
+ const result = calculateOrderTotal(orderDto, { customer }, store, false, storeDiscounts, ORDER_DEMO.discountCodeObj);
1010
+
1011
+ // gross before discount = 50, NUMBER $30 applied pre-tax
1012
+ // net before discount = 50/1.16 ≈ 43.10
1013
+ // gross after discount = 50 - 30 = 20
1014
+ // tax on $20 gross = 20 - 20/1.16 ≈ 2.76
1015
+ expect(result.productTotalWithoutDiscount).toBe(50); // gross-before-discount (display subtotal)
1016
+ expect(result.productTotal).toBeCloseTo(20, 1); // gross-after-discount
1017
+ expect(result.totalDiscountAmount).toBe(30); // user-facing discount
1018
+ expect(result.taxesTotal).toBeCloseTo(2.76, 1);
1019
+ expect(result.total).toBeCloseTo(20, 1);
1020
+
1021
+ const bd = result.pricingBreakdown;
1022
+ expect(bd.subtotalBeforeDiscountGross).toBe(50);
1023
+ expect(bd.subtotalAfterDiscountGross).toBeCloseTo(20, 1);
1024
+ expect(bd.discountTotalGross).toBeCloseTo(30, 1);
1025
+ expect(bd.subtotalBeforeDiscountNet).toBeCloseTo(43.1, 1);
1026
+ expect(bd.subtotalAfterDiscountNet).toBeCloseTo(17.24, 1);
1027
+ expect(bd.discountTotalNet).toBeCloseTo(25.86, 1);
1028
+ expect(bd.taxesTotal).toBeCloseTo(2.76, 1);
1029
+ expect(bd.totalBeforePaymentsGross).toBeCloseTo(20, 1);
1030
+ expect(bd.total).toBeCloseTo(20, 1);
1031
+
1032
+ const dt = result.displayTotals;
1033
+ expect(dt.subtotal).toBe(50);
1034
+ expect(dt.discount).toBe(30);
1035
+ expect(dt.taxesLabel).toBe("taxesIncluded");
1036
+ expect(dt.total).toBeCloseTo(20, 1);
1037
+ });
1038
+
1039
+ it("pricingBreakdown: over_price + NUMBER coupon $30 on $50 product (acceptance criteria issue#40)", () => {
1040
+ ORDER_DEMO.store.taxesType = "over_price";
1041
+ ORDER_DEMO.orderDto.products = [{
1042
+ _id: "prod1",
1043
+ name: "Test",
1044
+ price: 50,
1045
+ expressPrice: 50,
1046
+ qty: 1,
1047
+ quantity: 1,
1048
+ extraAmount: 0,
1049
+ taxExemptOne: false, taxExemptTwo: false, taxExemptThree: false,
1050
+ discountAmount: 0,
1051
+ }];
1052
+ ORDER_DEMO.orderDto.buyAndGetProducts = [];
1053
+ ORDER_DEMO.orderDto.discountCode = "THIRTY";
1054
+ ORDER_DEMO.discountCodeObj = {
1055
+ _id: "dc1",
1056
+ type: "number",
1057
+ value: 30,
1058
+ applyToAllProducts: true,
1059
+ applyOnceOnOrder: true,
1060
+ products: [],
1061
+ freeProductSetting: [],
1062
+ buyAndGetConditions: [],
1063
+ };
1064
+
1065
+ const { orderDto, customer, store, storeDiscounts } = ORDER_DEMO;
1066
+ const result = calculateOrderTotal(orderDto, { customer }, store, false, storeDiscounts, ORDER_DEMO.discountCodeObj);
1067
+
1068
+ // net before discount = 50; NUMBER $30 net; net after discount = 20
1069
+ // tax = 20 * 0.16 = 3.20; total = 23.20
1070
+ expect(result.productTotalWithoutDiscount).toBe(50); // net-before-discount (display subtotal for over_price)
1071
+ expect(result.totalDiscountAmount).toBe(30);
1072
+ expect(result.taxesTotal).toBeCloseTo(3.2, 1);
1073
+ expect(result.total).toBeCloseTo(23.2, 1);
1074
+
1075
+ const bd = result.pricingBreakdown;
1076
+ expect(bd.subtotalBeforeDiscountNet).toBe(50);
1077
+ expect(bd.subtotalAfterDiscountNet).toBeCloseTo(20, 1);
1078
+ expect(bd.discountTotalNet).toBeCloseTo(30, 1);
1079
+ expect(bd.subtotalBeforeDiscountGross).toBeCloseTo(58, 1); // 50 * 1.16
1080
+ expect(bd.subtotalAfterDiscountGross).toBeCloseTo(23.2, 1); // 20 * 1.16
1081
+ expect(bd.discountTotalGross).toBeCloseTo(34.8, 1); // 30 * 1.16
1082
+ expect(bd.taxesTotal).toBeCloseTo(3.2, 1);
1083
+ expect(bd.totalBeforePaymentsGross).toBeCloseTo(23.2, 1);
1084
+ expect(bd.total).toBeCloseTo(23.2, 1);
1085
+
1086
+ const dt = result.displayTotals;
1087
+ expect(dt.subtotal).toBe(50); // net = display price for over_price
1088
+ expect(dt.discount).toBe(30);
1089
+ expect(dt.taxesLabel).toBe("taxesTotal");
1090
+ expect(dt.total).toBeCloseTo(23.2, 1);
1091
+ });
1092
+
929
1093
  // Add more test cases as needed
930
1094
  });
@@ -106,7 +106,9 @@ export const calculateOrderTotal = (
106
106
  totalImporTaxes,
107
107
  totalDiscountAmount,
108
108
  totalSubtotalAmount,
109
- taxBreakdown
109
+ taxBreakdown,
110
+ totalNetAfterDiscount,
111
+ totalGrossBeforeDiscount,
110
112
  } = getProductLineTotals(productTableImports);
111
113
 
112
114
  // NOTE: the old post-tax "discountCodeAmount" subtraction was removed.
@@ -141,6 +143,50 @@ export const calculateOrderTotal = (
141
143
  redeemPointsDiscount
142
144
  ).toFixed(2);
143
145
 
146
+ // === PRICING BREAKDOWN ===
147
+ // For included: totalImportWithoutDiscount=gross-before-discount, totalSubtotalAmount=net-before-discount
148
+ // For over_price: totalImportWithoutDiscount=net-before-discount, totalSubtotalAmount=net-after-discount
149
+ const taxMode = storeSettings?.taxesType === 'over_price' ? 'over_price' : 'included';
150
+ const subtotalBeforeDiscountNet =
151
+ taxMode === 'included' ? totalSubtotalAmount : totalImportWithoutDiscount;
152
+ const subtotalAfterDiscountNet =
153
+ taxMode === 'included' ? totalNetAfterDiscount : totalSubtotalAmount;
154
+ const subtotalBeforeDiscountGross =
155
+ taxMode === 'included' ? totalImportWithoutDiscount : totalGrossBeforeDiscount;
156
+ const subtotalAfterDiscountGross = totalImportWithDiscount;
157
+
158
+ const pricingBreakdown = {
159
+ subtotalBeforeDiscountNet,
160
+ subtotalAfterDiscountNet,
161
+ subtotalBeforeDiscountGross,
162
+ subtotalAfterDiscountGross,
163
+ discountTotalNet: +(subtotalBeforeDiscountNet - subtotalAfterDiscountNet).toFixed(2),
164
+ discountTotalGross: +(subtotalBeforeDiscountGross - subtotalAfterDiscountGross).toFixed(2),
165
+ taxesTotal: totalImporTaxes,
166
+ shippingServiceTotal: shippingCost,
167
+ creditApplied,
168
+ redeemPointsApplied: redeemPointsDiscount,
169
+ totalBeforePaymentsGross: orderTotalWithOutCredit,
170
+ total: orderTotal,
171
+ };
172
+
173
+ // For included: non-NUMBER lineDiscountAmount is net (discountAmountPerUnit*qty), so
174
+ // totalDiscountAmount is mixed-semantic. Use discountTotalGross (gross-before minus gross-after)
175
+ // which is unambiguously user-facing in both modes.
176
+ const displayDiscount =
177
+ taxMode === 'included' ? pricingBreakdown.discountTotalGross : pricingBreakdown.discountTotalNet;
178
+
179
+ const displayTotals = {
180
+ subtotal: totalImportWithoutDiscount, // display-safe: gross (included) or net (over_price) — both equal the price-tag amount
181
+ discount: displayDiscount,
182
+ taxesTotal: totalImporTaxes,
183
+ taxesLabel: taxMode === 'included' ? 'taxesIncluded' as const : 'taxesTotal' as const,
184
+ shippingServiceTotal: shippingCost,
185
+ creditApplied,
186
+ redeemPointsApplied: redeemPointsDiscount,
187
+ total: orderTotal,
188
+ };
189
+
144
190
  // === RETURN FINAL OBJECT ===
145
191
  return {
146
192
  ...order,
@@ -151,12 +197,14 @@ export const calculateOrderTotal = (
151
197
  taxBreakdown,
152
198
  total: orderTotal,
153
199
  creditApplied,
154
- redeemPointsApplied: redeemPointsDiscount, // ✅ NUEVO CAMPO
200
+ redeemPointsApplied: redeemPointsDiscount,
155
201
  productTotalWithoutDiscount: totalImportWithoutDiscount,
156
202
  totalDiscountAmount: totalDiscountAmount,
157
203
  shippingServiceTotal: shippingCost,
158
204
  appliedOrderDiscounts,
159
- subtotal: totalSubtotalAmount
205
+ subtotal: totalSubtotalAmount,
206
+ pricingBreakdown,
207
+ displayTotals,
160
208
  };
161
209
  } catch (error) {
162
210
  throw error;
@@ -161,7 +161,8 @@ export const calculateTotalTaxesIncluded = (
161
161
  productLineTaxesTotal: totalTaxesApplied,
162
162
  taxBreakdown,
163
163
  lineDiscountAmount: lineDiscount,
164
- productLineSubtotal: productNetTotalWithoutDiscount // Subtotal neto (sin IVA)
164
+ productLineSubtotal: productNetTotalWithoutDiscount, // Subtotal neto (sin IVA) — for included this is NET before discount
165
+ lineNetAfterDiscount: productNetTotalWithDiscount, // NET after discount — needed to derive pricingBreakdown
165
166
  };
166
167
  });
167
168
  return productTableImports;
@@ -133,18 +133,22 @@ export const calculateTotalTaxesOverPrice = (
133
133
  // Precio total con descuento, reaplicando impuestos
134
134
  const prodLinePriceWithDiscount = +((subtotal * (1 + taxPercentage))).toFixed(2);
135
135
 
136
+ // GROSS before discount: net-before-discount * (1 + tax). Needed to derive pricingBreakdown.
137
+ const lineGrossBeforeDiscount = +((productBasePrice * qty + extraAmount) * (1 + taxPercentage)).toFixed(2);
138
+
136
139
  return {
137
140
  product: current,
138
141
  qty,
139
- // Total bruto original sin descuento: (precio base * cantidad) + extraAmount
142
+ // Total neto original sin descuento: (precio base * cantidad) + extraAmount
140
143
  productLineImportTotal: (productBasePrice * qty) + extraAmount,
141
144
  // Total final con descuento, con impuestos incluidos
142
145
  productLineTotalWithDiscount: prodLinePriceWithDiscount,
143
146
  productLineTaxesTotal: totalTaxesApplied,
144
147
  taxBreakdown,
145
148
  lineDiscountAmount: lineDiscount,
146
- // Subtotal neto (sin impuestos) con descuento aplicado
147
- productLineSubtotal: subtotal
149
+ // Subtotal neto (sin impuestos) con descuento aplicado — for over_price this is NET after discount
150
+ productLineSubtotal: subtotal,
151
+ lineGrossBeforeDiscount, // GROSS before discount — needed to derive pricingBreakdown
148
152
  };
149
153
  });
150
154
  return productTableImports;
@@ -317,6 +317,15 @@ export const getProductLineTotals = (productLinesTotals: ProductLineTotals[]) =>
317
317
  totalImporTaxes
318
318
  );
319
319
 
320
+ // lineNetAfterDiscount is set by the included calculator (for over_price, productLineSubtotal IS net-after-discount)
321
+ // lineGrossBeforeDiscount is set by the over_price calculator (for included, productLineImportTotal IS gross-before-discount)
322
+ const totalNetAfterDiscount = +productLinesTotals
323
+ .reduce((acum, line) => acum + (line.lineNetAfterDiscount ?? line.productLineSubtotal), 0)
324
+ .toFixed(2);
325
+ const totalGrossBeforeDiscount = +productLinesTotals
326
+ .reduce((acum, line) => acum + (line.lineGrossBeforeDiscount ?? line.productLineImportTotal), 0)
327
+ .toFixed(2);
328
+
320
329
  return {
321
330
  totalQuantity,
322
331
  totalImportWithDiscount,
@@ -324,7 +333,9 @@ export const getProductLineTotals = (productLinesTotals: ProductLineTotals[]) =>
324
333
  totalImporTaxes,
325
334
  totalDiscountAmount,
326
335
  totalSubtotalAmount,
327
- taxBreakdown
336
+ taxBreakdown,
337
+ totalNetAfterDiscount,
338
+ totalGrossBeforeDiscount,
328
339
  };
329
340
  };
330
341
 
@@ -0,0 +1,294 @@
1
+ import { utils } from "../src";
2
+
3
+ const noCustomer = { customer: { credit: 0, discount: 0 } };
4
+
5
+ const storeIncluded = {
6
+ taxesType: "included",
7
+ taxOne: { name: "IVA", value: 16 },
8
+ orderPageConfig: { shippingServiceCost: 0 },
9
+ } as any;
10
+
11
+ const storeOverPrice = {
12
+ taxesType: "over_price",
13
+ taxOne: { name: "IVA", value: 16 },
14
+ orderPageConfig: { shippingServiceCost: 0 },
15
+ } as any;
16
+
17
+ const singleProduct = (price: number, qty = 1) => ({
18
+ express: false,
19
+ products: [
20
+ {
21
+ _id: "p1",
22
+ qty,
23
+ quantity: qty,
24
+ price,
25
+ expressPrice: price,
26
+ extraAmount: 0,
27
+ taxExemptOne: false,
28
+ taxExemptTwo: false,
29
+ taxExemptThree: false,
30
+ discountAmount: 0,
31
+ },
32
+ ],
33
+ buyAndGetProducts: [],
34
+ discountCode: null,
35
+ });
36
+
37
+ const numberCoupon = (value: number) => ({
38
+ _id: "dc1",
39
+ type: "number",
40
+ value,
41
+ applyToAllProducts: true,
42
+ applyOnceOnOrder: true,
43
+ products: [],
44
+ freeProductSetting: [],
45
+ buyAndGetConditions: [],
46
+ });
47
+
48
+ describe("pricingBreakdown and displayTotals", () => {
49
+ describe("included mode — no discount", () => {
50
+ it("all gross fields equal productTotal; net fields are net; legacy fields unchanged", () => {
51
+ const result = utils.calculateOrderTotal(
52
+ { ...singleProduct(50), discountCode: null },
53
+ noCustomer as any,
54
+ storeIncluded,
55
+ false,
56
+ [],
57
+ null,
58
+ 0,
59
+ );
60
+
61
+ // Legacy fields
62
+ expect(result.productTotalWithoutDiscount).toBe(50);
63
+ expect(result.productTotal).toBe(50);
64
+ expect(result.totalDiscountAmount).toBe(0);
65
+
66
+ const bd = result.pricingBreakdown;
67
+ expect(bd.subtotalBeforeDiscountGross).toBe(50);
68
+ expect(bd.subtotalAfterDiscountGross).toBe(50);
69
+ expect(bd.discountTotalGross).toBe(0);
70
+ expect(bd.subtotalBeforeDiscountNet).toBeCloseTo(43.1, 1);
71
+ expect(bd.subtotalAfterDiscountNet).toBeCloseTo(43.1, 1);
72
+ expect(bd.discountTotalNet).toBeCloseTo(0, 2);
73
+
74
+ const dt = result.displayTotals;
75
+ expect(dt.subtotal).toBe(50);
76
+ expect(dt.taxesLabel).toBe("taxesIncluded");
77
+ expect(dt.discount).toBe(0);
78
+ });
79
+ });
80
+
81
+ describe("over_price mode — no discount", () => {
82
+ it("net fields equal productTotalWithoutDiscount; gross fields = net * 1.16; legacy unchanged", () => {
83
+ const result = utils.calculateOrderTotal(
84
+ { ...singleProduct(50), discountCode: null },
85
+ noCustomer as any,
86
+ storeOverPrice,
87
+ false,
88
+ [],
89
+ null,
90
+ 0,
91
+ );
92
+
93
+ expect(result.productTotalWithoutDiscount).toBe(50);
94
+ expect(result.productTotal).toBeCloseTo(58, 0); // 50 * 1.16
95
+
96
+ const bd = result.pricingBreakdown;
97
+ expect(bd.subtotalBeforeDiscountNet).toBe(50);
98
+ expect(bd.subtotalAfterDiscountNet).toBe(50);
99
+ expect(bd.discountTotalNet).toBe(0);
100
+ expect(bd.subtotalBeforeDiscountGross).toBeCloseTo(58, 0);
101
+ expect(bd.subtotalAfterDiscountGross).toBeCloseTo(58, 0);
102
+ expect(bd.discountTotalGross).toBeCloseTo(0, 1);
103
+
104
+ const dt = result.displayTotals;
105
+ expect(dt.subtotal).toBe(50);
106
+ expect(dt.taxesLabel).toBe("taxesTotal");
107
+ expect(dt.discount).toBe(0);
108
+ });
109
+ });
110
+
111
+ describe("included mode + NUMBER $30 on $50 product (issue #40 acceptance criteria)", () => {
112
+ it("gross fields correct; net fields correct; display subtotal = $50", () => {
113
+ const order = { ...singleProduct(50), discountCode: "THIRTY" };
114
+ const coupon = numberCoupon(30);
115
+
116
+ const result = utils.calculateOrderTotal(
117
+ order as any,
118
+ noCustomer as any,
119
+ storeIncluded,
120
+ false,
121
+ [],
122
+ coupon as any,
123
+ 0,
124
+ );
125
+
126
+ // Legacy fields must be preserved
127
+ expect(result.productTotalWithoutDiscount).toBe(50);
128
+ expect(result.productTotal).toBeCloseTo(20, 1);
129
+ expect(result.totalDiscountAmount).toBe(30);
130
+ expect(result.taxesTotal).toBeCloseTo(2.76, 1);
131
+ expect(result.total).toBeCloseTo(20, 1);
132
+
133
+ const bd = result.pricingBreakdown;
134
+ expect(bd.subtotalBeforeDiscountGross).toBe(50);
135
+ expect(bd.subtotalAfterDiscountGross).toBeCloseTo(20, 1);
136
+ expect(bd.discountTotalGross).toBeCloseTo(30, 1);
137
+ expect(bd.subtotalBeforeDiscountNet).toBeCloseTo(43.1, 1);
138
+ expect(bd.subtotalAfterDiscountNet).toBeCloseTo(17.24, 1);
139
+ expect(bd.discountTotalNet).toBeCloseTo(25.86, 1);
140
+ expect(bd.taxesTotal).toBeCloseTo(2.76, 1);
141
+ expect(bd.totalBeforePaymentsGross).toBeCloseTo(20, 1);
142
+ expect(bd.total).toBeCloseTo(20, 1);
143
+
144
+ const dt = result.displayTotals;
145
+ expect(dt.subtotal).toBe(50);
146
+ expect(dt.discount).toBe(30);
147
+ expect(dt.taxesLabel).toBe("taxesIncluded");
148
+ expect(dt.total).toBeCloseTo(20, 1);
149
+ });
150
+ });
151
+
152
+ describe("over_price mode + NUMBER $30 on $50 product (issue #40 acceptance criteria)", () => {
153
+ it("net fields correct; gross fields = net * 1.16; display subtotal = $50", () => {
154
+ const order = { ...singleProduct(50), discountCode: "THIRTY" };
155
+ const coupon = numberCoupon(30);
156
+
157
+ const result = utils.calculateOrderTotal(
158
+ order as any,
159
+ noCustomer as any,
160
+ storeOverPrice,
161
+ false,
162
+ [],
163
+ coupon as any,
164
+ 0,
165
+ );
166
+
167
+ // Legacy fields
168
+ expect(result.productTotalWithoutDiscount).toBe(50);
169
+ expect(result.totalDiscountAmount).toBe(30);
170
+ expect(result.taxesTotal).toBeCloseTo(3.2, 1);
171
+ expect(result.total).toBeCloseTo(23.2, 1);
172
+
173
+ const bd = result.pricingBreakdown;
174
+ expect(bd.subtotalBeforeDiscountNet).toBe(50);
175
+ expect(bd.subtotalAfterDiscountNet).toBeCloseTo(20, 1);
176
+ expect(bd.discountTotalNet).toBeCloseTo(30, 1);
177
+ expect(bd.subtotalBeforeDiscountGross).toBeCloseTo(58, 0);
178
+ expect(bd.subtotalAfterDiscountGross).toBeCloseTo(23.2, 1);
179
+ expect(bd.discountTotalGross).toBeCloseTo(34.8, 1);
180
+ expect(bd.taxesTotal).toBeCloseTo(3.2, 1);
181
+ expect(bd.totalBeforePaymentsGross).toBeCloseTo(23.2, 1);
182
+ expect(bd.total).toBeCloseTo(23.2, 1);
183
+
184
+ const dt = result.displayTotals;
185
+ expect(dt.subtotal).toBe(50);
186
+ expect(dt.discount).toBe(30);
187
+ expect(dt.taxesLabel).toBe("taxesTotal");
188
+ expect(dt.total).toBeCloseTo(23.2, 1);
189
+ });
190
+ });
191
+
192
+ describe("displayTotals.discount — included + PERCENTAGE coupon", () => {
193
+ it("uses discountTotalGross (not net lineDiscountAmount) as display discount", () => {
194
+ // 20% PERCENTAGE coupon on $50 gross (included, 16% tax)
195
+ // net-before-discount = 50/1.16 ≈ 43.10; discount net = 43.10*0.20 ≈ 8.62
196
+ // gross-before-discount = 50; gross-after-discount = 50*0.80 = 40
197
+ // discountTotalGross = 50 - 40 = 10 (user sees "$10 off")
198
+ // discountTotalNet = 43.10 - 34.48 ≈ 8.62
199
+ const order = {
200
+ ...singleProduct(50),
201
+ discountCode: "PCT20",
202
+ };
203
+ const percentageCoupon = {
204
+ _id: "dc2",
205
+ type: "percentage",
206
+ value: 20,
207
+ applyToAllProducts: true,
208
+ applyOnceOnOrder: false,
209
+ products: [],
210
+ freeProductSetting: [],
211
+ buyAndGetConditions: [],
212
+ };
213
+
214
+ const result = utils.calculateOrderTotal(
215
+ order as any,
216
+ noCustomer as any,
217
+ storeIncluded,
218
+ false,
219
+ [],
220
+ percentageCoupon as any,
221
+ 0,
222
+ );
223
+
224
+ const bd = result.pricingBreakdown;
225
+ const dt = result.displayTotals;
226
+
227
+ expect(bd.discountTotalGross).toBeCloseTo(10, 1);
228
+ expect(bd.discountTotalNet).toBeCloseTo(8.62, 1);
229
+
230
+ // displayTotals.discount must equal discountTotalGross for included mode
231
+ expect(dt.discount).toBeCloseTo(10, 1);
232
+ expect(dt.discount).toBe(bd.discountTotalGross);
233
+ });
234
+ });
235
+
236
+ describe("displayTotals.discount — over_price + PERCENTAGE coupon", () => {
237
+ it("uses discountTotalNet (= user-facing discount for over_price) as display discount", () => {
238
+ // 20% PERCENTAGE coupon on $50 net (over_price, 16% tax)
239
+ // discount net = 50*0.20 = 10; net-after = 40; gross-after = 40*1.16 = 46.40
240
+ const order = {
241
+ ...singleProduct(50),
242
+ discountCode: "PCT20",
243
+ };
244
+ const percentageCoupon = {
245
+ _id: "dc2",
246
+ type: "percentage",
247
+ value: 20,
248
+ applyToAllProducts: true,
249
+ applyOnceOnOrder: false,
250
+ products: [],
251
+ freeProductSetting: [],
252
+ buyAndGetConditions: [],
253
+ };
254
+
255
+ const result = utils.calculateOrderTotal(
256
+ order as any,
257
+ noCustomer as any,
258
+ storeOverPrice,
259
+ false,
260
+ [],
261
+ percentageCoupon as any,
262
+ 0,
263
+ );
264
+
265
+ const bd = result.pricingBreakdown;
266
+ const dt = result.displayTotals;
267
+
268
+ expect(bd.discountTotalNet).toBeCloseTo(10, 1);
269
+
270
+ // displayTotals.discount must equal discountTotalNet for over_price mode
271
+ expect(dt.discount).toBeCloseTo(10, 1);
272
+ expect(dt.discount).toBe(bd.discountTotalNet);
273
+ });
274
+ });
275
+
276
+ describe("shipping included in totalBeforePaymentsGross", () => {
277
+ it("included: totalBeforePaymentsGross = subtotalAfterDiscountGross + shipping", () => {
278
+ const storeWithShipping = { ...storeIncluded, orderPageConfig: { shippingServiceCost: 10 } };
279
+ const result = utils.calculateOrderTotal(
280
+ { ...singleProduct(50), discountCode: null } as any,
281
+ noCustomer as any,
282
+ storeWithShipping,
283
+ true, // hasShippingCost
284
+ [],
285
+ null,
286
+ 0,
287
+ );
288
+
289
+ const bd = result.pricingBreakdown;
290
+ expect(bd.shippingServiceTotal).toBe(10);
291
+ expect(bd.totalBeforePaymentsGross).toBeCloseTo(60, 1); // 50 + 10
292
+ });
293
+ });
294
+ });