washday-sdk 1.6.63 → 1.6.65
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/companies/get.js +20 -0
- package/dist/api/companies/post.js +30 -0
- package/dist/api/index.js +5 -2
- package/dist/utils/orders/calculateOrderTotal.js +2 -2
- package/dist/utils/orders/calculateTotalTaxesIncluded.js +7 -1
- package/dist/utils/orders/calculateTotalTaxesOverPrice.js +7 -1
- package/dist/utils/orders/helpers.js +100 -1
- package/package.json +1 -1
- package/src/api/companies/get.ts +28 -1
- package/src/api/companies/post.ts +37 -1
- package/src/api/index.ts +5 -2
- package/src/index.ts +7 -0
- package/src/interfaces/Api.ts +5 -2
- package/src/interfaces/Company.ts +91 -0
- package/src/interfaces/Order.ts +57 -0
- package/src/interfaces/Product.ts +2 -0
- package/src/utils/orders/calculateOrderTotal.ts +3 -1
- package/src/utils/orders/calculateTotalTaxesIncluded.ts +7 -1
- package/src/utils/orders/calculateTotalTaxesOverPrice.ts +7 -1
- package/src/utils/orders/helpers.ts +155 -1
- package/test/companies.subscriptionPlanChange.test.ts +92 -0
- package/test/orders.calculateOrderTotalTaxBreakdown.test.ts +147 -0
|
@@ -69,3 +69,23 @@ export const getCFDIAddonStatus = function (data) {
|
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
71
|
};
|
|
72
|
+
export const getAvailableSubscriptionPlans = function () {
|
|
73
|
+
return __awaiter(this, arguments, void 0, function* (data = {}) {
|
|
74
|
+
try {
|
|
75
|
+
const config = {
|
|
76
|
+
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
77
|
+
};
|
|
78
|
+
const params = new URLSearchParams();
|
|
79
|
+
if (data.companyId) {
|
|
80
|
+
params.set("companyId", data.companyId);
|
|
81
|
+
}
|
|
82
|
+
const query = params.toString();
|
|
83
|
+
const response = yield this.axiosInstance.get(`${GET_SET_BILLING}/subscription/available-plans${query ? `?${query}` : ""}`, config);
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error('Error fetching getAvailableSubscriptionPlans:', error);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
};
|
|
@@ -99,3 +99,33 @@ export const disableCFDIAddon = function (data) {
|
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
};
|
|
102
|
+
export const previewSubscriptionPlanChange = function (data) {
|
|
103
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
try {
|
|
105
|
+
const config = {
|
|
106
|
+
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
107
|
+
};
|
|
108
|
+
const response = yield this.axiosInstance.post(`${GET_SET_BILLING}/subscription/change-plan/preview`, data, config);
|
|
109
|
+
return response;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error('Error fetching previewSubscriptionPlanChange:', error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
export const changeSubscriptionPlan = function (data) {
|
|
118
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
try {
|
|
120
|
+
const config = {
|
|
121
|
+
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
122
|
+
};
|
|
123
|
+
const response = yield this.axiosInstance.post(`${GET_SET_BILLING}/subscription/change-plan`, data, config);
|
|
124
|
+
return response;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error('Error fetching changeSubscriptionPlan:', error);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
};
|
package/dist/api/index.js
CHANGED
|
@@ -2,8 +2,8 @@ import { deleteCashierBoxById, deleteCashierBoxMovementById } from "./cashierbox
|
|
|
2
2
|
import { getCashierBoxMovementsHistory, getCashierboxesById, getCashierboxesByStoreId } from "./cashierbox/get";
|
|
3
3
|
import { addCashierBoxMovement, createCashierBox } from "./cashierbox/post";
|
|
4
4
|
import { updateCashierBoxById, updateCashierBoxMovementById } from "./cashierbox/put";
|
|
5
|
-
import { getCFDIAddonStatus, getCompanyById, getCompanyOrdersMetrics, getGarmentAttributeCatalogs } from "./companies/get";
|
|
6
|
-
import { disableBillingOverage, disableCFDIAddon, enableBillingOverage, enableCFDIAddon, updateCFDIOrgLogo, updateCompanyTaxInfoCertificates } from "./companies/post";
|
|
5
|
+
import { getAvailableSubscriptionPlans, getCFDIAddonStatus, getCompanyById, getCompanyOrdersMetrics, getGarmentAttributeCatalogs } from "./companies/get";
|
|
6
|
+
import { changeSubscriptionPlan, disableBillingOverage, disableCFDIAddon, enableBillingOverage, enableCFDIAddon, previewSubscriptionPlanChange, updateCFDIOrgLogo, updateCompanyTaxInfoCertificates } from "./companies/post";
|
|
7
7
|
import { updateCompanyById, updateCompanyLogoById, updateCompanySubscriptionBillingInfoById, updateCompanyTaxInfoById, updateGarmentAttributeCatalogsById } from "./companies/put";
|
|
8
8
|
import { getCountries } from "./countries/get";
|
|
9
9
|
import * as configEndpoints from "./config";
|
|
@@ -288,6 +288,9 @@ const WashdayClient = function WashdayClient(apiToken, env = 'PROD', clientId, c
|
|
|
288
288
|
enableCFDIAddon: enableCFDIAddon,
|
|
289
289
|
disableCFDIAddon: disableCFDIAddon,
|
|
290
290
|
getCFDIAddonStatus: getCFDIAddonStatus,
|
|
291
|
+
getAvailableSubscriptionPlans: getAvailableSubscriptionPlans,
|
|
292
|
+
previewSubscriptionPlanChange: previewSubscriptionPlanChange,
|
|
293
|
+
changeSubscriptionPlan: changeSubscriptionPlan,
|
|
291
294
|
});
|
|
292
295
|
this.stripe = bindMethods(this, {
|
|
293
296
|
createCreateSuscriptionCheckoutSession: createCreateSuscriptionCheckoutSession,
|
|
@@ -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
package/src/api/companies/get.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { WashdayClientInstance } from "../../interfaces/Api";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
GetAvailableSubscriptionPlansRequest,
|
|
4
|
+
GetCFDIAddonStatusRequest,
|
|
5
|
+
} from "../../interfaces/Company";
|
|
3
6
|
|
|
4
7
|
const GET_SET_COMPANIES = 'api/company';
|
|
5
8
|
const GET_SET_BILLING = 'api/billing';
|
|
@@ -58,3 +61,27 @@ export const getCFDIAddonStatus = async function (
|
|
|
58
61
|
throw error;
|
|
59
62
|
}
|
|
60
63
|
};
|
|
64
|
+
|
|
65
|
+
export const getAvailableSubscriptionPlans = async function (
|
|
66
|
+
this: WashdayClientInstance,
|
|
67
|
+
data: GetAvailableSubscriptionPlansRequest = {},
|
|
68
|
+
): Promise<any> {
|
|
69
|
+
try {
|
|
70
|
+
const config = {
|
|
71
|
+
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
72
|
+
};
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
if (data.companyId) {
|
|
75
|
+
params.set("companyId", data.companyId);
|
|
76
|
+
}
|
|
77
|
+
const query = params.toString();
|
|
78
|
+
const response = await this.axiosInstance.get(
|
|
79
|
+
`${GET_SET_BILLING}/subscription/available-plans${query ? `?${query}` : ""}`,
|
|
80
|
+
config,
|
|
81
|
+
);
|
|
82
|
+
return response;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error fetching getAvailableSubscriptionPlans:', error);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { WashdayClientInstance } from "../../interfaces/Api";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ApplySubscriptionPlanChangeRequest,
|
|
4
|
+
CFDIAddonToggleRequest,
|
|
5
|
+
PreviewSubscriptionPlanChangeRequest,
|
|
6
|
+
} from "../../interfaces/Company";
|
|
3
7
|
import axiosInstance from "../axiosInstance";
|
|
4
8
|
const GET_SET_COMPANIES = 'api/company';
|
|
5
9
|
const GET_SET_BILLING = 'api/billing';
|
|
@@ -91,3 +95,35 @@ export const disableCFDIAddon = async function (
|
|
|
91
95
|
throw error;
|
|
92
96
|
}
|
|
93
97
|
}
|
|
98
|
+
|
|
99
|
+
export const previewSubscriptionPlanChange = async function (
|
|
100
|
+
this: WashdayClientInstance,
|
|
101
|
+
data: PreviewSubscriptionPlanChangeRequest
|
|
102
|
+
): Promise<any> {
|
|
103
|
+
try {
|
|
104
|
+
const config = {
|
|
105
|
+
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
106
|
+
};
|
|
107
|
+
const response = await this.axiosInstance.post(`${GET_SET_BILLING}/subscription/change-plan/preview`, data, config);
|
|
108
|
+
return response;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Error fetching previewSubscriptionPlanChange:', error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const changeSubscriptionPlan = async function (
|
|
116
|
+
this: WashdayClientInstance,
|
|
117
|
+
data: ApplySubscriptionPlanChangeRequest
|
|
118
|
+
): Promise<any> {
|
|
119
|
+
try {
|
|
120
|
+
const config = {
|
|
121
|
+
headers: { Authorization: `Bearer ${this.apiToken}` }
|
|
122
|
+
};
|
|
123
|
+
const response = await this.axiosInstance.post(`${GET_SET_BILLING}/subscription/change-plan`, data, config);
|
|
124
|
+
return response;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('Error fetching changeSubscriptionPlan:', error);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/api/index.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { deleteCashierBoxById, deleteCashierBoxMovementById } from "./cashierbox
|
|
|
3
3
|
import { getCashierBoxMovementsHistory, getCashierboxesById, getCashierboxesByStoreId } from "./cashierbox/get";
|
|
4
4
|
import { addCashierBoxMovement, createCashierBox } from "./cashierbox/post";
|
|
5
5
|
import { updateCashierBoxById, updateCashierBoxMovementById } from "./cashierbox/put";
|
|
6
|
-
import { getCFDIAddonStatus, getCompanyById, getCompanyOrdersMetrics, getGarmentAttributeCatalogs } from "./companies/get";
|
|
7
|
-
import { disableBillingOverage, disableCFDIAddon, enableBillingOverage, enableCFDIAddon, updateCFDIOrgLogo, updateCompanyTaxInfoCertificates } from "./companies/post";
|
|
6
|
+
import { getAvailableSubscriptionPlans, getCFDIAddonStatus, getCompanyById, getCompanyOrdersMetrics, getGarmentAttributeCatalogs } from "./companies/get";
|
|
7
|
+
import { changeSubscriptionPlan, disableBillingOverage, disableCFDIAddon, enableBillingOverage, enableCFDIAddon, previewSubscriptionPlanChange, updateCFDIOrgLogo, updateCompanyTaxInfoCertificates } from "./companies/post";
|
|
8
8
|
import { updateCompanyById, updateCompanyLogoById, updateCompanySubscriptionBillingInfoById, updateCompanyTaxInfoById, updateGarmentAttributeCatalogsById } from "./companies/put";
|
|
9
9
|
import { getCountries } from "./countries/get";
|
|
10
10
|
import * as configEndpoints from "./config";
|
|
@@ -295,6 +295,9 @@ const WashdayClient: WashdayClientConstructor = function WashdayClient(this: Was
|
|
|
295
295
|
enableCFDIAddon: enableCFDIAddon,
|
|
296
296
|
disableCFDIAddon: disableCFDIAddon,
|
|
297
297
|
getCFDIAddonStatus: getCFDIAddonStatus,
|
|
298
|
+
getAvailableSubscriptionPlans: getAvailableSubscriptionPlans,
|
|
299
|
+
previewSubscriptionPlanChange: previewSubscriptionPlanChange,
|
|
300
|
+
changeSubscriptionPlan: changeSubscriptionPlan,
|
|
298
301
|
});
|
|
299
302
|
this.stripe = bindMethods(this, {
|
|
300
303
|
createCreateSuscriptionCheckoutSession: createCreateSuscriptionCheckoutSession,
|
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,
|
package/src/interfaces/Api.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { deleteCashierBoxById, deleteCashierBoxMovementById } from "../api/cashi
|
|
|
2
2
|
import { getCashierBoxMovementsHistory, getCashierboxesById, getCashierboxesByStoreId } from "../api/cashierbox/get";
|
|
3
3
|
import { addCashierBoxMovement, createCashierBox } from "../api/cashierbox/post";
|
|
4
4
|
import { updateCashierBoxById, updateCashierBoxMovementById } from "../api/cashierbox/put";
|
|
5
|
-
import { getCFDIAddonStatus, getCompanyById, getCompanyOrdersMetrics, getGarmentAttributeCatalogs } from "../api/companies/get";
|
|
6
|
-
import { disableBillingOverage, disableCFDIAddon, enableBillingOverage, enableCFDIAddon, updateCFDIOrgLogo, updateCompanyTaxInfoCertificates } from "../api/companies/post";
|
|
5
|
+
import { getAvailableSubscriptionPlans, getCFDIAddonStatus, getCompanyById, getCompanyOrdersMetrics, getGarmentAttributeCatalogs } from "../api/companies/get";
|
|
6
|
+
import { changeSubscriptionPlan, disableBillingOverage, disableCFDIAddon, enableBillingOverage, enableCFDIAddon, previewSubscriptionPlanChange, updateCFDIOrgLogo, updateCompanyTaxInfoCertificates } from "../api/companies/post";
|
|
7
7
|
import { updateCompanyById, updateCompanyLogoById, updateCompanySubscriptionBillingInfoById, updateCompanyTaxInfoById, updateGarmentAttributeCatalogsById } from "../api/companies/put";
|
|
8
8
|
import { getCountries } from "../api/countries/get";
|
|
9
9
|
import * as configEndpoints from "../api/config";
|
|
@@ -280,6 +280,9 @@ export interface WashdayClientInstance {
|
|
|
280
280
|
enableCFDIAddon: typeof enableCFDIAddon;
|
|
281
281
|
disableCFDIAddon: typeof disableCFDIAddon;
|
|
282
282
|
getCFDIAddonStatus: typeof getCFDIAddonStatus;
|
|
283
|
+
getAvailableSubscriptionPlans: typeof getAvailableSubscriptionPlans;
|
|
284
|
+
previewSubscriptionPlanChange: typeof previewSubscriptionPlanChange;
|
|
285
|
+
changeSubscriptionPlan: typeof changeSubscriptionPlan;
|
|
283
286
|
};
|
|
284
287
|
stripe: {
|
|
285
288
|
createCreateSuscriptionCheckoutSession: typeof createCreateSuscriptionCheckoutSession;
|
|
@@ -54,7 +54,98 @@ export interface GetCFDIAddonStatusRequest {
|
|
|
54
54
|
companyId: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export type SubscriptionPlanChangePlan = "basic" | "standard" | "premium";
|
|
58
|
+
export type SubscriptionPlanChangeDirection = "upgrade" | "downgrade" | "same_plan";
|
|
59
|
+
export type SubscriptionPlanChangeBehavior =
|
|
60
|
+
| "immediate_prorated_charge"
|
|
61
|
+
| "contact_support"
|
|
62
|
+
| "no_change";
|
|
63
|
+
export type SubscriptionPlanChangeAddon = "cfdi" | "orders_overage";
|
|
64
|
+
|
|
65
|
+
export interface GetAvailableSubscriptionPlansRequest {
|
|
66
|
+
companyId?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AvailableSubscriptionPlan {
|
|
70
|
+
plan: SubscriptionPlanChangePlan;
|
|
71
|
+
priceId: string;
|
|
72
|
+
amount: number;
|
|
73
|
+
currency: string;
|
|
74
|
+
interval: string | null;
|
|
75
|
+
displayPrice: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface AvailableSubscriptionPlansResponse {
|
|
79
|
+
currentPlan: SubscriptionPlanChangePlan;
|
|
80
|
+
currentPlanPriceId: string;
|
|
81
|
+
country: string;
|
|
82
|
+
currency: string;
|
|
83
|
+
plans: AvailableSubscriptionPlan[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface PreviewSubscriptionPlanChangeRequest {
|
|
87
|
+
companyId?: string;
|
|
88
|
+
targetPlan: SubscriptionPlanChangePlan;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface PlanChangePreviewLine {
|
|
92
|
+
description: string;
|
|
93
|
+
amount: number;
|
|
94
|
+
currency: string;
|
|
95
|
+
amountFormatted?: string;
|
|
96
|
+
proration?: boolean;
|
|
97
|
+
priceId?: string;
|
|
98
|
+
type?: "credit" | "charge" | "tax" | "addon" | "plan";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface PlanChangePreviewResponse {
|
|
102
|
+
status: "preview_ready" | "downgrade_not_supported" | "no_change";
|
|
103
|
+
currentPlan: SubscriptionPlanChangePlan;
|
|
104
|
+
targetPlan: SubscriptionPlanChangePlan;
|
|
105
|
+
direction: SubscriptionPlanChangeDirection;
|
|
106
|
+
behavior: SubscriptionPlanChangeBehavior;
|
|
107
|
+
prorationDate?: number;
|
|
108
|
+
currency: string;
|
|
109
|
+
amountDueNow: number;
|
|
110
|
+
amountDueNowFormatted?: string;
|
|
111
|
+
subtotal?: number;
|
|
112
|
+
tax?: number | null;
|
|
113
|
+
total?: number;
|
|
114
|
+
currentPeriodEnd?: string;
|
|
115
|
+
nextBillingDate?: string;
|
|
116
|
+
currentPlanPriceId?: string;
|
|
117
|
+
targetPlanPriceId: string;
|
|
118
|
+
preservedAddons: SubscriptionPlanChangeAddon[];
|
|
119
|
+
lines: PlanChangePreviewLine[];
|
|
120
|
+
message: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ApplySubscriptionPlanChangeRequest {
|
|
124
|
+
companyId?: string;
|
|
125
|
+
targetPlan: SubscriptionPlanChangePlan;
|
|
126
|
+
expectedCurrentPlanPriceId: string;
|
|
127
|
+
expectedTargetPlanPriceId: string;
|
|
128
|
+
expectedProrationDate: number;
|
|
129
|
+
previewIdempotencyKey?: string;
|
|
130
|
+
idempotencyKey?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ApplySubscriptionPlanChangeResponse {
|
|
134
|
+
status: "applied" | "requires_payment_action" | "payment_failed" | "pending_update" | "no_change";
|
|
135
|
+
currentPlan: SubscriptionPlanChangePlan;
|
|
136
|
+
targetPlan: SubscriptionPlanChangePlan;
|
|
137
|
+
subscriptionId: string;
|
|
138
|
+
invoiceId?: string;
|
|
139
|
+
hostedInvoiceUrl?: string;
|
|
140
|
+
paymentIntentClientSecret?: string;
|
|
141
|
+
amountDueNow?: number;
|
|
142
|
+
currency?: string;
|
|
143
|
+
currentPeriodEnd?: string;
|
|
144
|
+
message: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
57
147
|
export interface IStripeSettings {
|
|
148
|
+
planPriceId?: string | null;
|
|
58
149
|
stripeCFDISubID: string | null;
|
|
59
150
|
stripeCFDISubItemID: string | null;
|
|
60
151
|
cfdiBillingMode?: CFDIBillingMode;
|
package/src/interfaces/Order.ts
CHANGED
|
@@ -46,6 +46,61 @@ export interface IOrderPaymentLines {
|
|
|
46
46
|
facturapiPaymentInvoiceID?: string | null,
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export type TaxBreakdownKey = "taxOne" | "taxTwo" | "taxThree";
|
|
50
|
+
|
|
51
|
+
export interface ITaxBreakdownItem {
|
|
52
|
+
key: TaxBreakdownKey;
|
|
53
|
+
name: string;
|
|
54
|
+
rate: number;
|
|
55
|
+
amount: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface IOrderPricingSnapshotLine {
|
|
59
|
+
lineId: string;
|
|
60
|
+
quantity: number;
|
|
61
|
+
netBeforeDiscount: number;
|
|
62
|
+
netDiscount: number;
|
|
63
|
+
netAfterDiscount: number;
|
|
64
|
+
tax: number;
|
|
65
|
+
taxBreakdown?: ITaxBreakdownItem[];
|
|
66
|
+
grossBeforeDiscount: number;
|
|
67
|
+
gross: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface IOrderPricingSnapshotTotals {
|
|
71
|
+
totalQuantity: number;
|
|
72
|
+
productTotal: number;
|
|
73
|
+
productTotalWithoutDiscount: number;
|
|
74
|
+
taxesTotal: number;
|
|
75
|
+
taxBreakdown?: ITaxBreakdownItem[];
|
|
76
|
+
shippingServiceTotal: number;
|
|
77
|
+
orderLevelDiscountGross?: number;
|
|
78
|
+
discountTotalGrossLines?: number;
|
|
79
|
+
totalDiscountGross?: number;
|
|
80
|
+
subtotalBeforeDiscountNet: number;
|
|
81
|
+
subtotalNetAfterDiscount: number;
|
|
82
|
+
discountTotalNet: number;
|
|
83
|
+
creditApplied: number;
|
|
84
|
+
redeemPointsApplied: number;
|
|
85
|
+
total: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface IOrderPricingSnapshot {
|
|
89
|
+
version: string;
|
|
90
|
+
engine: string;
|
|
91
|
+
engineVersion?: string;
|
|
92
|
+
calculatedAt: Date;
|
|
93
|
+
currency: string;
|
|
94
|
+
taxesType: "included" | "over_price";
|
|
95
|
+
express: boolean;
|
|
96
|
+
discountCodeId?: string;
|
|
97
|
+
redeemPointsAsPaymentAmount: number;
|
|
98
|
+
totalsCents: IOrderPricingSnapshotTotals;
|
|
99
|
+
lines: IOrderPricingSnapshotLine[];
|
|
100
|
+
appliedOrderDiscounts: Record<string, number>;
|
|
101
|
+
meta?: Record<string, any>;
|
|
102
|
+
}
|
|
103
|
+
|
|
49
104
|
export interface IOrderPhaseCompletion {
|
|
50
105
|
source: RouteOrderCompletionSource,
|
|
51
106
|
confirmedAt?: Date | null,
|
|
@@ -73,6 +128,7 @@ export interface IOrder {
|
|
|
73
128
|
taxThree?: ITaxConfig,
|
|
74
129
|
creditApplied: number,
|
|
75
130
|
taxesTotal: number,
|
|
131
|
+
taxBreakdown?: ITaxBreakdownItem[],
|
|
76
132
|
total: number,
|
|
77
133
|
totalQuantity: number,
|
|
78
134
|
productTotal: number,
|
|
@@ -110,6 +166,7 @@ export interface IOrder {
|
|
|
110
166
|
appliedDiscountCodes: Array<string> | null,
|
|
111
167
|
paymentLines: Array<IOrderPaymentLines> | Array<string> | any,
|
|
112
168
|
facturapiInvoiceID?: string | null,
|
|
169
|
+
pricingSnapshot?: IOrderPricingSnapshot | null,
|
|
113
170
|
}
|
|
114
171
|
|
|
115
172
|
|
|
@@ -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,92 @@
|
|
|
1
|
+
import { getAvailableSubscriptionPlans } from "../src/api/companies/get";
|
|
2
|
+
import {
|
|
3
|
+
changeSubscriptionPlan,
|
|
4
|
+
previewSubscriptionPlanChange,
|
|
5
|
+
} from "../src/api/companies/post";
|
|
6
|
+
import type {
|
|
7
|
+
ApplySubscriptionPlanChangeRequest,
|
|
8
|
+
PlanChangePreviewResponse,
|
|
9
|
+
} from "../src/interfaces/Company";
|
|
10
|
+
|
|
11
|
+
describe("subscription plan change company endpoints", () => {
|
|
12
|
+
it("fetches available plans through the billing endpoint", async () => {
|
|
13
|
+
const get = jest.fn().mockResolvedValue({ data: { data: { plans: [] } } });
|
|
14
|
+
const client = {
|
|
15
|
+
apiToken: "token-1",
|
|
16
|
+
axiosInstance: { get },
|
|
17
|
+
} as any;
|
|
18
|
+
|
|
19
|
+
await getAvailableSubscriptionPlans.call(client, { companyId: "company-1" });
|
|
20
|
+
|
|
21
|
+
expect(get).toHaveBeenCalledWith(
|
|
22
|
+
"api/billing/subscription/available-plans?companyId=company-1",
|
|
23
|
+
{ headers: { Authorization: "Bearer token-1" } },
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("fetches available plans without sending an undefined companyId", async () => {
|
|
28
|
+
const get = jest.fn().mockResolvedValue({ data: { data: { plans: [] } } });
|
|
29
|
+
const client = {
|
|
30
|
+
apiToken: "token-1",
|
|
31
|
+
axiosInstance: { get },
|
|
32
|
+
} as any;
|
|
33
|
+
|
|
34
|
+
await getAvailableSubscriptionPlans.call(client, {});
|
|
35
|
+
|
|
36
|
+
expect(get).toHaveBeenCalledWith(
|
|
37
|
+
"api/billing/subscription/available-plans",
|
|
38
|
+
{ headers: { Authorization: "Bearer token-1" } },
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("previews a subscription plan change through the billing endpoint", async () => {
|
|
43
|
+
const post = jest.fn().mockResolvedValue({
|
|
44
|
+
data: {
|
|
45
|
+
data: {
|
|
46
|
+
status: "preview_ready",
|
|
47
|
+
prorationDate: 1770000000,
|
|
48
|
+
} satisfies Partial<PlanChangePreviewResponse>,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const client = {
|
|
52
|
+
apiToken: "token-1",
|
|
53
|
+
axiosInstance: { post },
|
|
54
|
+
} as any;
|
|
55
|
+
|
|
56
|
+
await previewSubscriptionPlanChange.call(client, {
|
|
57
|
+
companyId: "company-1",
|
|
58
|
+
targetPlan: "premium",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(post).toHaveBeenCalledWith(
|
|
62
|
+
"api/billing/subscription/change-plan/preview",
|
|
63
|
+
{ companyId: "company-1", targetPlan: "premium" },
|
|
64
|
+
{ headers: { Authorization: "Bearer token-1" } },
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("applies a subscription plan change with expected preview fields", async () => {
|
|
69
|
+
const post = jest.fn().mockResolvedValue({ data: { data: { status: "applied" } } });
|
|
70
|
+
const client = {
|
|
71
|
+
apiToken: "token-1",
|
|
72
|
+
axiosInstance: { post },
|
|
73
|
+
} as any;
|
|
74
|
+
const payload: ApplySubscriptionPlanChangeRequest = {
|
|
75
|
+
companyId: "company-1",
|
|
76
|
+
targetPlan: "premium",
|
|
77
|
+
expectedCurrentPlanPriceId: "price_standard",
|
|
78
|
+
expectedTargetPlanPriceId: "price_premium",
|
|
79
|
+
expectedProrationDate: 1770000000,
|
|
80
|
+
previewIdempotencyKey: "preview-1",
|
|
81
|
+
idempotencyKey: "apply-1",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
await changeSubscriptionPlan.call(client, payload);
|
|
85
|
+
|
|
86
|
+
expect(post).toHaveBeenCalledWith(
|
|
87
|
+
"api/billing/subscription/change-plan",
|
|
88
|
+
payload,
|
|
89
|
+
{ headers: { Authorization: "Bearer token-1" } },
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -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
|
+
});
|