@acontplus/core 1.0.4 → 1.0.6
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/index.cjs.js +742 -0
- package/index.d.ts +1 -0
- package/index.esm.js +724 -0
- package/package.json +15 -13
- package/README.md +0 -8
package/index.cjs.js
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var utils = require('@acontplus/utils');
|
|
4
|
+
var uuid = require('uuid');
|
|
5
|
+
|
|
6
|
+
class AxiosAdapter {
|
|
7
|
+
constructor() {
|
|
8
|
+
//this.axios = axios.create({ baseURL: HttpClientFactory.getBaseURL() });
|
|
9
|
+
}
|
|
10
|
+
async get(url, options) {
|
|
11
|
+
// const res = await this.axios.get<T>(url, { headers: options?.headers, params: options?.params });
|
|
12
|
+
// return res.data;
|
|
13
|
+
throw new Error('override');
|
|
14
|
+
}
|
|
15
|
+
delete(url, options) {
|
|
16
|
+
throw new Error('override');
|
|
17
|
+
}
|
|
18
|
+
post(url, body, options) {
|
|
19
|
+
throw new Error('override');
|
|
20
|
+
}
|
|
21
|
+
put(url, body, options) {
|
|
22
|
+
throw new Error('override');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Clase que actúa como fábrica y contenedor para un cliente HTTP configurable.
|
|
28
|
+
* Es agnóstica al framework, por lo que puede usarse en React, Vue, Angular, etc.
|
|
29
|
+
*/
|
|
30
|
+
class HttpClientFactory {
|
|
31
|
+
/**
|
|
32
|
+
* Configura el cliente HTTP y opcionalmente la URL base.
|
|
33
|
+
* Este método debe llamarse al iniciar la aplicación.
|
|
34
|
+
*
|
|
35
|
+
* @param client - Instancia que implementa HttpPort (por ejemplo, AxiosAdapter)
|
|
36
|
+
* @param baseURL - URL base para las peticiones (opcional)
|
|
37
|
+
*/
|
|
38
|
+
static configure(client, baseURL) {
|
|
39
|
+
this.client = client;
|
|
40
|
+
if (baseURL) this.baseURL = baseURL;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Devuelve el cliente HTTP configurado.
|
|
44
|
+
* Si no se ha configurado explícitamente, se usa el cliente por defecto.
|
|
45
|
+
*
|
|
46
|
+
* @returns Instancia de HttpPort
|
|
47
|
+
*/
|
|
48
|
+
static getClient() {
|
|
49
|
+
if (!this.client) {
|
|
50
|
+
throw new Error('HttpClientFactory: No se configuró ningún cliente HTTP');
|
|
51
|
+
}
|
|
52
|
+
return this.client;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Devuelve la URL base configurada.
|
|
56
|
+
* Si no se ha definido, retorna una cadena vacía.
|
|
57
|
+
*
|
|
58
|
+
* @returns string con la baseURL
|
|
59
|
+
*/
|
|
60
|
+
static getBaseURL() {
|
|
61
|
+
return this.baseURL;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class FetchAdapter {
|
|
66
|
+
async get(url, options) {
|
|
67
|
+
return this.request('GET', url, undefined, options);
|
|
68
|
+
}
|
|
69
|
+
async post(url, body, options) {
|
|
70
|
+
return this.request('POST', url, body, options);
|
|
71
|
+
}
|
|
72
|
+
async put(url, body, options) {
|
|
73
|
+
return this.request('PUT', url, body, options);
|
|
74
|
+
}
|
|
75
|
+
async delete(url, options) {
|
|
76
|
+
return this.request('DELETE', url, undefined, options);
|
|
77
|
+
}
|
|
78
|
+
async request(method, url, body, options) {
|
|
79
|
+
const baseURL = HttpClientFactory.getBaseURL();
|
|
80
|
+
const response = await fetch(`${baseURL}${url}`, {
|
|
81
|
+
method,
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
...(options?.headers || {})
|
|
85
|
+
},
|
|
86
|
+
body: body ? JSON.stringify(body) : undefined
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(`HTTP error ${response.status}`);
|
|
90
|
+
}
|
|
91
|
+
return response.json();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
exports.SRI_DOCUMENT_TYPE = void 0;
|
|
96
|
+
(function (SRI_DOCUMENT_TYPE) {
|
|
97
|
+
SRI_DOCUMENT_TYPE["RUC"] = "04";
|
|
98
|
+
SRI_DOCUMENT_TYPE["CEDULA"] = "05";
|
|
99
|
+
SRI_DOCUMENT_TYPE["PASSPORT"] = "06";
|
|
100
|
+
SRI_DOCUMENT_TYPE["CONSUMIDOR_FINAL"] = "07";
|
|
101
|
+
SRI_DOCUMENT_TYPE["IDENTIFICACION_EXTERIOR"] = "08";
|
|
102
|
+
})(exports.SRI_DOCUMENT_TYPE || (exports.SRI_DOCUMENT_TYPE = {}));
|
|
103
|
+
exports.SRI_DOCUMENT_TYPE_CUSTOM = void 0;
|
|
104
|
+
(function (SRI_DOCUMENT_TYPE_CUSTOM) {
|
|
105
|
+
SRI_DOCUMENT_TYPE_CUSTOM["RUC"] = "R";
|
|
106
|
+
SRI_DOCUMENT_TYPE_CUSTOM["CEDULA"] = "C";
|
|
107
|
+
SRI_DOCUMENT_TYPE_CUSTOM["PASSPORT"] = "P";
|
|
108
|
+
SRI_DOCUMENT_TYPE_CUSTOM["CONSUMIDOR_FINAL"] = "CF";
|
|
109
|
+
SRI_DOCUMENT_TYPE_CUSTOM["IDENTIFICACION_EXTERIOR"] = "IE";
|
|
110
|
+
})(exports.SRI_DOCUMENT_TYPE_CUSTOM || (exports.SRI_DOCUMENT_TYPE_CUSTOM = {}));
|
|
111
|
+
exports.SEPARATOR_KEY_CODE = void 0;
|
|
112
|
+
(function (SEPARATOR_KEY_CODE) {
|
|
113
|
+
SEPARATOR_KEY_CODE["SLASH"] = "|";
|
|
114
|
+
SEPARATOR_KEY_CODE["PUNTO_COMA"] = ";";
|
|
115
|
+
SEPARATOR_KEY_CODE["DOS_PUNTOS"] = ":";
|
|
116
|
+
})(exports.SEPARATOR_KEY_CODE || (exports.SEPARATOR_KEY_CODE = {}));
|
|
117
|
+
const SEPARADORES_REGEX = new RegExp(`[${Object.values(exports.SEPARATOR_KEY_CODE).join('')}]`);
|
|
118
|
+
|
|
119
|
+
const isSuccessResponse = response => response?.status === 'success';
|
|
120
|
+
const isErrorResponse = response => response?.status === 'error';
|
|
121
|
+
|
|
122
|
+
class LegacyApiResponse {
|
|
123
|
+
constructor(code = '0', message = '', payload = null) {
|
|
124
|
+
this.code = code;
|
|
125
|
+
this.message = message;
|
|
126
|
+
this.payload = payload;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class PaginationParams {
|
|
131
|
+
constructor(params) {
|
|
132
|
+
this._pageIndex = 1;
|
|
133
|
+
this._pageSize = 10;
|
|
134
|
+
this.sortDirection = 'asc';
|
|
135
|
+
if (params) {
|
|
136
|
+
this.pageIndex = params.pageIndex ?? 1;
|
|
137
|
+
this.pageSize = params.pageSize ?? 10;
|
|
138
|
+
this.sortBy = params.sortBy;
|
|
139
|
+
this.sortDirection = params.sortDirection ?? 'asc';
|
|
140
|
+
this.searchTerm = params.searchTerm;
|
|
141
|
+
this.filters = params.filters;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
get pageIndex() {
|
|
145
|
+
return this._pageIndex;
|
|
146
|
+
}
|
|
147
|
+
set pageIndex(value) {
|
|
148
|
+
this._pageIndex = value < 1 ? 1 : value;
|
|
149
|
+
}
|
|
150
|
+
get pageSize() {
|
|
151
|
+
return this._pageSize;
|
|
152
|
+
}
|
|
153
|
+
set pageSize(value) {
|
|
154
|
+
this._pageSize = value < 1 ? 10 : value > 1000 ? 1000 : value;
|
|
155
|
+
}
|
|
156
|
+
get skip() {
|
|
157
|
+
return (this.pageIndex - 1) * this.pageSize;
|
|
158
|
+
}
|
|
159
|
+
get take() {
|
|
160
|
+
return this.pageSize;
|
|
161
|
+
}
|
|
162
|
+
get isEmpty() {
|
|
163
|
+
return (!this.searchTerm || this.searchTerm.trim() === '') && (!this.filters || Object.keys(this.filters).length === 0);
|
|
164
|
+
}
|
|
165
|
+
buildFilters() {
|
|
166
|
+
if (!this.filters || Object.keys(this.filters).length === 0) return null;
|
|
167
|
+
return {
|
|
168
|
+
...this.filters
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
buildFiltersWithPrefix(prefix) {
|
|
172
|
+
if (!this.filters || Object.keys(this.filters).length === 0) return null;
|
|
173
|
+
const result = {};
|
|
174
|
+
for (const key in this.filters) {
|
|
175
|
+
result[prefix + key] = this.filters[key];
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
buildSqlParameters() {
|
|
180
|
+
return this.buildFiltersWithPrefix('@');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
class PricingCalculationError extends Error {
|
|
185
|
+
constructor(operation, originalError) {
|
|
186
|
+
super(`Pricing calculation failed for operation: ${operation}. ${originalError?.message || ''}`);
|
|
187
|
+
this.name = 'PricingCalculationError';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Calculadora especializada para descuentos
|
|
193
|
+
*/
|
|
194
|
+
class DiscountCalculator {
|
|
195
|
+
constructor(decimales = 4) {
|
|
196
|
+
this.decimales = decimales;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Calcula el valor del descuento a partir del porcentaje
|
|
200
|
+
* @param originalPrice - Precio antes del descuento
|
|
201
|
+
* @param discountPercentage - Porcentaje de descuento a aplicar (0-100)
|
|
202
|
+
* @returns Valor del descuento en moneda
|
|
203
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const valorDescuento = calculadora.calculateDiscountAmount(100, 10); // 10
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
calculateDiscountAmount(originalPrice, discountPercentage) {
|
|
210
|
+
try {
|
|
211
|
+
utils.ParameterValidator.validatePositiveNumber(originalPrice, 'originalPrice');
|
|
212
|
+
utils.ParameterValidator.validatePercentage(discountPercentage, 'discountPercentage');
|
|
213
|
+
const percentage = utils.DecimalConverter.divideAsNumber(discountPercentage, 100);
|
|
214
|
+
return utils.DecimalConverter.multiplyAsNumber(originalPrice, percentage);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new PricingCalculationError('calculateDiscountAmount', error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Aplica un descuento al precio original
|
|
221
|
+
* @param originalPrice - Precio antes del descuento
|
|
222
|
+
* @param discountPercentage - Porcentaje de descuento a aplicar
|
|
223
|
+
* @returns Precio final después del descuento
|
|
224
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* const precioConDescuento = calculadora.applyDiscount(100, 10); // 90
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
applyDiscount(originalPrice, discountPercentage) {
|
|
231
|
+
try {
|
|
232
|
+
const discountAmount = this.calculateDiscountAmount(originalPrice, discountPercentage);
|
|
233
|
+
return utils.DecimalConverter.subtractAsNumber(originalPrice, discountAmount);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new PricingCalculationError('applyDiscount', error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Calcula el porcentaje de descuento a partir de los valores
|
|
240
|
+
* @param originalPrice - Precio antes del descuento
|
|
241
|
+
* @param discountAmount - Valor del descuento aplicado
|
|
242
|
+
* @returns Porcentaje de descuento
|
|
243
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* const porcentaje = calculadora.calculateDiscountPercentage(100, 10); // 10
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
calculateDiscountPercentage(originalPrice, discountAmount) {
|
|
250
|
+
try {
|
|
251
|
+
utils.ParameterValidator.validatePositiveNumber(originalPrice, 'originalPrice');
|
|
252
|
+
utils.ParameterValidator.validatePositiveNumber(discountAmount, 'discountAmount');
|
|
253
|
+
if (discountAmount > originalPrice) {
|
|
254
|
+
throw new utils.InvalidParameterError('discountAmount', 'No puede ser mayor que el precio original');
|
|
255
|
+
}
|
|
256
|
+
const percentage = utils.DecimalConverter.divideAsNumber(discountAmount, originalPrice);
|
|
257
|
+
return utils.DecimalConverter.multiplyAsNumber(percentage, 100);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw new PricingCalculationError('calculateDiscountPercentage', error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Obtiene los detalles completos del cálculo de descuento
|
|
264
|
+
* @param originalPrice - Precio antes del descuento
|
|
265
|
+
* @param discountPercentage - Porcentaje de descuento a aplicar
|
|
266
|
+
* @returns Objeto con todos los detalles del cálculo
|
|
267
|
+
* @example
|
|
268
|
+
* ```typescript
|
|
269
|
+
* const detalles = calculadora.getDiscountCalculationDetails(100, 10);
|
|
270
|
+
* // { originalPrice: 100, discountRate: 10, discountAmount: 10, finalPrice: 90 }
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
getDiscountCalculationDetails(originalPrice, discountPercentage) {
|
|
274
|
+
const discountAmount = this.calculateDiscountAmount(originalPrice, discountPercentage);
|
|
275
|
+
const finalPrice = this.applyDiscount(originalPrice, discountPercentage);
|
|
276
|
+
return {
|
|
277
|
+
originalPrice,
|
|
278
|
+
discountRate: discountPercentage,
|
|
279
|
+
discountAmount,
|
|
280
|
+
finalPrice
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Calculadora especializada para líneas de productos/servicios en facturas
|
|
287
|
+
*/
|
|
288
|
+
class LineItemCalculator {
|
|
289
|
+
constructor(decimales = 4) {
|
|
290
|
+
this.decimales = decimales;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Calcula el total de una línea de producto incluyendo descuentos e impuestos
|
|
294
|
+
* @param unitPrice - Precio por unidad del producto
|
|
295
|
+
* @param quantity - Cantidad de productos
|
|
296
|
+
* @param discountAmount - Valor del descuento a aplicar (por defecto 0)
|
|
297
|
+
* @param taxAmount - Valor del impuesto a aplicar (por defecto 0)
|
|
298
|
+
* @returns Objeto con todos los cálculos de la línea
|
|
299
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* const linea = calculadora.calcularTotalLinea(50, 2, 10, 8.4);
|
|
303
|
+
* // Resultado: subtotal: 100, con descuento: 90, total con impuesto: 98.4
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
calculateLineItemTotal(unitPrice, quantity, discountAmount = 0, taxAmount = 0) {
|
|
307
|
+
try {
|
|
308
|
+
utils.ParameterValidator.validatePositiveNumber(unitPrice, 'unitPrice');
|
|
309
|
+
utils.ParameterValidator.validatePositiveNumber(quantity, 'quantity');
|
|
310
|
+
utils.ParameterValidator.validatePositiveNumber(discountAmount, 'discountAmount');
|
|
311
|
+
utils.ParameterValidator.validatePositiveNumber(taxAmount, 'taxAmount');
|
|
312
|
+
const subtotal = utils.DecimalConverter.multiplyAsNumber(unitPrice, quantity);
|
|
313
|
+
const subtotalAfterDiscount = utils.DecimalConverter.subtractAsNumber(subtotal, discountAmount);
|
|
314
|
+
const total = utils.DecimalConverter.addAsNumber(subtotalAfterDiscount, taxAmount);
|
|
315
|
+
return {
|
|
316
|
+
unitPrice,
|
|
317
|
+
quantity,
|
|
318
|
+
subtotal,
|
|
319
|
+
discountAmount,
|
|
320
|
+
subtotalAfterDiscount,
|
|
321
|
+
taxAmount,
|
|
322
|
+
total
|
|
323
|
+
};
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw new PricingCalculationError('calcularTotalLinea', error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Calcula el subtotal antes de aplicar descuentos
|
|
330
|
+
* @param unitPrice - Precio por unidad
|
|
331
|
+
* @param quantity - Cantidad de productos
|
|
332
|
+
* @returns Subtotal calculado
|
|
333
|
+
* @example
|
|
334
|
+
* ```typescript
|
|
335
|
+
* const subtotal = calculadora.calculateSubtotal(25, 4); // 100
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
calculateSubtotal(unitPrice, quantity) {
|
|
339
|
+
return utils.DecimalConverter.multiplyAsNumber(unitPrice, quantity);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Calculadora especializada para márgenes de ganancia y utilidades
|
|
345
|
+
*/
|
|
346
|
+
class ProfitCalculator {
|
|
347
|
+
constructor(decimals = 4) {
|
|
348
|
+
this.decimals = decimals;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Calcula el precio de venta a partir del costo y el margen de ganancia
|
|
352
|
+
* @param cost - Costo del producto o servicio
|
|
353
|
+
* @param profitMarginPercentage - Porcentaje de margen de ganancia deseado
|
|
354
|
+
* @returns Precio de venta calculado
|
|
355
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
356
|
+
* @example
|
|
357
|
+
* ```typescript
|
|
358
|
+
* const precioVenta = calculadora.calculateSalePriceFromMargin(80, 25); // 100
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
calculateSalePriceFromMargin(cost, profitMarginPercentage) {
|
|
362
|
+
try {
|
|
363
|
+
utils.ParameterValidator.validatePositiveNumber(cost, 'cost');
|
|
364
|
+
utils.ParameterValidator.validatePositiveNumber(profitMarginPercentage, 'profitMarginPercentage');
|
|
365
|
+
const marginMultiplier = utils.DecimalConverter.divideAsNumber(profitMarginPercentage, 100);
|
|
366
|
+
const profitAmount = utils.DecimalConverter.multiplyAsNumber(cost, marginMultiplier);
|
|
367
|
+
return utils.DecimalConverter.addAsNumber(cost, profitAmount);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
throw new PricingCalculationError('calculateSalePriceFromMargin', error);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Calcula el porcentaje de margen de ganancia a partir del precio de venta y costo
|
|
374
|
+
* @param salePrice - Precio de venta del producto
|
|
375
|
+
* @param cost - Costo del producto
|
|
376
|
+
* @returns Porcentaje de margen de ganancia
|
|
377
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
378
|
+
* @example
|
|
379
|
+
* ```typescript
|
|
380
|
+
* const margen = calculadora.calculateProfitMarginPercentage(100, 80); // 25
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
calculateProfitMarginPercentage(salePrice, cost) {
|
|
384
|
+
try {
|
|
385
|
+
utils.ParameterValidator.validatePositiveNumber(salePrice, 'salePrice');
|
|
386
|
+
utils.ParameterValidator.validatePositiveNumber(cost, 'cost');
|
|
387
|
+
if (salePrice < cost) {
|
|
388
|
+
throw new utils.InvalidParameterError('salePrice', 'El precio de venta no puede ser menor que el costo');
|
|
389
|
+
}
|
|
390
|
+
const profitAmount = utils.DecimalConverter.subtractAsNumber(salePrice, cost);
|
|
391
|
+
const profitRatio = utils.DecimalConverter.divideAsNumber(profitAmount, cost);
|
|
392
|
+
return utils.DecimalConverter.multiplyAsNumber(profitRatio, 100);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw new PricingCalculationError('calculateProfitMarginPercentage', error);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Calcula el valor de la ganancia
|
|
399
|
+
* @param salePrice - Precio de venta
|
|
400
|
+
* @param cost - Costo del producto
|
|
401
|
+
* @returns Valor de la ganancia obtenida
|
|
402
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
403
|
+
* @example
|
|
404
|
+
* ```typescript
|
|
405
|
+
* const ganancia = calculadora.calculateProfitAmount(100, 80); // 20
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
calculateProfitAmount(salePrice, cost) {
|
|
409
|
+
try {
|
|
410
|
+
utils.ParameterValidator.validatePositiveNumber(salePrice, 'salePrice');
|
|
411
|
+
utils.ParameterValidator.validatePositiveNumber(cost, 'cost');
|
|
412
|
+
return utils.DecimalConverter.subtractAsNumber(salePrice, cost);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
throw new PricingCalculationError('calculateProfitAmount', error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Obtiene los detalles completos del cálculo de ganancia
|
|
419
|
+
* @param salePrice - Precio de venta
|
|
420
|
+
* @param cost - Costo del producto
|
|
421
|
+
* @returns Objeto con todos los detalles del cálculo
|
|
422
|
+
* @example
|
|
423
|
+
* ```typescript
|
|
424
|
+
* const detalles = calculadora.obtenerDetallesCalculoGanancia(100, 80);
|
|
425
|
+
* // { salePrice: 100, cost: 80, profitAmount: 20, profitMargin: 25 }
|
|
426
|
+
* ```
|
|
427
|
+
*/
|
|
428
|
+
getProfitCalculationDetails(salePrice, cost) {
|
|
429
|
+
const profitAmount = this.calculateProfitAmount(salePrice, cost);
|
|
430
|
+
const profitMargin = this.calculateProfitMarginPercentage(salePrice, cost);
|
|
431
|
+
return {
|
|
432
|
+
salePrice,
|
|
433
|
+
cost,
|
|
434
|
+
profitAmount,
|
|
435
|
+
profitMargin
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Calculadora especializada para impuestos (IVA, IGV, etc.)
|
|
442
|
+
*/
|
|
443
|
+
class TaxCalculator {
|
|
444
|
+
constructor(decimales = 4) {
|
|
445
|
+
this.decimales = decimales;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Calcula el valor del impuesto basado en el precio base y la tasa
|
|
449
|
+
* @param basePrice - Precio sin impuestos
|
|
450
|
+
* @param taxRate - Porcentaje del impuesto (ej: 21 para 21%)
|
|
451
|
+
* @returns Valor del impuesto calculado
|
|
452
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* const calculadora = new TaxCalculator();
|
|
456
|
+
* const valorIva = calculadora.calculateTaxAmount(100, 21); // 21
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
calculateTaxAmount(basePrice, taxRate) {
|
|
460
|
+
try {
|
|
461
|
+
utils.ParameterValidator.validatePositiveNumber(basePrice, 'basePrice');
|
|
462
|
+
utils.ParameterValidator.validatePositiveNumber(taxRate, 'taxRate');
|
|
463
|
+
const multiplicador = utils.DecimalConverter.multiplyAsNumber(basePrice, taxRate);
|
|
464
|
+
return utils.DecimalConverter.divideAsNumber(multiplicador, 100);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
throw new PricingCalculationError('calculateTaxAmount', error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Calcula el precio incluyendo impuestos
|
|
471
|
+
* @param basePrice - Precio sin impuestos
|
|
472
|
+
* @param taxRate - Porcentaje del impuesto
|
|
473
|
+
* @returns Precio total con impuestos incluidos
|
|
474
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
475
|
+
* @example
|
|
476
|
+
* ```typescript
|
|
477
|
+
* const precioConIva = calculadora.calculatePriceWithTax(100, 21); // 121
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
calculatePriceWithTax(basePrice, taxRate) {
|
|
481
|
+
try {
|
|
482
|
+
const taxAmount = this.calculateTaxAmount(basePrice, taxRate);
|
|
483
|
+
return utils.DecimalConverter.addAsNumber(basePrice, taxAmount);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
throw new PricingCalculationError('calculatePriceWithTax', error);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Calcula el precio base a partir de un precio que ya incluye impuestos
|
|
490
|
+
* @param priceWithTax - Precio que incluye impuestos
|
|
491
|
+
* @param taxRate - Porcentaje del impuesto aplicado
|
|
492
|
+
* @returns Precio base sin impuestos
|
|
493
|
+
* @throws {PricingCalculationError} Si ocurre un error en el cálculo
|
|
494
|
+
* @example
|
|
495
|
+
* ```typescript
|
|
496
|
+
* const precioBase = calculadora.calculateBasePriceFromTaxIncluded(121, 21); // 100
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
calculateBasePriceFromTaxIncluded(priceWithTax, taxRate) {
|
|
500
|
+
try {
|
|
501
|
+
utils.ParameterValidator.validatePositiveNumber(priceWithTax, 'priceWithTax');
|
|
502
|
+
utils.ParameterValidator.validatePositiveNumber(taxRate, 'taxRate');
|
|
503
|
+
const taxMultiplier = utils.DecimalConverter.divideAsNumber(taxRate, 100);
|
|
504
|
+
const divisor = utils.DecimalConverter.addAsNumber(1, taxMultiplier);
|
|
505
|
+
return utils.DecimalConverter.divideAsNumber(priceWithTax, divisor);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
throw new PricingCalculationError('calculateBasePriceFromTaxIncluded', error);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Obtiene los detalles completos del cálculo de impuestos
|
|
512
|
+
* @param basePrice - Precio sin impuestos
|
|
513
|
+
* @param taxRate - Porcentaje del impuesto
|
|
514
|
+
* @returns Objeto con todos los detalles del cálculo
|
|
515
|
+
* @example
|
|
516
|
+
* ```typescript
|
|
517
|
+
* const detalles = calculadora.getTaxCalculationDetails(100, 21);
|
|
518
|
+
* // { montoBase: 100, taxRate: 21, valorImpuesto: 21, montoTotal: 121 }
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
getTaxCalculationDetails(basePrice, taxRate) {
|
|
522
|
+
const taxAmount = this.calculateTaxAmount(basePrice, taxRate);
|
|
523
|
+
const totalAmount = this.calculatePriceWithTax(basePrice, taxRate);
|
|
524
|
+
return {
|
|
525
|
+
baseAmount: basePrice,
|
|
526
|
+
taxRate,
|
|
527
|
+
taxAmount,
|
|
528
|
+
totalAmount
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
class PricingCalculator {
|
|
534
|
+
constructor(config = {}) {
|
|
535
|
+
this.config = {
|
|
536
|
+
defaultDecimals: config.defaultDecimals ?? 4,
|
|
537
|
+
roundingMode: config.roundingMode ?? 'round',
|
|
538
|
+
errorHandling: config.errorHandling ?? 'throw'
|
|
539
|
+
};
|
|
540
|
+
this.tax = new TaxCalculator(this.config.defaultDecimals);
|
|
541
|
+
this.discount = new DiscountCalculator(this.config.defaultDecimals);
|
|
542
|
+
this.profit = new ProfitCalculator(this.config.defaultDecimals);
|
|
543
|
+
this.lineItem = new LineItemCalculator(this.config.defaultDecimals);
|
|
544
|
+
}
|
|
545
|
+
updateConfig(newConfig) {
|
|
546
|
+
this.config = {
|
|
547
|
+
...this.config,
|
|
548
|
+
...newConfig
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
getConfig() {
|
|
552
|
+
return {
|
|
553
|
+
...this.config
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/domain/value-objects/base.vo.ts
|
|
559
|
+
class BaseVo {
|
|
560
|
+
constructor(value) {
|
|
561
|
+
this.value = value;
|
|
562
|
+
this.validate(value);
|
|
563
|
+
}
|
|
564
|
+
getValue() {
|
|
565
|
+
return this.value;
|
|
566
|
+
}
|
|
567
|
+
equals(other) {
|
|
568
|
+
return JSON.stringify(this.value) === JSON.stringify(other.value);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
class EntityIdVo extends BaseVo {
|
|
573
|
+
constructor(value) {
|
|
574
|
+
super(value);
|
|
575
|
+
}
|
|
576
|
+
static generate() {
|
|
577
|
+
return new EntityIdVo(uuid.v4());
|
|
578
|
+
}
|
|
579
|
+
validate(value) {
|
|
580
|
+
if (!value || value.trim().length === 0) {
|
|
581
|
+
throw new Error('EntityIdVo cannot be empty');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/domain/value-objects/money.vo.ts
|
|
587
|
+
class MoneyVo extends BaseVo {
|
|
588
|
+
constructor(amount, currency = 'USD') {
|
|
589
|
+
super({
|
|
590
|
+
amount,
|
|
591
|
+
currency
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
validate(value) {
|
|
595
|
+
if (value.amount < 0) throw new Error('Amount cannot be negative');
|
|
596
|
+
if (!value.currency || value.currency.length !== 3) {
|
|
597
|
+
throw new Error('Invalid currency code');
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
add(other) {
|
|
601
|
+
if (this.value.currency !== other.value.currency) {
|
|
602
|
+
throw new Error('Cannot add different currencies');
|
|
603
|
+
}
|
|
604
|
+
return new MoneyVo(this.value.amount + other.value.amount, this.value.currency);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
class IdentificationNumberVo extends BaseVo {
|
|
609
|
+
constructor(id) {
|
|
610
|
+
super(id);
|
|
611
|
+
this.id = id?.trim() || '';
|
|
612
|
+
this.type = this.determineType();
|
|
613
|
+
if (!this.validate()) {
|
|
614
|
+
throw new Error(`Número de identificación inválido: ${this.id}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
determineType() {
|
|
618
|
+
if (this.id.length === 10) return exports.SRI_DOCUMENT_TYPE.CEDULA;
|
|
619
|
+
if (this.id.length === 13) return exports.SRI_DOCUMENT_TYPE.RUC;
|
|
620
|
+
throw new Error('Número de identificación debe tener 10 o 13 dígitos');
|
|
621
|
+
}
|
|
622
|
+
validate() {
|
|
623
|
+
if (this.type === exports.SRI_DOCUMENT_TYPE.CEDULA) {
|
|
624
|
+
return this.isValidCedulaAlgorithm();
|
|
625
|
+
}
|
|
626
|
+
if (this.type === exports.SRI_DOCUMENT_TYPE.RUC) {
|
|
627
|
+
return this.isValidRucAlgorithm();
|
|
628
|
+
}
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
// Validación completa de cédula ecuatoriana con algoritmo
|
|
632
|
+
isValidCedulaAlgorithm() {
|
|
633
|
+
if (!/^\d{10}$/.test(this.id)) return false;
|
|
634
|
+
// Verificar que los primeros 2 dígitos sean una provincia válida (01-24)
|
|
635
|
+
const provincia = parseInt(this.id.substring(0, 2));
|
|
636
|
+
if (provincia < 1 || provincia > 24) return false;
|
|
637
|
+
// Algoritmo de validación del dígito verificador
|
|
638
|
+
const coeficientes = [2, 1, 2, 1, 2, 1, 2, 1, 2];
|
|
639
|
+
let suma = 0;
|
|
640
|
+
for (let i = 0; i < 9; i++) {
|
|
641
|
+
const valor = parseInt(this.id.charAt(i)) * coeficientes[i];
|
|
642
|
+
suma += valor > 9 ? valor - 9 : valor;
|
|
643
|
+
}
|
|
644
|
+
const digitoVerificador = suma % 10 === 0 ? 0 : 10 - suma % 10;
|
|
645
|
+
return digitoVerificador === parseInt(this.id.charAt(9));
|
|
646
|
+
}
|
|
647
|
+
// Validación de RUC más flexible
|
|
648
|
+
isValidRucAlgorithm() {
|
|
649
|
+
if (!/^\d{13}$/.test(this.id)) return false;
|
|
650
|
+
// Los primeros 10 dígitos deben ser una cédula válida para personas naturales
|
|
651
|
+
// o seguir reglas específicas para sociedades
|
|
652
|
+
const tercerDigito = parseInt(this.id.charAt(2));
|
|
653
|
+
// RUC de persona natural (tercer dígito < 6)
|
|
654
|
+
if (tercerDigito < 6) {
|
|
655
|
+
const cedula = this.id.substring(0, 10);
|
|
656
|
+
const tempCedula = IdentificationNumberVo.validateCedula(cedula);
|
|
657
|
+
return tempCedula && this.id.endsWith('001');
|
|
658
|
+
}
|
|
659
|
+
// RUC de sociedad privada (tercer dígito = 9)
|
|
660
|
+
if (tercerDigito === 9) {
|
|
661
|
+
return this.isValidSociedadRuc();
|
|
662
|
+
}
|
|
663
|
+
// RUC de entidad pública (tercer dígito = 6)
|
|
664
|
+
if (tercerDigito === 6) {
|
|
665
|
+
return this.id.endsWith('0001');
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
isValidSociedadRuc() {
|
|
670
|
+
const coeficientes = [4, 3, 2, 7, 6, 5, 4, 3, 2];
|
|
671
|
+
let suma = 0;
|
|
672
|
+
for (let i = 0; i < 9; i++) {
|
|
673
|
+
suma += parseInt(this.id.charAt(i)) * coeficientes[i];
|
|
674
|
+
}
|
|
675
|
+
const residuo = suma % 11;
|
|
676
|
+
const digitoVerificador = residuo === 0 ? 0 : 11 - residuo;
|
|
677
|
+
return digitoVerificador === parseInt(this.id.charAt(9)) && this.id.endsWith('001');
|
|
678
|
+
}
|
|
679
|
+
// Método auxiliar estático para validar cédula sin crear instancia
|
|
680
|
+
static validateCedula(cedula) {
|
|
681
|
+
if (!/^\d{10}$/.test(cedula)) return false;
|
|
682
|
+
const provincia = parseInt(cedula.substring(0, 2));
|
|
683
|
+
if (provincia < 1 || provincia > 24) return false;
|
|
684
|
+
const coeficientes = [2, 1, 2, 1, 2, 1, 2, 1, 2];
|
|
685
|
+
let suma = 0;
|
|
686
|
+
for (let i = 0; i < 9; i++) {
|
|
687
|
+
const valor = parseInt(cedula.charAt(i)) * coeficientes[i];
|
|
688
|
+
suma += valor > 9 ? valor - 9 : valor;
|
|
689
|
+
}
|
|
690
|
+
const digitoVerificador = suma % 10 === 0 ? 0 : 10 - suma % 10;
|
|
691
|
+
return digitoVerificador === parseInt(cedula.charAt(9));
|
|
692
|
+
}
|
|
693
|
+
// Métodos públicos mejorados
|
|
694
|
+
isValidCedula() {
|
|
695
|
+
return this.type === exports.SRI_DOCUMENT_TYPE.CEDULA && this.isValidCedulaAlgorithm();
|
|
696
|
+
}
|
|
697
|
+
isValidRuc() {
|
|
698
|
+
return this.type === exports.SRI_DOCUMENT_TYPE.RUC && this.isValidRucAlgorithm();
|
|
699
|
+
}
|
|
700
|
+
getId() {
|
|
701
|
+
return this.id;
|
|
702
|
+
}
|
|
703
|
+
getType() {
|
|
704
|
+
return this.type;
|
|
705
|
+
}
|
|
706
|
+
// Método estático mejorado
|
|
707
|
+
static tryCreate(id) {
|
|
708
|
+
try {
|
|
709
|
+
new IdentificationNumberVo(id);
|
|
710
|
+
return true;
|
|
711
|
+
} catch {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Método adicional para obtener información del tipo de RUC
|
|
716
|
+
getRucType() {
|
|
717
|
+
if (this.type !== exports.SRI_DOCUMENT_TYPE.RUC) return null;
|
|
718
|
+
const tercerDigito = parseInt(this.id.charAt(2));
|
|
719
|
+
if (tercerDigito < 6) return 'Persona Natural';
|
|
720
|
+
if (tercerDigito === 6) return 'Entidad Pública';
|
|
721
|
+
if (tercerDigito === 9) return 'Sociedad Privada';
|
|
722
|
+
return 'Desconocido';
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
exports.AxiosAdapter = AxiosAdapter;
|
|
727
|
+
exports.BaseVo = BaseVo;
|
|
728
|
+
exports.DiscountCalculator = DiscountCalculator;
|
|
729
|
+
exports.EntityIdVo = EntityIdVo;
|
|
730
|
+
exports.FetchAdapter = FetchAdapter;
|
|
731
|
+
exports.HttpClientFactory = HttpClientFactory;
|
|
732
|
+
exports.IdentificationNumberVo = IdentificationNumberVo;
|
|
733
|
+
exports.LegacyApiResponse = LegacyApiResponse;
|
|
734
|
+
exports.LineItemCalculator = LineItemCalculator;
|
|
735
|
+
exports.MoneyVo = MoneyVo;
|
|
736
|
+
exports.PaginationParams = PaginationParams;
|
|
737
|
+
exports.PricingCalculator = PricingCalculator;
|
|
738
|
+
exports.ProfitCalculator = ProfitCalculator;
|
|
739
|
+
exports.SEPARADORES_REGEX = SEPARADORES_REGEX;
|
|
740
|
+
exports.TaxCalculator = TaxCalculator;
|
|
741
|
+
exports.isErrorResponse = isErrorResponse;
|
|
742
|
+
exports.isSuccessResponse = isSuccessResponse;
|