softseti-sale-calculator-library
Version:
Sales calculation engine by Softseti
114 lines (99 loc) • 3.61 kB
JavaScript
// src/rules/RuleEngine.js
// Resolve a dotted path ("sale.dwSaleProducts") against an object.
function resolvePath(obj, path) {
return String(path).split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
}
class RuleEngine {
constructor(zenEngine) {
this.engine = zenEngine ? new zenEngine() : null;
this.ruleCache = new Map();
}
/**
* Evaluates a business rule with given context
* @param {Object} ruleContent - GoRules JSON rule content
* @param {Object} context - Execution context
* @returns {Promise<Object>} Rule evaluation result
*/
async evaluateRule(ruleContent, context) {
try {
// Verificar si el motor está disponible
if (!this.engine) {
throw new Error('Rule engine not initialized. ZenEngine dependency not provided.');
}
const decision = this.engine.createDecision(ruleContent);
const result = await decision.safeEvaluate(context);
return result;
} catch (error) {
console.error('Rule evaluation error:', error);
throw new Error(`Rule evaluation failed: ${error.message}`);
}
}
/**
* Executes multiple rules for a trigger event
* @param {Array} rules - Business rules to execute
* @param {Object} context - Execution context
* @returns {Promise<Array>} Array of rule execution results
*/
async executeRules(rules, context) {
if (!this.engine || !rules || rules.length === 0) {
return [];
}
const results = [];
for (const rule of rules) {
// Tag every result with its source rule so the caller can build the
// rule_tracking (which rule was evaluated / applied / skipped).
const meta = { id: rule.id, name: rule.name };
// Loop mode (opt-in): when the rule declares { loop: { over, as } } and
// that path resolves to an array in the context, evaluate the decision
// once per element, exposing the element under its alias. Rules without a
// loop behave exactly as before.
const loop = rule.loop;
const list = loop && loop.over ? resolvePath(context, loop.over) : null;
if (loop && loop.over && Array.isArray(list)) {
const alias = loop.as || 'item';
for (let i = 0; i < list.length; i++) {
const itemContext = Object.assign({}, context, { [alias]: list[i] });
try {
const result = await this.evaluateRule(rule.rule_content, itemContext);
result._rule = Object.assign({}, meta, { loop_index: i });
results.push(result);
} catch (error) {
results.push({
ruleId: rule.id,
_rule: Object.assign({}, meta, { loop_index: i }),
success: false,
error: error.message
});
}
}
continue;
}
try {
const result = await this.evaluateRule(rule.rule_content, context);
result._rule = meta;
results.push(result);
} catch (error) {
results.push({
ruleId: rule.id,
_rule: meta,
success: false,
error: error.message
});
}
}
return results;
}
/**
* Validates rule structure
* @param {Object} ruleContent - Rule content to validate
* @returns {boolean} True if rule is valid
*/
validateRule(ruleContent) {
if (!ruleContent || typeof ruleContent !== 'object') {
return false;
}
const requiredProperties = ['nodes', 'edges', 'contentType'];
return requiredProperties.every(prop => ruleContent.hasOwnProperty(prop));
}
}
module.exports = RuleEngine;