diff --git a/app/Domains/Routing/Services/SmartRoutingEngine.php b/app/Domains/Routing/Services/SmartRoutingEngine.php new file mode 100644 index 0000000..5929fb1 --- /dev/null +++ b/app/Domains/Routing/Services/SmartRoutingEngine.php @@ -0,0 +1,60 @@ +sortBy('priority'); + + foreach ($rules as $rule) { + if ($this->ruleMatches($rule, $context)) { + return $rule['target']; + } + } + + return $config['fallback']; + } + + private function ruleMatches(array $rule, array $context): bool + { + $conditions = $rule['conditions'] ?? []; + $matchMode = $rule['match'] ?? 'all'; + + if (empty($conditions)) { + return false; + } + + $results = array_map( + fn ($condition) => $this->evaluateCondition($condition, $context), + $conditions + ); + + return $matchMode === 'all' + ? ! in_array(false, $results, true) + : in_array(true, $results, true); + } + + private function evaluateCondition(array $condition, array $context): bool + { + $field = $condition['field']; + $op = $condition['op']; + $expected = $condition['value']; + $actual = $context[$field] ?? null; + + return match ($op) { + 'equals' => $actual === $expected, + 'not_equals' => $actual !== $expected, + 'in' => in_array($actual, (array) $expected, true), + 'not_in' => ! in_array($actual, (array) $expected, true), + 'contains' => str_contains((string) $actual, (string) $expected), + 'starts_with' => str_starts_with((string) $actual, (string) $expected), + 'between' => $actual >= $expected[0] && $actual <= $expected[1], + 'greater_than' => $actual > $expected, + 'less_than' => $actual < $expected, + default => false, + }; + } +} diff --git a/tests/Unit/Routing/SmartRoutingEngineTest.php b/tests/Unit/Routing/SmartRoutingEngineTest.php new file mode 100644 index 0000000..6218e3a --- /dev/null +++ b/tests/Unit/Routing/SmartRoutingEngineTest.php @@ -0,0 +1,76 @@ + [], + 'fallback' => 'https://fallback.com', + ]; + $ctx = ['country' => 'US', 'device' => 'desktop']; + + expect($engine->resolve($rules, $ctx))->toBe('https://fallback.com'); +}); + +it('matches device rule', function () { + $engine = new SmartRoutingEngine; + $rules = [ + 'rules' => [ + [ + 'id' => 'r1', 'priority' => 1, + 'conditions' => [['field' => 'device', 'op' => 'in', 'value' => ['mobile']]], + 'match' => 'all', + 'target' => 'https://mobile.com', + ], + ], + 'fallback' => 'https://fallback.com', + ]; + + expect($engine->resolve($rules, ['device' => 'mobile']))->toBe('https://mobile.com'); + expect($engine->resolve($rules, ['device' => 'desktop']))->toBe('https://fallback.com'); +}); + +it('matches country IN condition', function () { + $engine = new SmartRoutingEngine; + $rules = [ + 'rules' => [ + [ + 'id' => 'r1', 'priority' => 1, + 'conditions' => [['field' => 'country', 'op' => 'in', 'value' => ['DE', 'AT', 'CH']]], + 'match' => 'all', + 'target' => 'https://dach.com', + ], + ], + 'fallback' => 'https://fallback.com', + ]; + + expect($engine->resolve($rules, ['country' => 'DE']))->toBe('https://dach.com'); + expect($engine->resolve($rules, ['country' => 'US']))->toBe('https://fallback.com'); +}); + +it('matches ANY condition with mixed fields', function () { + $engine = new SmartRoutingEngine; + $rules = [ + 'rules' => [ + [ + 'id' => 'r1', 'priority' => 1, + 'conditions' => [ + ['field' => 'country', 'op' => 'in', 'value' => ['DE']], + ['field' => 'device', 'op' => 'in', 'value' => ['mobile']], + ], + 'match' => 'any', + 'target' => 'https://any.com', + ], + ], + 'fallback' => 'https://fallback.com', + ]; + + // Either DE or mobile matches + expect($engine->resolve($rules, ['country' => 'DE', 'device' => 'desktop']))->toBe('https://any.com'); + expect($engine->resolve($rules, ['country' => 'US', 'device' => 'mobile']))->toBe('https://any.com'); + // Neither matches + expect($engine->resolve($rules, ['country' => 'US', 'device' => 'desktop']))->toBe('https://fallback.com'); +});