UNPKG

softseti-sale-calculator-library

Version:

Sales calculation engine by Softseti

131 lines (115 loc) 4.6 kB
// src/rules/RuleTaxApplicator.js /** * Single integration seam for tax business rules. It owns everything about * running rules against a tax: which rules apply for a trigger, evaluating them * through the injected engine, picking the applied result, extracting the * adjusted rate/amount and collecting the rule_tracking trace. * * The rule engine and rule manager are injected, so the calculator no longer * reaches into module-level engine globals and a different engine (e.g. a future * web-wasm one) or rule set can be swapped in by constructing a new applicator. */ class RuleTaxApplicator { constructor(ruleEngine, ruleManager) { this.ruleEngine = ruleEngine || null; this.ruleManager = ruleManager || null; this.tracking = []; } /** True when rules can actually run (engine present). */ enabled() { return !!this.ruleEngine; } /** Rules registered for a trigger event (empty when none / no manager). */ rulesFor(trigger) { return (this.ruleManager && this.ruleManager.getRulesForTrigger(trigger)) || []; } /** Rule trace collected so far this calculation. */ getTracking() { return this.tracking; } /** * Evaluate the given rules for one tax and return the (possibly) adjusted * rate/amount. Falls back to the tax's own rate with amount 0 when no rule * applies, so the caller computes the tax normally. Records the trace. * * @param {{track?: boolean}} [options] track=false skips recording the trace * (used by the internal totals helper so the same tax isn't traced twice). * @returns {{adjustedRate: number, taxAmount: number}} */ async resolveTax(trigger, rules, tax, context, product, options = {}) { const track = options.track !== false; let adjustedRate = tax.rate; let taxAmount = 0; if (!rules || rules.length === 0 || !this.ruleEngine) { return { adjustedRate, taxAmount }; } try { const ruleResults = await this.ruleEngine.executeRules(rules, context); const applied = this._pickApplied(ruleResults); if (applied && applied.data) { adjustedRate = this._extractRate(applied.data, adjustedRate); taxAmount = this._extractAmount(applied.data, taxAmount); } if (track) { this._track(trigger, tax, product, ruleResults, applied, tax.rate, adjustedRate); } } catch (error) { console.error(`Error applying rules for tax ${tax.abbreviation}:`, error); } return { adjustedRate, taxAmount }; } /** First successful result that carries a usable rate or amount. */ _pickApplied(ruleResults) { for (const result of ruleResults) { if (result.success && result.data) { const rd = result.data; const hasRate = rd.result?.tax?.rate !== undefined || rd['tax.rate'] !== undefined || rd.rate !== undefined; const hasAmount = rd.result?.tax?.amount !== undefined || rd['tax.amount'] !== undefined || rd.amount !== undefined; if (hasRate || hasAmount) { return result; } } } return null; } _extractRate(rd, fallback) { if (rd.result?.tax?.rate !== undefined) return parseFloat(rd.result.tax.rate); if (rd['tax.rate'] !== undefined) return parseFloat(rd['tax.rate']); if (rd.rate !== undefined) return parseFloat(rd.rate); return fallback; } _extractAmount(rd, fallback) { if (rd.result?.tax?.amount !== undefined) return parseFloat(rd.result.tax.amount); if (rd['tax.amount'] !== undefined) return parseFloat(rd['tax.amount']); if (rd.amount !== undefined) return parseFloat(rd.amount); return fallback; } /** One entry per evaluated rule: applied vs not, with the rate it produced. */ _track(trigger, tax, product, ruleResults, appliedResult, originalRate, adjustedRate) { for (const result of ruleResults || []) { const meta = result && result._rule ? result._rule : {}; const applied = appliedResult != null && result === appliedResult; this.tracking.push({ trigger, rule_id: meta.id || null, rule_name: meta.name || null, tax_id: tax ? tax.id : null, tax_abbreviation: tax ? tax.abbreviation : null, product_id: product ? product.product_id : null, evaluated: true, success: !!(result && result.success), applied, original_rate: originalRate, adjusted_rate: applied ? adjustedRate : null, error: (result && result.error) || null, }); } } } module.exports = RuleTaxApplicator;