<?php
 namespace App\Rules; use App\Services\Billing\InvoicingRuleEvaluator; use Closure; use Illuminate\Contracts\Validation\ValidationRule; class ValidConditionTree implements ValidationRule { private const MAX_DEPTH = 5; public function validate(string $attribute, mixed $value, Closure $fail): void { if (! is_array($value)) { $fail('Conditions must be a structured object.'); return; } $error = $this->walk($value, 0); if ($error !== null) { $fail($error); } } private function walk(array $node, int $depth): ?string { if ($depth > self::MAX_DEPTH) { return 'Conditions tree exceeds maximum depth ('.self::MAX_DEPTH.').'; } $type = $node['type'] ?? null; if (! in_array($type, ['group', 'condition'], true)) { return "Each node must have type 'group' or 'condition'."; } if ($type === 'group') { $op = $node['operator'] ?? null; if (! in_array($op, ['AND', 'OR'], true)) { return "Group operator must be 'AND' or 'OR'."; } $children = $node['children'] ?? null; if (! is_array($children)) { return 'Group must have children array.'; } foreach ($children as $child) { if (! is_array($child)) { return 'Group children must be node objects.'; } $err = $this->walk($child, $depth + 1); if ($err !== null) { return $err; } } return null; } $field = $node['field'] ?? null; if (! in_array($field, InvoicingRuleEvaluator::ALLOWED_FIELDS, true)) { return "Unknown condition field: {$field}"; } $op = $node['op'] ?? null; if (! in_array($op, InvoicingRuleEvaluator::ALLOWED_OPERATORS, true)) { return "Unsupported operator: {$op}"; } if (! array_key_exists('value', $node)) { return 'Condition must have a value key.'; } return null; } } 