Files
Investidor-V2/app/Services/SimulationPublishedFiscal.php
T

153 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\InvestmentLegalSource;
use App\Models\SimulationTaxRuleVersion;
use App\Support\StructuredData;
use Brick\Math\BigRational;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/** Admission freezes eligibility without granting the public consumer governance write access. */
final class SimulationPublishedFiscal
{
/**
* @return array<array-key, mixed>
*/
public function availability(): array
{
$enabled = (bool) config('simulation-fiscal-consumer.enabled');
$eligible = $enabled && $this->boundRule()->where('status', 'published')->exists();
return ['enabled' => $eligible, 'coverage_start' => '2026-01-01', 'coverage_end' => '2026-12-31',
'message' => $eligible ? 'IVA geral sobre base líquida disponível; enquadramento declarado.'
: 'O cálculo de IVA geral sobre base líquida está indisponível para novas execuções. Os históricos mantêm-se acessíveis.'];
}
/**
* @return Builder<SimulationTaxRuleVersion>
*/
private function boundRule(): Builder
{
if (! Str::isUuid((string) config('simulation-fiscal-consumer.rule_id')) || config('simulation-fiscal-consumer.rule_version') < 1) {
return SimulationTaxRuleVersion::query()->whereRaw('false');
}
return SimulationTaxRuleVersion::query()->whereKey(config('simulation-fiscal-consumer.rule_id'))
->where('rule_version', config('simulation-fiscal-consumer.rule_version'))
->where('rule_code', 'IVA-GERAL-BASE-LIQUIDA')->where('component', 'IVA')
->where('scope_key', 'mz.internal-general-net.mzn');
}
/**
* @param array<array-key, mixed> $input
* @return array<array-key, mixed>
*/
public function admit(array $input, bool $missingBase = false): array
{
if (isset($input['monthly_sales'])) {
return $this->admitMonthly($input);
}
// Called inside createPayload, after idempotent replay, never by the worker.
abort_unless(DB::transactionLevel() > 0, 500);
abort_unless(config('simulation-fiscal-consumer.enabled'), 503,
'O cálculo de IVA está temporariamente indisponível. Os dados e históricos foram preservados.');
DB::select('select pg_advisory_xact_lock_shared(261021, hashtext(?))', [(string) config('simulation-fiscal-consumer.rule_id')]);
$rule = $this->boundRule()->first();
abort_unless($rule && $rule->status === 'published', 503,
'A versão autorizada de IVA não está disponível para novas execuções. Os históricos foram preservados.');
abort_if($input['reference_date'] < '2026-01-01' || $input['reference_date'] > '2026-12-31', 422,
'Este cálculo abrange apenas exigibilidade em 2026. As entradas foram preservadas.');
abort_if(collect(StructuredData::collectionItems($rule->rule_definition['nodes'] ?? []))->contains(fn (array $node): bool => ($node['op'] ?? null) === 'round'), 422,
'A versão seleccionada não é compatível com o cálculo exacto deste consumidor.');
DB::select('select pg_advisory_xact_lock_shared(261022, hashtext(?))', [(string) $rule->legal_source_id]);
$source = InvestmentLegalSource::whereKey($rule->legal_source_id)->firstOrFail();
// Publish calculation provenance, not private governance notes or staff identifiers.
$version = array_intersect_key($rule->attributesToArray(), array_flip([
'id', 'rule_code', 'rule_version', 'component', 'scope_key', 'status', 'valid_from', 'valid_until',
'legal_source_id', 'source_version_no', 'source_sha256', 'rule_definition', 'parameters', 'rounding_policy',
]));
$version['source'] = array_intersect_key($source->attributesToArray(), array_flip([
'id', 'title', 'url', 'diploma', 'article', 'competent_entity', 'valid_from', 'valid_until', 'validation_status', 'version_no',
]));
$resolution = app(SimulationTaxParameterResolver::class)->resolvePublishedForPeriod('IVA',
$input['reference_date'], $input['reference_date'], ['currency' => $input['currency'], 'operation' => $input['operation']]);
$rate = collect($resolution)->firstWhere('parameter_code', 'IVA_GENERAL_RATE');
$missing = [];
if ($missingBase) {
$missing[] = 'taxable_sales';
}
$evaluation = null;
if (! ($input['scope_declared'] ?? false)) {
$missing[] = 'operation_scope';
}
if (($rate['status'] ?? null) !== 'resolved' || ($rate['unit'] ?? null) !== 'percent'
|| ($rate['rule_id'] ?? null) !== (string) $rule->id
|| (int) ($rate['rule_version'] ?? 0) !== (int) $rule->rule_version) {
$missing[] = 'eligible_parameter';
}
if ($missing === []) {
$evaluation = app(SimulationTaxRuleExpression::class)->evaluate($rule->rule_definition,
['base_tributavel' => $input['base']], ['iva_general_rate' => $rate['value']]);
}
return app(SimulationFiscalSnapshot::class)->build($version, $input, $resolution, $evaluation, $missing, true);
}
/**
* @param array<array-key, mixed> $input
* @return array<array-key, mixed>
*/
private function admitMonthly(array $input): array
{
$rows = [];
$baseTotal = BigRational::zero();
$vatTotal = BigRational::zero();
$complete = true;
$previous = null;
foreach ($input['monthly_sales'] as $sale) {
abort_unless($previous === null || $sale['date'] === CarbonImmutable::parse($previous)->addMonth()->format('Y-m-d'), 422, 'A projecção deve conter meses consecutivos, sem duplicações.');
$previous = $sale['date'];
$known = $sale['amount'] !== null && $sale['amount'] !== '';
$net = $input['sales_vat_basis'] === 'net';
$month = $this->admit(['base' => $known && $net ? $sale['amount'] : null,
'currency' => $input['currency'], 'reference_date' => $sale['date'],
'operation' => 'internal_general_net', 'scope_declared' => $input['scope_declared']], ! $known || ! $net);
$snapshot ??= $month;
$rate = collect(StructuredData::collectionItems($month['fiscal_provenance']['resolution']))->firstWhere('parameter_code', 'IVA_GENERAL_RATE');
$vat = $month['result']['value'];
$status = ! $known ? 'missing_sales' : (! $net ? 'unsupported_basis' : ($vat === null ? 'indeterminate' : 'calculated'));
$rows[] = ['date' => $sale['date'], 'sales_declared' => $sale['amount'],
'net_sales' => $known && $net ? $sale['amount'] : null,
'rate' => $vat !== null && ($rate['status'] ?? null) === 'resolved' ? $rate['value'] : null,
'vat' => $vat, 'gross_sales' => $vat === null ? null : (string) BigRational::of($sale['amount'])->plus($vat)->toBigDecimal(),
'status' => $status, 'rule_id' => $month['fiscal_provenance']['rule']['id'],
'rule_version' => $month['fiscal_provenance']['rule']['rule_version']];
if ($vat === null) {
$complete = false;
} else {
$baseTotal = $baseTotal->plus($sale['amount']);
$vatTotal = $vatTotal->plus($vat);
}
}
$value = $complete ? (string) $vatTotal->toBigDecimal() : null;
$snapshot['base'] = $complete ? (string) $baseTotal->toBigDecimal() : null;
$snapshot['indicative_total'] = $value;
$snapshot['result']['value'] = $value;
$snapshot['result']['status'] = $complete ? 'calculated' : 'indeterminate';
$snapshot['result']['missing_inputs'] = $complete ? [] : ['monthly_sales_or_net_basis_or_scope'];
$snapshot['result']['explanation'] = 'IVA mensal sobre vendas declaradas sem IVA, pela versão publicada congelada na admissão. Valores com IVA incluído ou sem informação permanecem indeterminados. Não é imposto final a pagar.';
$snapshot['presentation']['value'] = $value;
$snapshot['intermediate'] = null;
$snapshot['monthly_projection'] = ['input' => $input, 'rows' => $rows,
'cash_treatment' => 'Informação separada da tesouraria: os recebimentos declarados não são aumentados pelo IVA nem o IVA é deduzido como custo ou imposto a pagar. Calendário de cobrança e liquidação fiscal não é inferido.'];
return $snapshot;
}
}