feat(routing): smart routing engine with condition evaluator (device, country, UTM)
parent
86f767a626
commit
e06db03320
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Domains\Routing\Services;
|
||||
|
||||
class SmartRoutingEngine
|
||||
{
|
||||
public function resolve(array $config, array $context): string
|
||||
{
|
||||
$rules = collect($config['rules'] ?? [])
|
||||
->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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
use App\Domains\Routing\Services\SmartRoutingEngine;
|
||||
|
||||
uses(Tests\TestCase::class);
|
||||
|
||||
it('returns fallback when no rules match', function () {
|
||||
$engine = new SmartRoutingEngine;
|
||||
$rules = [
|
||||
'rules' => [],
|
||||
'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');
|
||||
});
|
||||
Loading…
Reference in New Issue