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 +1 -0
- package/dist/api/reports/get.js +19 -0
- package/dist/utils/orders/calculateOrderTotal.js +39 -2
- package/dist/utils/orders/calculateTotalTaxesIncluded.js +2 -1
- package/dist/utils/orders/calculateTotalTaxesOverPrice.js +6 -3
- package/dist/utils/orders/helpers.js +11 -1
- package/package.json +1 -1
- package/src/api/index.ts +1 -0
- package/src/api/reports/get.ts +21 -0
- package/src/interfaces/Api.ts +1 -0
- package/src/interfaces/Product.ts +4 -0
- package/src/utils/orders/calculateOrderTotal.test.js +164 -0
- package/src/utils/orders/calculateOrderTotal.ts +51 -3
- package/src/utils/orders/calculateTotalTaxesIncluded.ts +2 -1
- package/src/utils/orders/calculateTotalTaxesOverPrice.ts +7 -3
- package/src/utils/orders/helpers.ts +12 -1
- package/test/orders.pricingBreakdown.test.ts +294 -0
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,
|
package/dist/api/reports/get.js
CHANGED
|
@@ -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
|
|
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
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,
|
package/src/api/reports/get.ts
CHANGED
|
@@ -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
|
package/src/interfaces/Api.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
+
});
|