softseti-sale-calculator-library 3.7.5 → 4.0.0
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/package.json +4 -1
- package/src/{refund-calculator.js → core/refund-calculator.js} +1 -1
- package/src/core/rule-engine.js +73 -0
- package/src/{SaleCalculator.js → core/sale-calculator.js} +277 -115
- package/src/index.d.ts +18 -18
- package/src/index.js +2 -2
- package/src/models/BusinessRuleModel.js +53 -0
- package/src/rules/RuleManager.js +75 -0
- /package/src/{interfaces.ts → models/interfaces.ts} +0 -0
- /package/src/{helpers.js → utils/helpers.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softseti-sale-calculator-library",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Sales calculation engine by Softseti",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -28,5 +28,8 @@
|
|
|
28
28
|
"jest-environment-jsdom": "^29.7.0",
|
|
29
29
|
"jest-localstorage-mock": "^2.4.26",
|
|
30
30
|
"ts-jest": "^29.4.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@gorules/zen-engine": "^0.51.5"
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/rules/RuleEngine.js
|
|
2
|
+
const { ZenEngine } = require('@gorules/zen-engine');
|
|
3
|
+
|
|
4
|
+
class RuleEngine {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.engine = new ZenEngine();
|
|
7
|
+
this.ruleCache = new Map();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Evaluates a business rule with given context
|
|
12
|
+
* @param {Object} ruleContent - GoRules JSON rule content
|
|
13
|
+
* @param {Object} context - Execution context
|
|
14
|
+
* @returns {Promise<Object>} Rule evaluation result
|
|
15
|
+
*/
|
|
16
|
+
async evaluateRule(ruleContent, context) {
|
|
17
|
+
try {
|
|
18
|
+
|
|
19
|
+
const decision = this.engine.createDecision(ruleContent);
|
|
20
|
+
const result = await decision.safeEvaluate(context);
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Rule evaluation error:', error);
|
|
26
|
+
throw new Error(`Rule evaluation failed: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Executes multiple rules for a trigger event
|
|
32
|
+
* @param {Array} rules - Business rules to execute
|
|
33
|
+
* @param {Object} context - Execution context
|
|
34
|
+
* @returns {Promise<Array>} Array of rule execution results
|
|
35
|
+
*/
|
|
36
|
+
async executeRules(rules, context) {
|
|
37
|
+
const results = [];
|
|
38
|
+
|
|
39
|
+
for (const rule of rules) {
|
|
40
|
+
try {
|
|
41
|
+
const result = await this.evaluateRule(rule.rule_content, context);
|
|
42
|
+
results.push(
|
|
43
|
+
result
|
|
44
|
+
);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
|
|
47
|
+
results.push({
|
|
48
|
+
ruleId: rule.id,
|
|
49
|
+
success: false,
|
|
50
|
+
error: error.message
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validates rule structure
|
|
60
|
+
* @param {Object} ruleContent - Rule content to validate
|
|
61
|
+
* @returns {boolean} True if rule is valid
|
|
62
|
+
*/
|
|
63
|
+
validateRule(ruleContent) {
|
|
64
|
+
if (!ruleContent || typeof ruleContent !== 'object') {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const requiredProperties = ['nodes', 'edges', 'contentType'];
|
|
69
|
+
return requiredProperties.every(prop => ruleContent.hasOwnProperty(prop));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = RuleEngine;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
const RuleEngine = require('./rule-engine');
|
|
2
|
+
const RuleManager = require('../rules/RuleManager');
|
|
3
|
+
|
|
4
|
+
let ruleEngine = null;
|
|
5
|
+
let ruleManager = null;
|
|
6
|
+
|
|
1
7
|
let products = {};
|
|
2
8
|
let productTaxes = {};
|
|
3
9
|
let taxes = {};
|
|
@@ -6,10 +12,13 @@ let settings = {};
|
|
|
6
12
|
let exchangeRates = {};
|
|
7
13
|
let currencyConverterRates = {};
|
|
8
14
|
let currencies = {};
|
|
15
|
+
let businessRules = {};
|
|
9
16
|
let decimalPlaces = 2;
|
|
10
17
|
let roundingFactor = 100;
|
|
11
18
|
let useRounding = false;
|
|
12
19
|
|
|
20
|
+
let preprocessedSale = {};
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* Sales Calculation Engine (V1)
|
|
15
24
|
* @author softseti
|
|
@@ -20,10 +29,6 @@ let useRounding = false;
|
|
|
20
29
|
* - Deal/discount allocations
|
|
21
30
|
*/
|
|
22
31
|
|
|
23
|
-
// ==============================================
|
|
24
|
-
// INITIALIZATION
|
|
25
|
-
// ==============================================
|
|
26
|
-
|
|
27
32
|
/**
|
|
28
33
|
* Initializes the calculator with indexed data
|
|
29
34
|
* @param {Object} params - Data collections:
|
|
@@ -44,11 +49,41 @@ function init(params) {
|
|
|
44
49
|
exchangeRates = params.exchange_rates || {};
|
|
45
50
|
currencyConverterRates = params.currency_converter_rates || {};
|
|
46
51
|
currencies = params.currencies || {};
|
|
52
|
+
businessRules = params.businessRules || {};
|
|
47
53
|
|
|
48
54
|
decimalPlaces = params.decimal_places !== undefined ? params.decimal_places : 2;
|
|
49
55
|
roundingFactor = Math.pow(10, decimalPlaces);
|
|
50
56
|
|
|
51
57
|
useRounding = params.use_rounding !== undefined ? params.use_rounding : false;
|
|
58
|
+
|
|
59
|
+
// Start engine
|
|
60
|
+
initializeRuleSystem(businessRules);
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ==============================================
|
|
65
|
+
// Rule engine
|
|
66
|
+
// ==============================================
|
|
67
|
+
|
|
68
|
+
function initializeRuleSystem(businessRules) {
|
|
69
|
+
try {
|
|
70
|
+
ruleEngine = new RuleEngine();
|
|
71
|
+
ruleManager = new RuleManager();
|
|
72
|
+
|
|
73
|
+
if (businessRules && Object.keys(businessRules).length > 0) {
|
|
74
|
+
|
|
75
|
+
ruleManager.loadRules(businessRules);
|
|
76
|
+
console.log('Sistema de reglas inicializado correctamente');
|
|
77
|
+
|
|
78
|
+
} else {
|
|
79
|
+
console.warn('No se encontraron reglas de negocio para cargar');
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Error al inicializar el sistema de reglas:', error);
|
|
83
|
+
// Fallback: continuar sin reglas
|
|
84
|
+
ruleEngine = null;
|
|
85
|
+
ruleManager = null;
|
|
86
|
+
}
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
// ==============================================
|
|
@@ -71,18 +106,20 @@ function validateSaleStructure(sale) {
|
|
|
71
106
|
// CORE CALCULATION ENGINE (V2.1 SCOPE)
|
|
72
107
|
// ==============================================
|
|
73
108
|
|
|
74
|
-
function calcTotal(sale) {
|
|
109
|
+
async function calcTotal(sale) {
|
|
75
110
|
validateSaleStructure(sale);
|
|
76
111
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
112
|
+
let total = 0;
|
|
113
|
+
|
|
114
|
+
for (const product of sale.dwSaleProducts) {
|
|
115
|
+
const productSum = calcDwSaleProductSum(product, sale);
|
|
116
|
+
const discount = calcDwSaleProductDiscount(product, sale);
|
|
117
|
+
const netAmount = roundCurrency(productSum - discount);
|
|
118
|
+
const taxes = await getApplicableProductTaxes(product, netAmount, sale);
|
|
119
|
+
total += netAmount + taxes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return roundCurrency(total);
|
|
86
123
|
}
|
|
87
124
|
|
|
88
125
|
function calcSubtotal(sale) {
|
|
@@ -102,7 +139,7 @@ function calcSubtotal(sale) {
|
|
|
102
139
|
* @param {Object} params
|
|
103
140
|
* - sale:
|
|
104
141
|
*/
|
|
105
|
-
function preprocessSale(sale) {
|
|
142
|
+
async function preprocessSale(sale) {
|
|
106
143
|
|
|
107
144
|
const saleCurrency = currencies[sale.currency_id];
|
|
108
145
|
|
|
@@ -125,36 +162,40 @@ function preprocessSale(sale) {
|
|
|
125
162
|
}
|
|
126
163
|
|
|
127
164
|
if (sale.payments) {
|
|
128
|
-
|
|
165
|
+
const processedPayments = [];
|
|
166
|
+
for (const payment of sale.payments) {
|
|
129
167
|
const originalCurrency = currencies[payment.currency_id];
|
|
130
168
|
|
|
131
169
|
// If payment already has calculated fields, use them
|
|
132
170
|
if (payment.amount && payment.original_amount) {
|
|
133
|
-
|
|
171
|
+
processedPayments.push({
|
|
134
172
|
...payment,
|
|
135
173
|
original_currency_id: payment.currency_id,
|
|
136
174
|
original_currency_iso: originalCurrency?.iso,
|
|
137
175
|
currency_id: sale.currency_id,
|
|
138
176
|
currency_iso: sale.currency_iso,
|
|
139
|
-
change_amount:
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
...payment,
|
|
146
|
-
original_currency_id: payment.currency_id,
|
|
147
|
-
original_currency_iso: originalCurrency?.iso,
|
|
148
|
-
currency_id: sale.currency_id,
|
|
149
|
-
currency_iso: sale.currency_iso,
|
|
150
|
-
original_gived_amount: payment.gived_amount || payment.amount,
|
|
151
|
-
...calculatePaymentDetails({
|
|
177
|
+
change_amount: 0
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
// Otherwise calculate from basic payment data
|
|
181
|
+
const details = await calculatePaymentDetails({
|
|
152
182
|
...payment,
|
|
153
183
|
original_gived_amount: payment.gived_amount || payment.amount,
|
|
154
184
|
original_currency_iso: originalCurrency?.iso
|
|
155
|
-
}, sale)
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
}, sale);
|
|
186
|
+
|
|
187
|
+
processedPayments.push({
|
|
188
|
+
...payment,
|
|
189
|
+
original_currency_id: payment.currency_id,
|
|
190
|
+
original_currency_iso: originalCurrency?.iso,
|
|
191
|
+
currency_id: sale.currency_id,
|
|
192
|
+
currency_iso: sale.currency_iso,
|
|
193
|
+
original_gived_amount: payment.gived_amount || payment.amount,
|
|
194
|
+
...details
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
sale.payments = processedPayments;
|
|
158
199
|
}
|
|
159
200
|
|
|
160
201
|
if (sale.dwSaleProducts) {
|
|
@@ -183,6 +224,8 @@ function preprocessSale(sale) {
|
|
|
183
224
|
}));
|
|
184
225
|
});
|
|
185
226
|
}
|
|
227
|
+
// Set data for preprocessedSale
|
|
228
|
+
preprocessedSale = sale;
|
|
186
229
|
|
|
187
230
|
return sale;
|
|
188
231
|
}
|
|
@@ -270,18 +313,93 @@ function sumProductQuantity(products, productId) {
|
|
|
270
313
|
.reduce((sum, p) => sum + p.quantity, 0);
|
|
271
314
|
}
|
|
272
315
|
|
|
273
|
-
function getApplicableProductTaxes(saleProduct, productSum, sale) {
|
|
316
|
+
async function getApplicableProductTaxes(saleProduct, productSum, sale) {
|
|
274
317
|
// GET Taxes for specific products
|
|
275
318
|
const specificTaxes = getProductSpecificTaxes(saleProduct, sale);
|
|
276
319
|
// GET General Taxes
|
|
277
320
|
const generalTaxes = getGeneralTaxes(sale);
|
|
278
321
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
322
|
+
// Combination for taxes
|
|
323
|
+
const taxes = [...specificTaxes, ...generalTaxes];
|
|
324
|
+
|
|
325
|
+
// Get rules for taxes
|
|
326
|
+
const convertedRuleTaxes = ruleManager.getRulesForTrigger('calculate_taxes') || [];
|
|
327
|
+
|
|
328
|
+
// If no rules or engine, calculate normally
|
|
329
|
+
if (convertedRuleTaxes.length === 0 || !ruleEngine) {
|
|
330
|
+
const totalTax = taxes.reduce(
|
|
331
|
+
(total, tax) => {
|
|
332
|
+
const taxAmount = productSum * (tax.rate / 100);
|
|
333
|
+
return total + roundCurrency(taxAmount);
|
|
334
|
+
}, 0
|
|
335
|
+
);
|
|
336
|
+
return roundCurrency(totalTax);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Process each tax with rules
|
|
340
|
+
let totalTax = 0;
|
|
341
|
+
|
|
342
|
+
for (const tax of taxes) {
|
|
343
|
+
let adjustedRate = tax.rate;
|
|
344
|
+
let taxAmount = 0;
|
|
345
|
+
|
|
346
|
+
// Create context for each individual tax
|
|
347
|
+
const context = {
|
|
348
|
+
sale: {
|
|
349
|
+
...preprocessedSale,
|
|
350
|
+
subtotal: preprocessedSale.subtotal || calcSubtotal(sale),
|
|
351
|
+
customers: preprocessedSale.customers || sale.customers || []
|
|
352
|
+
},
|
|
353
|
+
tax: {
|
|
354
|
+
...tax,
|
|
355
|
+
tax_id: tax.id,
|
|
356
|
+
dw_product_id: tax.product_id || null,
|
|
357
|
+
sale_id: null,
|
|
358
|
+
amount: 0,
|
|
359
|
+
branch_id: sale.branch_id,
|
|
360
|
+
company_id: sale.company_id
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
// Execute rules for this specific tax
|
|
366
|
+
const ruleResults = await ruleEngine.executeRules(convertedRuleTaxes, context);
|
|
367
|
+
|
|
368
|
+
// Find successful rule result
|
|
369
|
+
const appliedRule = ruleResults.find(result => result.success && result.data);
|
|
370
|
+
|
|
371
|
+
if (appliedRule && appliedRule.data) {
|
|
372
|
+
const resultData = appliedRule.data;
|
|
373
|
+
|
|
374
|
+
if (resultData.result && resultData.result.tax) {
|
|
375
|
+
const taxResult = resultData.result.tax;
|
|
376
|
+
|
|
377
|
+
if (taxResult.rate !== undefined) {
|
|
378
|
+
adjustedRate = parseFloat(taxResult.rate);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (taxResult.amount !== undefined) {
|
|
382
|
+
taxAmount = parseFloat(taxResult.amount);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else if (resultData['tax.rate'] !== undefined) {
|
|
386
|
+
adjustedRate = parseFloat(resultData['tax.rate']);
|
|
387
|
+
} else if (resultData.rate !== undefined) {
|
|
388
|
+
adjustedRate = parseFloat(resultData.rate);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error(`Error applying rules for tax ${tax.abbreviation}:`, error);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Calculate tax amount
|
|
396
|
+
if (taxAmount > 0) {
|
|
397
|
+
totalTax += roundCurrency(taxAmount);
|
|
398
|
+
} else {
|
|
399
|
+
taxAmount = productSum * (adjustedRate / 100);
|
|
400
|
+
totalTax += roundCurrency(taxAmount);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
285
403
|
|
|
286
404
|
return roundCurrency(totalTax);
|
|
287
405
|
}
|
|
@@ -402,8 +520,8 @@ function applyExchangeStrategy(amount, exchangeRate) {
|
|
|
402
520
|
* @param {Object} sale - Sale object containing payments and calculation context
|
|
403
521
|
* @returns {number} Positive change amount if overpaid, otherwise 0
|
|
404
522
|
*/
|
|
405
|
-
function calcChange(sale) {
|
|
406
|
-
const total = calcTotal(sale);
|
|
523
|
+
async function calcChange(sale) {
|
|
524
|
+
const total = await calcTotal(sale);
|
|
407
525
|
const paymentSum = sumGivedAmountPayments(sale);
|
|
408
526
|
const change = paymentSum - total;
|
|
409
527
|
return roundCurrency(change > 0 ? change : 0);
|
|
@@ -414,9 +532,9 @@ function calcChange(sale) {
|
|
|
414
532
|
* @param {Object} sale - Sale object containing payments and calculation context
|
|
415
533
|
* @returns {number} Remaining debt amount (0 if fully paid)
|
|
416
534
|
*/
|
|
417
|
-
function calcDebt(sale) {
|
|
418
|
-
const total = calcTotal(sale);
|
|
419
|
-
const paymentSum = sumAmountPayments(sale);
|
|
535
|
+
async function calcDebt(sale) {
|
|
536
|
+
const total = await calcTotal(sale);
|
|
537
|
+
const paymentSum = await sumAmountPayments(sale);
|
|
420
538
|
const debt = total - paymentSum;
|
|
421
539
|
return roundCurrency(debt > 0 ? debt : 0);
|
|
422
540
|
}
|
|
@@ -426,10 +544,15 @@ function calcDebt(sale) {
|
|
|
426
544
|
* @param {Object} sale - Sale object containing payments array
|
|
427
545
|
* @returns {number} Total effective payment amount in sale currency
|
|
428
546
|
*/
|
|
429
|
-
function sumAmountPayments(sale) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
547
|
+
async function sumAmountPayments(sale) {
|
|
548
|
+
const payments = sale.payments || [];
|
|
549
|
+
let sum = 0;
|
|
550
|
+
|
|
551
|
+
for (const payment of payments) {
|
|
552
|
+
sum += await calcPaymentAmount(payment, sale);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return sum;
|
|
433
556
|
}
|
|
434
557
|
|
|
435
558
|
/**
|
|
@@ -449,8 +572,8 @@ function sumGivedAmountPayments(sale) {
|
|
|
449
572
|
* @param {Object} sale - Parent sale object for context
|
|
450
573
|
* @returns {number} Effective amount applied to sale total in sale currency
|
|
451
574
|
*/
|
|
452
|
-
function calcPaymentAmount(payment, sale) {
|
|
453
|
-
const total = calcTotal(sale);
|
|
575
|
+
async function calcPaymentAmount(payment, sale) {
|
|
576
|
+
const total = await calcTotal(sale);
|
|
454
577
|
const payments = sale.payments || [];
|
|
455
578
|
|
|
456
579
|
// Find current payment index
|
|
@@ -495,8 +618,8 @@ function calcPaymentAmount(payment, sale) {
|
|
|
495
618
|
* @param {Object} sale
|
|
496
619
|
* @returns {number} Amount in payment's original currency
|
|
497
620
|
*/
|
|
498
|
-
function calcOriginalPaymentAmount(payment, sale) {
|
|
499
|
-
const effectiveAmount = calcPaymentAmount(payment, sale);
|
|
621
|
+
async function calcOriginalPaymentAmount(payment, sale) {
|
|
622
|
+
const effectiveAmount = await calcPaymentAmount(payment, sale);
|
|
500
623
|
|
|
501
624
|
// Convert FROM sale currency TO payment's original currency
|
|
502
625
|
return exchange(
|
|
@@ -540,9 +663,9 @@ function calcGivedPaymentAmount(payment, sale) {
|
|
|
540
663
|
* - original_currency_iso: Payment currency ISO code
|
|
541
664
|
* - exchange_rate: Calculated exchange rate between currencies
|
|
542
665
|
*/
|
|
543
|
-
function calculatePaymentDetails(payment, sale) {
|
|
666
|
+
async function calculatePaymentDetails(payment, sale) {
|
|
544
667
|
// Calculate the actual amount that can be applied to the sale (in sale currency)
|
|
545
|
-
const amount = calcPaymentAmount(payment, sale);
|
|
668
|
+
const amount = await calcPaymentAmount(payment, sale);
|
|
546
669
|
|
|
547
670
|
// Calculate the given amount in sale currency (full conversion without debt cap)
|
|
548
671
|
const gived_amount = exchange(
|
|
@@ -568,7 +691,7 @@ function calculatePaymentDetails(payment, sale) {
|
|
|
568
691
|
original_amount, // Applied amount in payment's original currency
|
|
569
692
|
original_gived_amount: payment.original_gived_amount, // Original given amount unchanged
|
|
570
693
|
exchange_rate,
|
|
571
|
-
change_amount: roundCurrency(
|
|
694
|
+
change_amount: roundCurrency(gived_amount - amount) // Change in sale currency
|
|
572
695
|
};
|
|
573
696
|
}
|
|
574
697
|
|
|
@@ -620,40 +743,21 @@ function appliedWholesaleLevels(sale) {
|
|
|
620
743
|
return result;
|
|
621
744
|
}
|
|
622
745
|
|
|
623
|
-
function getSeparatedTaxes(sale) {
|
|
624
|
-
const result = getApplicableDwTaxes(sale);
|
|
625
|
-
|
|
626
|
-
return {
|
|
627
|
-
sale_taxes: result.taxes.map(tax => ({
|
|
628
|
-
tax_id: tax.id,
|
|
629
|
-
abbreviation: tax.abbreviation,
|
|
630
|
-
rate: tax.rate,
|
|
631
|
-
amount: tax.amount,
|
|
632
|
-
product_id: tax.product_id // null for general taxes
|
|
633
|
-
})),
|
|
634
|
-
tax_calculation_batches: result.tax_batches.map(batch => ({
|
|
635
|
-
product_id: batch.product_id,
|
|
636
|
-
product_index: batch.product_index,
|
|
637
|
-
sum: batch.sum,
|
|
638
|
-
amount: batch.amount,
|
|
639
|
-
rate: batch.rate,
|
|
640
|
-
tax_type: batch.tax_type
|
|
641
|
-
}))
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
|
|
645
746
|
/**
|
|
646
747
|
* Calculates all applicable taxes for the sale and returns them in API format
|
|
647
748
|
* @param {Object} sale - Sale data object
|
|
648
749
|
* @returns {Array} Array of tax objects with id, abbreviation, and amount
|
|
649
750
|
*/
|
|
650
|
-
function getApplicableDwTaxes(sale) {
|
|
751
|
+
async function getApplicableDwTaxes(sale) {
|
|
651
752
|
validateSaleStructure(sale);
|
|
652
753
|
|
|
653
754
|
const taxMap = {};
|
|
654
755
|
const taxBatches = [];
|
|
655
756
|
|
|
656
|
-
|
|
757
|
+
// Get rules for taxes
|
|
758
|
+
const convertedRuleTaxes = ruleManager.getRulesForTrigger('calculate_taxes') || [];
|
|
759
|
+
|
|
760
|
+
for (const [productIndex, product] of sale.dwSaleProducts.entries()) {
|
|
657
761
|
const productSum = calcDwSaleProductSum(product, sale);
|
|
658
762
|
const discount = calcDwSaleProductDiscount(product, sale);
|
|
659
763
|
const netAmount = roundCurrency(productSum - discount);
|
|
@@ -661,10 +765,69 @@ function getApplicableDwTaxes(sale) {
|
|
|
661
765
|
// Get applicable taxes for this product
|
|
662
766
|
const specificTaxes = getProductSpecificTaxes(product, sale);
|
|
663
767
|
const generalTaxes = getGeneralTaxes(sale);
|
|
664
|
-
|
|
768
|
+
let allTaxes = [...specificTaxes, ...generalTaxes];
|
|
769
|
+
|
|
770
|
+
// Process each tax with rules
|
|
771
|
+
for (const tax of allTaxes) {
|
|
772
|
+
let adjustedRate = tax.rate;
|
|
773
|
+
let taxAmount = 0;
|
|
774
|
+
|
|
775
|
+
if (convertedRuleTaxes.length > 0 && ruleEngine) {
|
|
776
|
+
try {
|
|
777
|
+
// Context
|
|
778
|
+
const context = {
|
|
779
|
+
sale: {
|
|
780
|
+
...preprocessedSale,
|
|
781
|
+
subtotal: preprocessedSale.subtotal || calcSubtotal(sale),
|
|
782
|
+
customers: preprocessedSale.customers || sale.customers || []
|
|
783
|
+
},
|
|
784
|
+
tax: {
|
|
785
|
+
...tax,
|
|
786
|
+
tax_id: tax.id,
|
|
787
|
+
dw_product_id: tax.product_id || null,
|
|
788
|
+
sale_id: null,
|
|
789
|
+
amount: 0,
|
|
790
|
+
branch_id: sale.branch_id,
|
|
791
|
+
company_id: sale.company_id
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const ruleResults = await ruleEngine.executeRules(convertedRuleTaxes, context);
|
|
796
|
+
const appliedRule = ruleResults.find(result => result.success && result.data);
|
|
797
|
+
|
|
798
|
+
if (appliedRule && appliedRule.data) {
|
|
799
|
+
// Result
|
|
800
|
+
const resultData = appliedRule.data;
|
|
801
|
+
|
|
802
|
+
if (resultData.result && resultData.result.tax) {
|
|
803
|
+
const taxResult = resultData.result.tax;
|
|
804
|
+
|
|
805
|
+
if (taxResult.rate !== undefined) {
|
|
806
|
+
adjustedRate = parseFloat(taxResult.rate);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (taxResult.amount !== undefined) {
|
|
810
|
+
taxAmount = parseFloat(taxResult.amount);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
else if (resultData['tax.rate'] !== undefined) {
|
|
815
|
+
adjustedRate = parseFloat(resultData['tax.rate']);
|
|
816
|
+
} else if (resultData.rate !== undefined) {
|
|
817
|
+
adjustedRate = parseFloat(resultData.rate);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} catch (error) {
|
|
821
|
+
console.error(`Error applying rules for tax ${tax.abbreviation}:`, error);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
665
824
|
|
|
666
|
-
|
|
667
|
-
|
|
825
|
+
if (taxAmount <= 0) {
|
|
826
|
+
taxAmount = netAmount * (adjustedRate / 100);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
taxAmount = roundCurrency(taxAmount);
|
|
830
|
+
|
|
668
831
|
const taxKey = getTaxProductIndex(tax, tax.product_id);
|
|
669
832
|
|
|
670
833
|
const taxBatch = {
|
|
@@ -672,41 +835,40 @@ function getApplicableDwTaxes(sale) {
|
|
|
672
835
|
product_index: productIndex,
|
|
673
836
|
sum: netAmount,
|
|
674
837
|
amount: taxAmount,
|
|
675
|
-
rate: tax.rate
|
|
676
|
-
|
|
838
|
+
rate: adjustedRate, // ¡¡Usar adjustedRate, no tax.rate!!
|
|
839
|
+
original_rate: tax.rate,
|
|
840
|
+
tax_type: tax.product_id ? 'specific' : 'general',
|
|
841
|
+
adjusted_by_rule: adjustedRate !== tax.rate
|
|
677
842
|
};
|
|
678
843
|
|
|
679
844
|
taxBatches.push(taxBatch);
|
|
680
845
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
846
|
+
// Usar adjustedRate en el mapa de impuestos
|
|
847
|
+
const mapTaxKey = `${tax.id}-${adjustedRate}-${tax.abbreviation}`;
|
|
848
|
+
|
|
849
|
+
if (taxMap[mapTaxKey]) {
|
|
850
|
+
taxMap[mapTaxKey].amount += taxAmount;
|
|
851
|
+
taxMap[mapTaxKey].batches.push(taxBatch);
|
|
684
852
|
} else {
|
|
685
|
-
taxMap[
|
|
853
|
+
taxMap[mapTaxKey] = {
|
|
686
854
|
id: tax.id,
|
|
687
855
|
abbreviation: tax.abbreviation,
|
|
688
|
-
rate:
|
|
856
|
+
rate: adjustedRate, // ¡¡IMPORTANTE: Usar adjustedRate!!
|
|
857
|
+
original_rate: tax.rate,
|
|
689
858
|
amount: taxAmount,
|
|
690
859
|
product_id: tax.product_id || null,
|
|
691
|
-
batches: [taxBatch]
|
|
860
|
+
batches: [taxBatch],
|
|
861
|
+
adjusted_by_rule: adjustedRate !== tax.rate,
|
|
862
|
+
rule_applied: convertedRuleTaxes.length > 0 ? 'f8147b8a-ad51-46e8-884d-c3374efc9b00' : null
|
|
692
863
|
};
|
|
693
864
|
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
696
867
|
|
|
697
868
|
return Object.values(taxMap).map(tax => ({
|
|
698
869
|
...tax,
|
|
699
870
|
amount: roundCurrency(tax.amount)
|
|
700
871
|
})).filter(tax => tax.amount > 0);
|
|
701
|
-
|
|
702
|
-
/*
|
|
703
|
-
return {
|
|
704
|
-
taxes: Object.values(taxMap).map(tax => ({
|
|
705
|
-
...tax,
|
|
706
|
-
amount: roundCurrency(tax.amount)
|
|
707
|
-
})).filter(tax => tax.amount > 0),
|
|
708
|
-
tax_batches: taxBatches
|
|
709
|
-
}; */
|
|
710
872
|
}
|
|
711
873
|
|
|
712
874
|
|
|
@@ -724,12 +886,12 @@ function getApplicableDwTaxes(sale) {
|
|
|
724
886
|
* - original_subtotal: Optional - subtotal in original currency
|
|
725
887
|
* - original_total: Optional - total in original currency
|
|
726
888
|
*/
|
|
727
|
-
function calculateSaleTotals(sale) {
|
|
889
|
+
async function calculateSaleTotals(sale) {
|
|
728
890
|
// Calculate base totals in sale currency
|
|
729
891
|
const subtotal = calcSubtotal(sale);
|
|
730
|
-
const total = calcTotal(sale);
|
|
731
|
-
const change = calcChange(sale);
|
|
732
|
-
const debt = calcDebt(sale);
|
|
892
|
+
const total = await calcTotal(sale);
|
|
893
|
+
const change = await calcChange(sale);
|
|
894
|
+
const debt = await calcDebt(sale);
|
|
733
895
|
|
|
734
896
|
const result = {
|
|
735
897
|
subtotal, // Sum of products before taxes
|
|
@@ -893,24 +1055,24 @@ function roundCurrency(value, overrideRounding) {
|
|
|
893
1055
|
|
|
894
1056
|
module.exports = {
|
|
895
1057
|
init,
|
|
896
|
-
preprocessSale,
|
|
897
|
-
calcTotal,
|
|
1058
|
+
preprocessSale, // Async
|
|
1059
|
+
calcTotal, // Async
|
|
898
1060
|
calcSubtotal,
|
|
899
|
-
calcDebt,
|
|
900
|
-
calcChange,
|
|
901
|
-
calcOriginalPaymentAmount,
|
|
1061
|
+
calcDebt, // Async
|
|
1062
|
+
calcChange, // Async
|
|
1063
|
+
calcOriginalPaymentAmount, // Async
|
|
902
1064
|
calcGivedPaymentAmount,
|
|
903
1065
|
calcTaxesByAbbreviation,
|
|
904
|
-
calculateSaleTotals,
|
|
905
|
-
calculatePaymentDetails,
|
|
1066
|
+
calculateSaleTotals, // Async
|
|
1067
|
+
calculatePaymentDetails, // Async
|
|
906
1068
|
appliedWholesaleLevels,
|
|
907
|
-
getApplicableDwTaxes,
|
|
1069
|
+
getApplicableDwTaxes, // Async
|
|
908
1070
|
getConsolidatedTaxes,
|
|
909
1071
|
calcDwSaleProductPrice,
|
|
910
1072
|
_internals: {
|
|
911
1073
|
calcDwSaleProductSum,
|
|
912
1074
|
calcDwSaleProductDiscount,
|
|
913
|
-
getApplicableProductTaxes,
|
|
1075
|
+
getApplicableProductTaxes, // Async
|
|
914
1076
|
exchange
|
|
915
1077
|
}
|
|
916
1078
|
};
|
package/src/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module softseti-sale-calculator-library
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { PaymentCalculation, RefundCalculation, RefundOptions, Sale, SaleItem, SaleTotals, WholesaleLevel } from "./interfaces";
|
|
6
|
+
import { PaymentCalculation, RefundCalculation, RefundOptions, Sale, SaleItem, SaleTotals, WholesaleLevel } from "./models/interfaces";
|
|
7
7
|
|
|
8
8
|
declare module 'softseti-sale-calculator-library' {
|
|
9
9
|
/**
|
|
@@ -15,9 +15,9 @@ declare module 'softseti-sale-calculator-library' {
|
|
|
15
15
|
/**
|
|
16
16
|
* Preprocesses sale data for calculations
|
|
17
17
|
* @param {Sale} sale - Sale object to preprocess
|
|
18
|
-
* @returns {Sale} Preprocessed sale object
|
|
18
|
+
* @returns {Promise<Sale>} Preprocessed sale object
|
|
19
19
|
*/
|
|
20
|
-
export function preprocessSale(sale: any): any
|
|
20
|
+
export function preprocessSale(sale: any): Promise<any>;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Calculates subtotal for a sale
|
|
@@ -29,23 +29,23 @@ declare module 'softseti-sale-calculator-library' {
|
|
|
29
29
|
/**
|
|
30
30
|
* Calculates total amount for a sale
|
|
31
31
|
* @param {Sale} sale - Sale object
|
|
32
|
-
* @returns {number} Total amount
|
|
32
|
+
* @returns {Promise<number>} Total amount
|
|
33
33
|
*/
|
|
34
|
-
export function calcTotal(sale: any): number
|
|
34
|
+
export function calcTotal(sale: any): Promise<number>;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Calculates remaining debt for a sale
|
|
38
38
|
* @param {Sale} sale - Sale object
|
|
39
|
-
* @returns {number} Debt amount
|
|
39
|
+
* @returns {Promise<number>} Debt amount
|
|
40
40
|
*/
|
|
41
|
-
export function calcDebt(sale: any): number
|
|
41
|
+
export function calcDebt(sale: any): Promise<number>;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Calculates change amount for a sale
|
|
45
45
|
* @param {Sale} sale - Sale object
|
|
46
|
-
* @returns {number} Change amount
|
|
46
|
+
* @returns {Promise<number>} Change amount
|
|
47
47
|
*/
|
|
48
|
-
export function calcChange(sale: any): number
|
|
48
|
+
export function calcChange(sale: any): Promise<number>;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Calculates taxes by tax abbreviation
|
|
@@ -59,9 +59,9 @@ declare module 'softseti-sale-calculator-library' {
|
|
|
59
59
|
* Calculates original payment amount
|
|
60
60
|
* @param {object} payment - Payment object
|
|
61
61
|
* @param {Sale} sale - Sale object
|
|
62
|
-
* @returns {number} Original payment amount
|
|
62
|
+
* @returns {Promise<number>} Original payment amount
|
|
63
63
|
*/
|
|
64
|
-
export function calcOriginalPaymentAmount(payment: any, sale: Sale): number
|
|
64
|
+
export function calcOriginalPaymentAmount(payment: any, sale: Sale): Promise<number>;
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Calculates given payment amount
|
|
@@ -75,9 +75,9 @@ declare module 'softseti-sale-calculator-library' {
|
|
|
75
75
|
* Calculates detailed payment information
|
|
76
76
|
* @param {object} payment - Payment object
|
|
77
77
|
* @param {Sale} sale - Sale object
|
|
78
|
-
* @returns {PaymentCalculation} Payment details
|
|
78
|
+
* @returns {Promise<PaymentCalculation>} Payment details
|
|
79
79
|
*/
|
|
80
|
-
export function calculatePaymentDetails(payment: any, sale: Sale): PaymentCalculation
|
|
80
|
+
export function calculatePaymentDetails(payment: any, sale: Sale): Promise<PaymentCalculation>;
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
export function calcDwSaleProductPrice(saleProduct: any, sale: any): number;
|
|
@@ -91,9 +91,9 @@ declare module 'softseti-sale-calculator-library' {
|
|
|
91
91
|
/**
|
|
92
92
|
* Calculates all applicable taxes for the sale and returns them in API format
|
|
93
93
|
* @param {Object} sale - Sale object
|
|
94
|
-
* @returns {Array} Array of tax objects with id, abbreviation, and amount
|
|
94
|
+
* @returns {Promise<Array>} Array of tax objects with id, abbreviation, and amount
|
|
95
95
|
*/
|
|
96
|
-
export function getApplicableDwTaxes(sale: any): any[]
|
|
96
|
+
export function getApplicableDwTaxes(sale: any): Promise<any[]>;
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
@@ -106,9 +106,9 @@ declare module 'softseti-sale-calculator-library' {
|
|
|
106
106
|
/**
|
|
107
107
|
* Calculates comprehensive sale totals
|
|
108
108
|
* @param {Sale} sale - Sale object
|
|
109
|
-
* @returns {SaleTotals} Sale totals
|
|
109
|
+
* @returns {Promise<SaleTotals>} Sale totals
|
|
110
110
|
*/
|
|
111
|
-
export function calculateSaleTotals(sale: any): SaleTotals
|
|
111
|
+
export function calculateSaleTotals(sale: any): Promise<SaleTotals>;
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Internal calculation methods (advanced use only)
|
|
@@ -130,7 +130,7 @@ declare module 'softseti-sale-calculator-library' {
|
|
|
130
130
|
* Gets applicable product taxes
|
|
131
131
|
* @internal
|
|
132
132
|
*/
|
|
133
|
-
getApplicableProductTaxes: (saleProduct: any, productSum: number, sale: Sale) => number
|
|
133
|
+
getApplicableProductTaxes: (saleProduct: any, productSum: number, sale: Sale) => Promise<number>;
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
136
|
* Currency exchange calculation
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const SaleCalculator = require('./
|
|
2
|
-
const RefundCalculator = require('./refund-calculator');
|
|
1
|
+
const SaleCalculator = require('./core/sale-calculator');
|
|
2
|
+
const RefundCalculator = require('./core/refund-calculator');
|
|
3
3
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
...SaleCalculator,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/models/BusinessRuleModel.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Business Rule Model for GoRules integration
|
|
5
|
+
* @class BusinessRuleModel
|
|
6
|
+
*/
|
|
7
|
+
class BusinessRuleModel {
|
|
8
|
+
constructor(data = {}) {
|
|
9
|
+
this.id = data.id || '';
|
|
10
|
+
this.name = data.name || '';
|
|
11
|
+
this.description = data.description || '';
|
|
12
|
+
this.entity_type = data.entity_type || '';
|
|
13
|
+
this.trigger_event = data.trigger_event || '';
|
|
14
|
+
this.type_rule = data.type_rule || '';
|
|
15
|
+
this.company_id = data.company_id || 0;
|
|
16
|
+
this.branch_id = data.branch_id || 0;
|
|
17
|
+
this.execution_order = data.execution_order || 1;
|
|
18
|
+
this.cumulative = data.cumulative || false;
|
|
19
|
+
this.valid_from = data.valid_from || null;
|
|
20
|
+
this.valid_until = data.valid_until || null;
|
|
21
|
+
this.rule_content = data.rule_content || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if rule is currently active
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
isActive() {
|
|
29
|
+
const now = new Date();
|
|
30
|
+
|
|
31
|
+
if (this.valid_from && new Date(this.valid_from) > now) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this.valid_until && new Date(this.valid_until) < now) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate rule structure
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
isValid() {
|
|
47
|
+
return this.rule_content &&
|
|
48
|
+
this.rule_content.nodes &&
|
|
49
|
+
this.rule_content.edges;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = BusinessRuleModel;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/rules/RuleManager.js
|
|
2
|
+
const BusinessRuleModel = require('../models/BusinessRuleModel');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rule Manager for handling business rule execution
|
|
6
|
+
* @class RuleManager
|
|
7
|
+
*/
|
|
8
|
+
class RuleManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.rulesByTrigger = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Load rules organized by trigger event
|
|
15
|
+
* @param {Object} rules - Rules indexed by trigger event
|
|
16
|
+
*/
|
|
17
|
+
loadRules(rules) {
|
|
18
|
+
this.rulesByTrigger.clear();
|
|
19
|
+
Object.entries(rules).forEach(([trigger, ruleList]) => {
|
|
20
|
+
const ruleModels = ruleList.map(rule => new BusinessRuleModel(rule));
|
|
21
|
+
this.rulesByTrigger.set(trigger, ruleModels);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get rules for a specific trigger event
|
|
27
|
+
* @param {string} triggerEvent - Trigger event name
|
|
28
|
+
* @returns {Array<BusinessRuleModel>}
|
|
29
|
+
*/
|
|
30
|
+
getRulesForTrigger(triggerEvent) {
|
|
31
|
+
const rules = this.rulesByTrigger.get(triggerEvent) || [];
|
|
32
|
+
return rules.filter(rule => rule.isActive() && rule.isValid());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get all trigger events with rules
|
|
37
|
+
* @returns {Array<string>}
|
|
38
|
+
*/
|
|
39
|
+
getAvailableTriggers() {
|
|
40
|
+
return Array.from(this.rulesByTrigger.keys());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Clear all loaded rules
|
|
45
|
+
*/
|
|
46
|
+
clearRules() {
|
|
47
|
+
this.rulesByTrigger.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get statistics about loaded rules
|
|
52
|
+
* @returns {Object}
|
|
53
|
+
*/
|
|
54
|
+
getStats() {
|
|
55
|
+
const stats = {
|
|
56
|
+
totalRules: 0,
|
|
57
|
+
byTrigger: {},
|
|
58
|
+
activeRules: 0
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
this.rulesByTrigger.forEach((rules, trigger) => {
|
|
62
|
+
const activeRules = rules.filter(rule => rule.isActive());
|
|
63
|
+
stats.byTrigger[trigger] = {
|
|
64
|
+
total: rules.length,
|
|
65
|
+
active: activeRules.length
|
|
66
|
+
};
|
|
67
|
+
stats.totalRules += rules.length;
|
|
68
|
+
stats.activeRules += activeRules.length;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return stats;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = RuleManager;
|
|
File without changes
|
|
File without changes
|