softseti-sale-calculator-library
Version:
Sales calculation engine by Softseti
131 lines (115 loc) • 4.6 kB
JavaScript
// 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;