softseti-sale-calculator-library 4.0.1 → 4.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "softseti-sale-calculator-library",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "Sales calculation engine by Softseti",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Calculation Engine
3
+ * Handles core sale calculations including subtotal, total, and wholesale pricing
4
+ */
5
+ const RoundingUtils = require('../utils/RoundingUtils');
6
+ const ValidationUtils = require('../utils/ValidationUtils');
7
+
8
+ class CalculationEngine {
9
+ /**
10
+ * @param {SalesCalculator} calculator - Parent calculator instance
11
+ */
12
+ constructor(calculator) {
13
+ this.calculator = calculator;
14
+
15
+ // Initialize ALL use cases
16
+ this.calculateSubtotal = new (require('../usecases/CalculateSubtotal'))(calculator);
17
+ this.calculateTotal = new (require('../usecases/CalculateTotal'))(calculator);
18
+ this.calculateProductPrice = new (require('../usecases/CalculateProductPrice'))(calculator);
19
+ this.calculateProductDiscount = new (require('../usecases/CalculateProductDiscount'))(calculator);
20
+ this.calculateProductSum = new (require('../usecases/CalculateProductSum'))(calculator);
21
+ this.calculateWholesale = new (require('../usecases/CalculateWholesale'))(calculator);
22
+ }
23
+
24
+ /**
25
+ * Preprocess sale data
26
+ * @param {Object} sale - Sale object
27
+ * @returns {Promise<Object>} Preprocessed sale
28
+ */
29
+ async preprocessSale(sale) {
30
+ ValidationUtils.validateSaleStructure(sale, this.calculator.data.products);
31
+
32
+ const saleCurrency = this.calculator.data.currencies[sale.currency_id];
33
+
34
+ // Add currency ISO if not provided
35
+ if (!sale.currency_iso && saleCurrency?.iso) {
36
+ sale.currency_iso = saleCurrency.iso;
37
+ }
38
+
39
+ // Set default currency if none specified
40
+ if (!sale.currency_id && !sale.currency_iso) {
41
+ sale.currency_id = 88; // Default currency ID
42
+ sale.currency_iso = 'MXN'; // Default currency
43
+ }
44
+
45
+ // Process deals
46
+ if (sale.dwSaleDeals) {
47
+ sale.dwSaleDeals = sale.dwSaleDeals.map(deal => ({
48
+ ...deal,
49
+ sum: deal.amount ? deal.amount * -1 : 0
50
+ }));
51
+ }
52
+
53
+ // Process payments
54
+ if (sale.payments) {
55
+ const processedPayments = [];
56
+ for (const payment of sale.payments) {
57
+ const originalCurrency = this.calculator.data.currencies[payment.currency_id];
58
+
59
+ if (payment.amount && payment.original_amount) {
60
+ processedPayments.push({
61
+ ...payment,
62
+ original_currency_id: payment.currency_id,
63
+ original_currency_iso: originalCurrency?.iso,
64
+ currency_id: sale.currency_id,
65
+ currency_iso: sale.currency_iso,
66
+ change_amount: 0
67
+ });
68
+ } else {
69
+ const details = await this.calculator.paymentEngine.calculatePaymentDetails({
70
+ ...payment,
71
+ original_gived_amount: payment.gived_amount || payment.amount,
72
+ original_currency_iso: originalCurrency?.iso
73
+ }, sale);
74
+
75
+ processedPayments.push({
76
+ ...payment,
77
+ original_currency_id: payment.currency_id,
78
+ original_currency_iso: originalCurrency?.iso,
79
+ currency_id: sale.currency_id,
80
+ currency_iso: sale.currency_iso,
81
+ original_gived_amount: payment.gived_amount || payment.amount,
82
+ ...details
83
+ });
84
+ }
85
+ }
86
+ sale.payments = processedPayments;
87
+ }
88
+
89
+ // Process products
90
+ if (sale.dwSaleProducts) {
91
+ sale.dwSaleProducts = sale.dwSaleProducts.map(product => {
92
+ const productData = this.calculator.data.products[product.product_id];
93
+ const productCurrency = this.calculator.data.currencies[productData?.currency_id];
94
+
95
+ // Calculate actual price (including wholesale)
96
+ const actualPrice = this.calcDwSaleProductPrice(product, sale);
97
+
98
+ return {
99
+ ...product,
100
+ price: actualPrice,
101
+ public_price: actualPrice,
102
+ currency_iso: productCurrency?.iso || sale.currency_iso,
103
+ currency_id: productData?.currency_id || sale.currency_id
104
+ };
105
+ });
106
+ }
107
+
108
+ // Process wholesale levels
109
+ if (this.calculator.data.wholesaleLevels) {
110
+ Object.keys(this.calculator.data.wholesaleLevels).forEach(productId => {
111
+ this.calculator.data.wholesaleLevels[productId] =
112
+ this.calculator.data.wholesaleLevels[productId].map(level => ({
113
+ ...level,
114
+ price: RoundingUtils.roundCurrency(level.price, this.calculator.config)
115
+ }));
116
+ });
117
+ }
118
+
119
+ // Store preprocessed sale
120
+ this.calculator.preprocessedSale = sale;
121
+
122
+ return sale;
123
+ }
124
+
125
+ /**
126
+ * Calculate total sale amount
127
+ * @param {Object} sale - Sale object
128
+ * @returns {Promise<number>} Total amount
129
+ */
130
+ async calcTotal(sale) {
131
+ return this.calculateTotal.calculate(sale);
132
+ }
133
+
134
+ /**
135
+ * Calculate subtotal
136
+ * @param {Object} sale - Sale object
137
+ * @returns {number} Subtotal amount
138
+ */
139
+ calcSubtotal(sale) {
140
+ return this.calculateSubtotal.calculate(sale);
141
+ }
142
+
143
+ /**
144
+ * Calculate product discount
145
+ * @param {Object} product - Product object
146
+ * @param {Object} sale - Sale object
147
+ * @returns {number} Discount amount
148
+ */
149
+ calcDwSaleProductDiscount(product, sale) {
150
+ return this.calculateProductDiscount.calculate(product, sale);
151
+ }
152
+
153
+ /**
154
+ * Calculate product sum
155
+ * @param {Object} saleProduct - Sale product object
156
+ * @param {Object} sale - Sale object
157
+ * @returns {number} Product sum
158
+ */
159
+ calcDwSaleProductSum(saleProduct, sale) {
160
+ return this.calculateProductSum.calculate(saleProduct, sale);
161
+ }
162
+
163
+ /**
164
+ * Calculate product price
165
+ * @param {Object} saleProduct - Sale product object
166
+ * @param {Object} sale - Sale object
167
+ * @returns {number} Product price
168
+ */
169
+ calcDwSaleProductPrice(saleProduct, sale) {
170
+ return this.calculateProductPrice.calculate(saleProduct, sale);
171
+ }
172
+
173
+ /**
174
+ * Get applied wholesale levels
175
+ * @param {Object} sale - Sale object
176
+ * @returns {Array} Applied wholesale levels
177
+ */
178
+ appliedWholesaleLevels(sale) {
179
+ return this.calculateWholesale.getAppliedLevels(sale);
180
+ }
181
+
182
+ /**
183
+ * Calculate sale totals
184
+ * @param {Object} sale - Sale object
185
+ * @returns {Promise<Object>} Sale totals object
186
+ */
187
+ async calculateSaleTotals(sale) {
188
+ const subtotal = this.calcSubtotal(sale);
189
+ const total = await this.calcTotal(sale);
190
+ const change = await this.calculator.paymentEngine.calcChange(sale);
191
+ const debt = await this.calculator.paymentEngine.calcDebt(sale);
192
+
193
+ const result = {
194
+ subtotal,
195
+ total,
196
+ change,
197
+ debt
198
+ };
199
+
200
+ // Add original currency totals if different
201
+ if (sale.original_currency_iso && sale.original_currency_iso !== sale.currency_iso) {
202
+ result.original_subtotal = this.calculator.currencyEngine.exchange(
203
+ subtotal,
204
+ sale.currency_iso,
205
+ sale.original_currency_iso
206
+ );
207
+ result.original_total = this.calculator.currencyEngine.exchange(
208
+ total,
209
+ sale.currency_iso,
210
+ sale.original_currency_iso
211
+ );
212
+ }
213
+
214
+ return result;
215
+ }
216
+ }
217
+
218
+ module.exports = CalculationEngine;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Currency Engine
3
+ * Handles currency exchange and conversion
4
+ */
5
+ const RoundingUtils = require('../utils/RoundingUtils');
6
+
7
+ class CurrencyEngine {
8
+ /**
9
+ * @param {SalesCalculator} calculator - Parent calculator instance
10
+ */
11
+ constructor(calculator) {
12
+ this.calculator = calculator;
13
+ }
14
+
15
+ /**
16
+ * Convert amount between currencies
17
+ * @param {number} amount - Amount to convert
18
+ * @param {string} fromCurrency - ISO code (e.g. 'MXN')
19
+ * @param {string} toCurrency - ISO code (e.g. 'USD')
20
+ * @returns {number} Converted amount
21
+ */
22
+ exchange(amount, fromCurrency, toCurrency) {
23
+ if (fromCurrency === toCurrency) {
24
+ return RoundingUtils.roundCurrency(amount, this.calculator.config);
25
+ }
26
+
27
+ // Try exchange rate first
28
+ const exchangeRateModel = this.findExchangeRate(fromCurrency, toCurrency);
29
+ if (exchangeRateModel) {
30
+ return RoundingUtils.roundCurrency(
31
+ this.applyExchangeStrategy(amount, exchangeRateModel),
32
+ this.calculator.config
33
+ );
34
+ }
35
+
36
+ // Fallback to currency converter rate
37
+ const currencyConverterRateModel = this.findCurrencyConverterRate(fromCurrency, toCurrency);
38
+ if (!currencyConverterRateModel) {
39
+ throw new Error(`No exchange rate found for ${fromCurrency}->${toCurrency}`);
40
+ }
41
+
42
+ return RoundingUtils.roundCurrency(
43
+ amount * currencyConverterRateModel.rate,
44
+ this.calculator.config
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Find exchange rate
50
+ * @param {string} from - From currency ISO
51
+ * @param {string} to - To currency ISO
52
+ * @returns {Object|null} Exchange rate model or null
53
+ */
54
+ findExchangeRate(from, to) {
55
+ return Object.values(this.calculator.data.exchangeRates).find(rate =>
56
+ rate.from_currency_iso === from &&
57
+ rate.to_currency_iso === to
58
+ );
59
+ }
60
+
61
+ /**
62
+ * Find currency converter rate
63
+ * @param {string} from - From currency ISO
64
+ * @param {string} to - To currency ISO
65
+ * @returns {Object|null} Currency converter rate or null
66
+ */
67
+ findCurrencyConverterRate(from, to) {
68
+ return Object.values(this.calculator.data.currencyConverterRates).find(rate =>
69
+ rate.from_currency_iso === from &&
70
+ rate.to_currency_iso === to
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Apply exchange strategy
76
+ * @param {number} amount - Amount to convert
77
+ * @param {Object} exchangeRate - Exchange rate model
78
+ * @returns {number} Converted amount
79
+ */
80
+ applyExchangeStrategy(amount, exchangeRate) {
81
+ return (amount * exchangeRate.to_currency_value) / exchangeRate.from_currency_value;
82
+ }
83
+
84
+ /**
85
+ * Get currency by ID
86
+ * @param {number} currencyId - Currency ID
87
+ * @returns {Object|null} Currency object or null
88
+ */
89
+ getCurrencyById(currencyId) {
90
+ return this.calculator.data.currencies[currencyId] || null;
91
+ }
92
+
93
+ /**
94
+ * Get currency ISO by ID
95
+ * @param {number} currencyId - Currency ID
96
+ * @returns {string|null} Currency ISO or null
97
+ */
98
+ getCurrencyIso(currencyId) {
99
+ const currency = this.getCurrencyById(currencyId);
100
+ return currency ? currency.iso : null;
101
+ }
102
+ }
103
+
104
+ module.exports = CurrencyEngine;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Payment Engine
3
+ * Handles payment calculations, currency conversions, and debt management
4
+ */
5
+ const RoundingUtils = require('../utils/RoundingUtils');
6
+
7
+ class PaymentEngine {
8
+ /**
9
+ * @param {SalesCalculator} calculator - Parent calculator instance
10
+ */
11
+ constructor(calculator) {
12
+ this.calculator = calculator;
13
+ }
14
+
15
+ /**
16
+ * Calculate change amount
17
+ * @param {Object} sale - Sale object
18
+ * @returns {Promise<number>} Change amount
19
+ */
20
+ async calcChange(sale) {
21
+ const total = await this.calculator.calculationEngine.calcTotal(sale);
22
+ const paymentSum = this.sumGivedAmountPayments(sale);
23
+ const change = paymentSum - total;
24
+ return RoundingUtils.roundCurrency(change > 0 ? change : 0, this.calculator.config);
25
+ }
26
+
27
+ /**
28
+ * Calculate remaining debt
29
+ * @param {Object} sale - Sale object
30
+ * @returns {Promise<number>} Debt amount
31
+ */
32
+ async calcDebt(sale) {
33
+ const total = await this.calculator.calculationEngine.calcTotal(sale);
34
+ const paymentSum = await this.sumAmountPayments(sale);
35
+ const debt = total - paymentSum;
36
+ return RoundingUtils.roundCurrency(debt > 0 ? debt : 0, this.calculator.config);
37
+ }
38
+
39
+ /**
40
+ * Sum effective payment amounts
41
+ * @param {Object} sale - Sale object
42
+ * @returns {Promise<number>} Total effective payment amount
43
+ */
44
+ async sumAmountPayments(sale) {
45
+ const payments = sale.payments || [];
46
+ let sum = 0;
47
+
48
+ for (const payment of payments) {
49
+ sum += await this.calcPaymentAmount(payment, sale);
50
+ }
51
+
52
+ return RoundingUtils.roundCurrency(sum, this.calculator.config);
53
+ }
54
+
55
+ /**
56
+ * Sum raw payment amounts
57
+ * @param {Object} sale - Sale object
58
+ * @returns {number} Total given payment amount
59
+ */
60
+ sumGivedAmountPayments(sale) {
61
+ return RoundingUtils.roundCurrency((sale.payments || []).reduce((sum, payment) => {
62
+ return sum + this.calcGivedPaymentAmount(payment, sale);
63
+ }, 0), this.calculator.config);
64
+ }
65
+
66
+ /**
67
+ * Calculate effective payment amount
68
+ * @param {Object} payment - Payment object
69
+ * @param {Object} sale - Sale object
70
+ * @returns {Promise<number>} Effective amount in sale currency
71
+ */
72
+ async calcPaymentAmount(payment, sale) {
73
+ const total = await this.calculator.calculationEngine.calcTotal(sale);
74
+ const payments = sale.payments || [];
75
+
76
+ const index = payments.findIndex(p =>
77
+ p.original_gived_amount === payment.original_gived_amount &&
78
+ p.currency_id === payment.currency_id &&
79
+ p.payment_type_id === payment.payment_type_id
80
+ );
81
+
82
+ if (index === -1) return 0;
83
+
84
+ let accumulated = 0;
85
+
86
+ for (let i = 0; i < index; i++) {
87
+ const prevPayment = payments[i];
88
+ const amount = this.calculator.currencyEngine.exchange(
89
+ prevPayment.original_gived_amount,
90
+ prevPayment.original_currency_iso,
91
+ sale.currency_iso
92
+ );
93
+ const remaining = total - accumulated;
94
+ accumulated += Math.min(amount, remaining);
95
+ }
96
+
97
+ const currentAmount = this.calculator.currencyEngine.exchange(
98
+ payment.original_gived_amount,
99
+ payment.original_currency_iso,
100
+ sale.currency_iso
101
+ );
102
+
103
+ const remaining = total - accumulated;
104
+ const appliedAmount = Math.min(currentAmount, Math.max(remaining, 0));
105
+
106
+ return RoundingUtils.roundCurrency(appliedAmount, this.calculator.config);
107
+ }
108
+
109
+ /**
110
+ * Calculate original payment amount
111
+ * @param {Object} payment - Payment object
112
+ * @param {Object} sale - Sale object
113
+ * @returns {Promise<number>} Amount in payment's original currency
114
+ */
115
+ async calcOriginalPaymentAmount(payment, sale) {
116
+ const effectiveAmount = await this.calcPaymentAmount(payment, sale);
117
+
118
+ return this.calculator.currencyEngine.exchange(
119
+ effectiveAmount,
120
+ sale.currency_iso,
121
+ payment.original_currency_iso
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Calculate given payment amount
127
+ * @param {Object} payment - Payment object
128
+ * @param {Object} sale - Sale object
129
+ * @returns {number} Converted amount without debt adjustment
130
+ */
131
+ calcGivedPaymentAmount(payment, sale) {
132
+ return RoundingUtils.roundCurrency(this.calculator.currencyEngine.exchange(
133
+ payment.original_gived_amount,
134
+ payment.original_currency_iso,
135
+ sale.currency_iso
136
+ ), this.calculator.config);
137
+ }
138
+
139
+ /**
140
+ * Calculate complete payment details
141
+ * @param {Object} payment - Payment object
142
+ * @param {Object} sale - Sale object
143
+ * @returns {Promise<Object>} Payment details
144
+ */
145
+ async calculatePaymentDetails(payment, sale) {
146
+ const amount = await this.calcPaymentAmount(payment, sale);
147
+
148
+ const gived_amount = this.calculator.currencyEngine.exchange(
149
+ payment.original_gived_amount,
150
+ payment.original_currency_iso,
151
+ sale.currency_iso
152
+ );
153
+
154
+ const original_amount = this.calculator.currencyEngine.exchange(
155
+ amount,
156
+ sale.currency_iso,
157
+ payment.original_currency_iso
158
+ );
159
+
160
+ const exchange_rate = gived_amount / payment.original_gived_amount;
161
+
162
+ return {
163
+ ...payment,
164
+ amount: RoundingUtils.roundCurrency(amount, this.calculator.config),
165
+ gived_amount: RoundingUtils.roundCurrency(gived_amount, this.calculator.config),
166
+ original_amount: RoundingUtils.roundCurrency(original_amount, this.calculator.config),
167
+ original_gived_amount: payment.original_gived_amount,
168
+ exchange_rate: RoundingUtils.roundCurrency(exchange_rate, this.calculator.config),
169
+ change_amount: RoundingUtils.roundCurrency(gived_amount - amount, this.calculator.config)
170
+ };
171
+ }
172
+ }
173
+
174
+ module.exports = PaymentEngine;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Rule Engine Wrapper
3
+ * Provides abstraction for rule engine with fallback support
4
+ */
5
+ class RuleEngineWrapper {
6
+ constructor() {
7
+ this.engine = null;
8
+ this.ruleCache = new Map();
9
+ this.initializeEngine();
10
+ }
11
+
12
+ /**
13
+ * Initialize engine with graceful fallback
14
+ */
15
+ initializeEngine() {
16
+ try {
17
+ // Try to load ZenEngine if available
18
+ const { ZenEngine } = require('@gorules/zen-engine');
19
+ this.engine = new ZenEngine();
20
+ console.log('ZenEngine loaded successfully');
21
+ } catch (error) {
22
+ console.warn('ZenEngine not available, using fallback mode:', error.message);
23
+ this.engine = null;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Check if rule engine is available
29
+ * @returns {boolean} True if engine is available
30
+ */
31
+ isAvailable() {
32
+ return this.engine !== null;
33
+ }
34
+
35
+ /**
36
+ * Evaluate a business rule
37
+ * @param {Object} ruleContent - Rule content
38
+ * @param {Object} context - Execution context
39
+ * @returns {Promise<Object>} Evaluation result
40
+ */
41
+ async evaluateRule(ruleContent, context) {
42
+ if (!this.isAvailable()) {
43
+ return this.fallbackEvaluation(ruleContent, context);
44
+ }
45
+
46
+ try {
47
+ const decision = this.engine.createDecision(ruleContent);
48
+ return await decision.safeEvaluate(context);
49
+ } catch (error) {
50
+ console.error('Rule evaluation error, using fallback:', error);
51
+ return this.fallbackEvaluation(ruleContent, context);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Execute multiple rules
57
+ * @param {Array} rules - Rules to execute
58
+ * @param {Object} context - Execution context
59
+ * @returns {Promise<Array>} Execution results
60
+ */
61
+ async executeRules(rules, context) {
62
+ const results = [];
63
+
64
+ for (const rule of rules) {
65
+ try {
66
+ const result = await this.evaluateRule(rule.rule_content, context);
67
+ results.push(result);
68
+ } catch (error) {
69
+ results.push({
70
+ ruleId: rule.id,
71
+ success: false,
72
+ error: error.message
73
+ });
74
+ }
75
+ }
76
+
77
+ return results;
78
+ }
79
+
80
+ /**
81
+ * Fallback evaluation when engine is not available
82
+ * @param {Object} ruleContent - Rule content
83
+ * @param {Object} context - Execution context
84
+ * @returns {Object} Default result
85
+ */
86
+ fallbackEvaluation(ruleContent, context) {
87
+ // Return default result when engine is not available
88
+ return {
89
+ success: true,
90
+ data: {},
91
+ message: 'Rule engine not available, using fallback'
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Validate rule structure
97
+ * @param {Object} ruleContent - Rule content
98
+ * @returns {boolean} True if rule is valid
99
+ */
100
+ validateRule(ruleContent) {
101
+ if (!ruleContent || typeof ruleContent !== 'object') {
102
+ return false;
103
+ }
104
+
105
+ const requiredProperties = ['nodes', 'edges', 'contentType'];
106
+ return requiredProperties.every(prop => ruleContent.hasOwnProperty(prop));
107
+ }
108
+ }
109
+
110
+ module.exports = RuleEngineWrapper;