Nexus Validation Engine (motor dinámico de validaciones)
6.1 Responsabilidad
El Nexus Validation Engine valida una solicitud canónica en base a reglas configurables por partnerId, companyId, sourceSystem, documentType, con versionado, prioridad y activación/desactivación. Es síncrono y corre en proceso con la Purchase Order API.
6.2 Entradas / Salidas
- Entradas:
(CanonicalPurchaseOrder canonical, PartnerContext partnerContext) - Salidas:
ValidationResult { valid, errors[{ruleCode, field, message, params, severity}], metadata[ruleCount, durationMs, evaluatedVersions[]] }
6.3 Comparativa de estrategias
| Estrategia | Ventajas | Inconvenientes |
|---|---|---|
| Specification Pattern (Java) | Fuerte tipado, performance, testable | Menos configurable por negocio |
| Strategy Pattern | Reglas por clase Spring Bean | Cada regla nueva = nuevo deploy |
| Chain of Responsibility | Ordenamiento fácil | Acoplamiento por código |
| Rules Engine (Drools) | Potente | Overkill, curva alta, riesgo ejecución arbitraria |
| Expression Language (SpEL/OGNL) | Flexibilidad alta | Permite ejecución arbitraria → riesgo seguridad |
| Nexus Rule DSL + PostgreSQL config | Expresivo, seguro, parametrizable, versionable | Toca implementar parser/evaluador |
6.3.1 Estrategia recomendada
Decisión
Nexus Rule DSL (subset declarativo, sin loops ni invocaciones arbitrarias) + Specification/Strategy para reglas que requieren lookup externo (catálogos) almacenadas en PostgreSQL con validation_rule + validation_rule_version.
- En código: funciones puras predefinidas (
required,minLength,regex,lookupSupplier,currencyAllowed,amountRange) + operadores booleanos (&&,||,!) + literales string/number/boolean + path JSON ($.header.currency.code). - En configuración PostgreSQL: la expresión, prioridad, alcance (partnerId, companyId, sourceSystem, documentType[]), versión y severidad.
- NO permitir ejecución de código arbitrario (no EL genérico). Parser en Java (ANTLR o parser de expresiones pequeño y a medida con gramática acotada).
6.3.2 Flujo interno del motor
6.4 Estructura de una validación almacenada (PostgreSQL)
- validation_rule:
id, code, partnerId, companyId, sourceSystem, documentTypes[], groups[], priority, status, createdBy, createdAt, updatedAt. - validation_rule_version:
id, rule_id FK, major, minor, status, expression, errorTemplate JSONB, params JSONB, audit, publishedBy, publishedAt.
6.5 Diseño por capas (Arquitectura Base Backend Alpura)
Alineado a la nueva arquitectura interna Nexus, el Validation Engine se divide en las siguientes capas y clases:
| Capa | Clases / Interfaces | Métodos (nomenclatura Alpura) |
|---|---|---|
| WEB (Config API, para administrar reglas) | ValidationRuleController | createValidationRule(), updateRuleVersion(), changeRuleStatus(), searchRules(), dryRunValidation() |
| Facade (Service Interfaces) | ValidationFacade (interface) | validatePurchaseOrder(), dryRun(), getRuleById() |
| Service (implementaciones) | ValidationEngineServiceImpl, RuleCacheService, RuleDslEvaluator | Públicos: validatePurchaseOrder(), reloadCache(), getRuleById(), dryRun()Privados (prefijo _): _loadActiveRulesByScope(), _sortByGroupPriority(), _prepareRuleParameters(), _evaluateRuleVersion(), _shortCircuitBySeverity(), _buildValidationResult(), _persistValidationResult() |
| Persistence | ValidationRuleRepository, ValidationRuleVersionRepository, ValidationResultRepository | Públicos: findActiveRulesByScope(), saveVersion(), findRuleById(), saveResult()Privados (en implementación): _buildScopeQuery(), _mapToEntity(), _mapToDto() |
| Commons | commons.valueobjects (RuleCode, ValidationScope, RuleExpression, ValidationSeverity) commons.exceptions (ValidationRuleNotFoundException, InvalidRuleDslException) commons.constants (ValidationGroups, ValidationStages) |
6.5.1 Interfaces y firmas (ejemplo)
// Facade (Service Interface) - capa Facade
public interface ValidationFacade {
ValidationResult validatePurchaseOrder(CanonicalPurchaseOrder canonical, ValidationScope scope);
ValidationResult dryRun(CanonicalPurchaseOrder canonical, UUID ruleVersionId);
ValidationRuleView getRuleById(UUID ruleId);
}
// Service - publicos sin prefijo; internos con _
@Service
public class ValidationEngineServiceImpl implements ValidationFacade {
@Override
public ValidationResult validatePurchaseOrder(CanonicalPurchaseOrder canonical, ValidationScope scope) {
List<ValidationRuleVersion> rules = _loadActiveRulesByScope(scope);
_sortByGroupPriority(rules);
ValidationExecutionContext ctx = _prepareRuleParameters(canonical, scope);
for (ValidationRuleVersion rule : rules) {
_evaluateRuleVersion(rule, ctx);
if (_shortCircuitBySeverity(ctx, rule.severity())) break;
}
ValidationResult result = _buildValidationResult(ctx, rules.size());
_persistValidationResult(scope, canonical.requestId(), result);
return result;
}
// ... otros publics: dryRun, getRuleById ...
private List<ValidationRuleVersion> _loadActiveRulesByScope(ValidationScope scope) { /* ... */ }
private void _sortByGroupPriority(List<ValidationRuleVersion> rules) { /* ... */ }
private ValidationExecutionContext _prepareRuleParameters(CanonicalPurchaseOrder po, ValidationScope scope) { /* ... */ }
private void _evaluateRuleVersion(ValidationRuleVersion rule, ValidationExecutionContext ctx) { /* ... */ }
private boolean _shortCircuitBySeverity(ValidationExecutionContext ctx, ValidationSeverity s) { /* ... */ }
private ValidationResult _buildValidationResult(ValidationExecutionContext ctx, int ruleCount) { /* ... */ }
private void _persistValidationResult(ValidationScope scope, UUID requestId, ValidationResult r) { /* ... */ }
}
// Persistence (Repository Spring Data)
public interface ValidationRuleVersionRepository extends JpaRepository<ValidationRuleVersion, UUID> {
List<ValidationRuleVersion> findActiveByScope(ValidationScope scope);
}
6.6 Evaluación y comparación final
| Tipo de regla | Implementación |
|---|---|
| Estructurales | OpenAPI + Jakarta validation (REST controller) + reglas internas para arrays/mapas |
| Sintácticas | Built-in functions pattern, email, dateFormat, currencyISO |
| Semánticas | Lookups por catálogos (DB, Redis cache) por partner |
| Negocio | Combinación de funciones, expresiones y prioridades |
6.6 Resultado (Response ValidationResult)
{
"valid": false,
"ruleCount": 23,
"durationMs": 42,
"errors": [
{
"ruleCode": "SUPPLIER_REQUIRED",
"severity": "ERROR",
"field": "supplier.code",
"message": "El proveedor es obligatorio",
"params": {}
}
],
"warnings": []
}
6.7 Consideraciones
- Cache: reglas activas (por partner/docType) en caché local (Caffeine) TTL configurable; invalidación por API PATCH status (o webhook event).
- Testing de reglas: antes de publicar una nueva versión, API de dry-run para ejecutar la regla sobre una solicitud canónica de prueba.
- No mezclar: lógica de procesamiento de creación de OC dentro de reglas; estas validan y nada más.
- Observabilidad: histograma
nexus_validation_duration_seconds+nexus_validation_failed_total{ruleCode, partnerId}.