153 lines
7.4 KiB
PHP
153 lines
7.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Services\SimulationTaxParameterResolver;
|
|
use InvalidArgumentException;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class SimulationTaxParameterResolverTest extends TestCase
|
|
{
|
|
#[Test]
|
|
public function synthetic_qa_versions_keep_historical_values_and_expose_a_period_transition_without_activating_rules(): void
|
|
{
|
|
$before = $this->publicVersion('2026-01-01', '2026-06-30', '7.5', 1);
|
|
$after = $this->publicVersion('2026-07-01', null, '9.25', 2);
|
|
$before['rule_code'] = $after['rule_code'] = 'QA.SYNTHETIC.NOT.LEGISLATION';
|
|
$before['source']['title'] = $after['source']['title'] = 'SINTETICO QA — nao e legislacao';
|
|
$before['source']['diploma'] = $after['source']['diploma'] = 'QA-FIXTURE-ONLY';
|
|
$resolver = new SimulationTaxParameterResolver;
|
|
$historical = $resolver->resolveAvailableParameters([$before], 'IVA', '2026-06-01', '2026-06-30', ['operation' => 'domestic']);
|
|
$frozen = json_encode($historical, JSON_THROW_ON_ERROR);
|
|
$current = $resolver->resolveAvailableParameters([$before, $after], 'IVA', '2026-07-01', '2026-07-31', ['operation' => 'domestic']);
|
|
self::assertSame('7.5', $historical[0]['value']);
|
|
self::assertSame('9.25', $current[0]['value']);
|
|
self::assertSame(2, $current[0]['rule_version']);
|
|
self::assertSame('indeterminate_period_change', $resolver->resolveAvailableParameters([$before, $after], 'IVA', '2026-01-01', '2026-12-31', ['operation' => 'domestic'])[0]['status']);
|
|
self::assertSame($frozen, json_encode($historical, JSON_THROW_ON_ERROR));
|
|
self::assertSame($historical, $resolver->resolveAvailableParameters([$before, $after], 'IVA', '2026-06-01', '2026-06-30', ['operation' => 'domestic']));
|
|
$after['source']['version_no'] = 2;
|
|
self::assertSame([], $resolver->resolveAvailableParameters([$after], 'IVA', '2026-07-01', '2026-07-31', ['operation' => 'domestic']));
|
|
}
|
|
|
|
#[Test]
|
|
public function it_selects_the_published_version_effective_on_the_requested_date_and_returns_its_source(): void
|
|
{
|
|
$old = $this->version('2025-01-01', '2025-12-31', '15', 1);
|
|
$current = $this->version('2026-01-01', null, '16', 2);
|
|
$resolver = new SimulationTaxParameterResolver;
|
|
|
|
self::assertSame('15', $resolver->resolve([$old, $current], 'IVA', 'scope:test', '2025-12-31', 'VAT_RATE')['value']);
|
|
$resolved = $resolver->resolve([$old, $current], 'IVA', 'scope:test', '2026-01-01', 'VAT_RATE');
|
|
self::assertSame('resolved', $resolved['status']);
|
|
self::assertSame('16', $resolved['value']);
|
|
self::assertSame('LAW-2026', $resolved['source']['diploma']);
|
|
self::assertSame(2, $resolved['rule_version']);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_returns_indeterminate_for_missing_or_overlapping_candidates(): void
|
|
{
|
|
$version = $this->version('2026-01-01', null, '16', 1);
|
|
$resolver = new SimulationTaxParameterResolver;
|
|
|
|
self::assertSame('indeterminate_missing', $resolver->resolve([], 'IVA', 'scope:test', '2026-07-01', 'VAT_RATE')['status']);
|
|
self::assertSame('indeterminate_missing', $resolver->resolve([$version], 'IVA', 'different-scope', '2026-07-01', 'VAT_RATE')['status']);
|
|
self::assertSame('indeterminate_missing', $resolver->resolve([$version], 'IVA', 'scope:test', '2025-12-31', 'VAT_RATE')['status']);
|
|
self::assertSame('indeterminate_overlap', $resolver->resolve([$version, $version], 'IVA', 'scope:test', '2026-07-01', 'VAT_RATE')['status']);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_requires_exactly_matching_conditions_and_a_still_validated_source_version(): void
|
|
{
|
|
$version = $this->version('2026-01-01', null, '16', 1);
|
|
$version['parameters']['VAT_RATE']['conditions'] = ['operation' => 'domestic'];
|
|
$resolver = new SimulationTaxParameterResolver;
|
|
|
|
self::assertSame('indeterminate_missing', $resolver->resolve([$version], 'IVA', 'scope:test', '2026-03-01', 'VAT_RATE', ['operation' => 'import'])['status']);
|
|
$version['source_version_no'] = 2;
|
|
self::assertSame('indeterminate_missing', $resolver->resolve([$version], 'IVA', 'scope:test', '2026-03-01', 'VAT_RATE', ['operation' => 'domestic'])['status']);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_rejects_negative_values_and_percentages_over_one_hundred(): void
|
|
{
|
|
$resolver = new SimulationTaxParameterResolver;
|
|
foreach (['-0.01' => 'mzn', '100.0001' => 'percent'] as $value => $unit) {
|
|
try {
|
|
$resolver->validateParameters(['TEST_VALUE' => ['value' => $value, 'unit' => $unit, 'conditions' => []]]);
|
|
self::fail('Invalid parameter value was accepted.');
|
|
} catch (InvalidArgumentException) {
|
|
self::assertTrue(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function it_does_not_select_a_parameter_when_its_validity_changes_inside_the_requested_period(): void
|
|
{
|
|
$before = $this->publicVersion('2026-01-01', '2026-06-30', '16', 1);
|
|
$after = $this->publicVersion('2026-07-01', null, '17', 2);
|
|
$resolver = new SimulationTaxParameterResolver;
|
|
|
|
$result = $resolver->resolveAvailableParameters([$before, $after], 'IVA', '2026-01-01', '2026-12-31', ['operation' => 'domestic']);
|
|
|
|
self::assertSame('indeterminate_period_change', $result[0]['status']);
|
|
self::assertNull($result[0]['value']);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_resolves_a_single_parameter_only_for_an_exactly_matching_context(): void
|
|
{
|
|
$version = $this->publicVersion('2026-01-01', null, '16', 1);
|
|
$resolver = new SimulationTaxParameterResolver;
|
|
|
|
$result = $resolver->resolveAvailableParameters([$version], 'IVA', '2026-01-01', '2026-12-31', ['operation' => 'domestic']);
|
|
|
|
self::assertSame('resolved', $result[0]['status']);
|
|
self::assertSame('16', $result[0]['value']);
|
|
self::assertSame('Synthetic source fixture', $result[0]['source']['title']);
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function version(string $from, ?string $until, string $value, int $version): array
|
|
{
|
|
return [
|
|
'id' => 'rule-'.$version,
|
|
'rule_code' => 'IVA.VAT.RATE',
|
|
'rule_version' => $version,
|
|
'component' => 'IVA',
|
|
'scope_key' => 'scope:test',
|
|
'status' => 'published',
|
|
'valid_from' => $from,
|
|
'valid_until' => $until,
|
|
'parameters' => ['VAT_RATE' => ['value' => $value, 'unit' => 'percent', 'conditions' => []]],
|
|
'source_version_no' => 1,
|
|
'source_sha256' => str_repeat('a', 64),
|
|
'source' => [
|
|
'validation_status' => 'validated',
|
|
'version_no' => 1,
|
|
'valid_from' => '2020-01-01',
|
|
'valid_until' => null,
|
|
'title' => 'Synthetic source fixture',
|
|
'diploma' => 'LAW-2026',
|
|
'article' => 'Article fixture',
|
|
'url' => 'https://example.invalid/source',
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function publicVersion(string $from, ?string $until, string $value, int $version): array
|
|
{
|
|
$record = $this->version($from, $until, $value, $version);
|
|
$record['rule_definition'] = ['language' => 'investmoz.tax.v1'];
|
|
$record['parameters']['VAT_RATE']['conditions'] = ['operation' => 'domestic'];
|
|
|
|
return $record;
|
|
}
|
|
}
|