fix: recover simulator cashflow and verify QA continuity and report assumptions

This commit is contained in:
2026-10-20 22:00:48 +02:00
parent a690c665fb
commit c22003a3da
20 changed files with 773 additions and 95 deletions
+14 -14
View File
@@ -95,16 +95,16 @@ final class SimulationTaxRuleExpression
private function topologicalOrder(array $definition): array
{
if (($definition['language'] ?? null) !== self::LANGUAGE || ! is_array($definition['nodes'] ?? null) || ! is_string($definition['result'] ?? null)) {
throw new InvalidArgumentException('Unsupported expression definition.');
throw new InvalidArgumentException('O formato da expressão não é suportado.');
}
$nodes = $definition['nodes'];
if ($nodes === [] || count($nodes) > 100) {
throw new InvalidArgumentException('Expression must contain between 1 and 100 nodes.');
throw new InvalidArgumentException('A expressão deve conter entre 1 e 100 elementos.');
}
foreach ($nodes as $id => $node) {
if (! is_string($id) || ! preg_match('/^[a-z][a-z0-9_]{0,47}$/', $id) || ! is_array($node) || ! is_string($node['op'] ?? null)) {
throw new InvalidArgumentException('Invalid expression node.');
throw new InvalidArgumentException('Reveja o elemento indicado na expressão.');
}
$op = $node['op'];
$allowedFields = match ($op) {
@@ -112,40 +112,40 @@ final class SimulationTaxRuleExpression
'literal' => ['op', 'value'],
'add', 'subtract', 'multiply', 'divide', 'min', 'max' => ['op', 'args'],
'round' => ['op', 'args', 'scale', 'mode'],
default => throw new InvalidArgumentException('Expression operation is not allowed.'),
default => throw new InvalidArgumentException('Esta operação não é suportada na expressão.'),
};
if (array_diff(array_keys($node), $allowedFields) !== [] || array_diff($allowedFields, array_keys($node)) !== []) {
throw new InvalidArgumentException('Expression node has missing or unsupported fields.');
throw new InvalidArgumentException('Há campos em falta ou não suportados num elemento da expressão.');
}
if (in_array($op, ['input', 'parameter'], true) && (! is_string($node['key']) || ! preg_match('/^[a-z][a-z0-9_.-]{0,79}$/', $node['key']))) {
throw new InvalidArgumentException('Invalid input or parameter key.');
throw new InvalidArgumentException('Reveja o identificador da entrada ou do parâmetro.');
}
if ($op === 'literal') {
$this->number($node['value']);
}
if (isset($node['args'])) {
if (! is_array($node['args'])) {
throw new InvalidArgumentException('Invalid expression arguments.');
throw new InvalidArgumentException('Reveja os argumentos da expressão.');
}
$count = count($node['args']);
$validCount = $op === 'round' ? $count === 1 : ($op === 'subtract' || $op === 'divide' ? $count === 2 : $count >= 2 && $count <= 20);
if (! $validCount || count(array_filter($node['args'], 'is_string')) !== $count) {
throw new InvalidArgumentException('Invalid expression arguments.');
throw new InvalidArgumentException('Reveja os argumentos da expressão.');
}
}
if ($op === 'round' && (! is_int($node['scale']) || $node['scale'] < 0 || $node['scale'] > 12 || ! isset(self::ROUNDING[$node['mode']]))) {
throw new InvalidArgumentException('Invalid explicit rounding configuration.');
throw new InvalidArgumentException('Reveja a configuração de arredondamento.');
}
}
if (! array_key_exists($definition['result'], $nodes)) {
throw new InvalidArgumentException('Expression result references an unknown node.');
throw new InvalidArgumentException('O resultado da expressão refere um elemento inexistente.');
}
$state = [];
$order = [];
$visit = function (string $id) use (&$visit, &$state, &$order, $nodes): void {
if (($state[$id] ?? 0) === 1) {
throw new InvalidArgumentException('Expression dependency graph contains a cycle.');
throw new InvalidArgumentException('A expressão contém uma dependência circular.');
}
if (($state[$id] ?? 0) === 2) {
return;
@@ -153,7 +153,7 @@ final class SimulationTaxRuleExpression
$state[$id] = 1;
foreach ($nodes[$id]['args'] ?? [] as $dependency) {
if (! is_string($dependency) || ! array_key_exists($dependency, $nodes)) {
throw new InvalidArgumentException('Expression references an unknown node.');
throw new InvalidArgumentException('A expressão refere um elemento inexistente.');
}
$visit($dependency);
}
@@ -191,13 +191,13 @@ final class SimulationTaxRuleExpression
if ($op === 'min' || $op === 'max') {
return array_reduce(array_slice($args, 1), static fn (BigRational $carry, BigRational $value): BigRational => ($op === 'min' ? $carry->compareTo($value) <= 0 : $carry->compareTo($value) >= 0) ? $carry : $value, $args[0]);
}
throw new InvalidArgumentException('Expression operation is not allowed.');
throw new InvalidArgumentException('Esta operação não é suportada na expressão.');
}
private function number(mixed $value): BigRational
{
if ((! is_string($value) && ! is_int($value)) || ! preg_match('/^-?\d+(?:\.\d+)?$/', (string) $value)) {
throw new InvalidArgumentException('Numbers must be plain decimal strings or integers.');
throw new InvalidArgumentException('Introduza valores numéricos decimais ou inteiros.');
}
return BigRational::of((string) $value);