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

82 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\InvestmentLegalSource;
use App\Models\User;
use App\Support\IsolatedQaDatabase;
use Brick\Math\BigRational;
use Illuminate\Support\Facades\DB;
/** Exact operation-level IVA calculation against an explicit QA draft; never a payment/return total. */
final class SimulationFiscalQaPreview
{
public function create(User $actor, string $ruleId, array $input, string $key): array
{
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404);
return DB::transaction(function () use ($actor, $ruleId, $input, $key): array {
$rule = app(SimulationTaxRuleGovernance::class)->show($actor, $ruleId);
abort_unless($rule->status === 'draft' && $rule->rule_code === 'IVA-GERAL-BASE-LIQUIDA', 422,
'Seleccione o rascunho do primeiro lote fiscal.');
abort_if(collect($rule->rule_definition['nodes'] ?? [])->contains(fn (array $node): bool => ($node['op'] ?? null) === 'round'), 422,
'Este percurso preserva o valor exacto e não aplica arredondamento automático.');
$source = InvestmentLegalSource::findOrFail($rule->legal_source_id);
$version = $rule->attributesToArray();
$version['source'] = $source->attributesToArray();
$date = $input['reference_date'];
$context = ['operation' => $input['operation'], 'currency' => $input['currency']];
$missing = [];
$resolution = [];
$evaluation = null;
if ($date < '2026-01-01' || $date > '2026-12-31') {
$missing[] = 'demonstrated_coverage';
} else {
$resolution = app(SimulationTaxParameterResolver::class)->resolveDraftForQa([$version], 'IVA', $date, $date, $context);
$rate = collect($resolution)->firstWhere('parameter_code', 'IVA_GENERAL_RATE');
if (($rate['status'] ?? null) !== 'resolved' || ($rate['unit'] ?? null) !== 'percent') {
$missing[] = 'eligible_parameter';
} else {
$evaluation = app(SimulationTaxRuleExpression::class)->evaluate($rule->rule_definition,
['base_tributavel' => $input['base']], ['iva_general_rate' => $rate['value']]);
}
}
$exact = ($evaluation['status'] ?? null) === 'determinate'
? (string) BigRational::of($evaluation['result'])->toBigDecimal()
: null;
if ($exact === null && $missing === []) {
$missing = $evaluation['missing'] ?? ['eligible_parameter'];
}
$snapshot = [
'contract_version' => 1, 'scenario_kind' => 'fiscal_rule_qa_preview',
'base' => $input['base'], 'currency' => $input['currency'], 'indicative_total' => $exact,
'legal_interpretation' => false, 'formula_version' => 'investmoz.tax.v1',
'context' => [
'reference_date' => $date, 'currency' => $input['currency'], 'currency_fraction_digits' => 2,
'scenario_id' => null, 'scenario_version' => 1, 'engine_version' => 'fiscal-draft-preview-v1',
'ruleset_version' => $rule->rule_code.':'.$rule->rule_version,
],
'result' => [
'status' => $exact === null ? 'indeterminate' : 'calculated', 'value' => $exact, 'unit' => 'MZN',
'explanation' => $exact === null
? 'IVA indeterminado: o contexto, a fonte ou o período não permitem resolver o parâmetro deste lote QA.'
: 'IVA calculado com precisão decimal exacta, sem arredondamento. Ensaio QA de regra em rascunho; enquadramento declarado, não validado. Não é saldo de declaração nem valor a pagar.',
'missing_inputs' => $missing, 'source_ids' => [(string) $source->id],
],
'presentation' => ['value' => $exact, 'currency' => $input['currency'], 'rounding_applied' => false, 'purpose' => 'operation_tax_not_payment'],
// Exact rational steps supplement the decimal result; no float conversion.
'intermediate' => $evaluation,
'fiscal_provenance' => [
'mode' => 'qa_draft_only', 'scope_status' => 'user_declared', 'context' => $context,
'rounding_policy' => $rule->rounding_policy, 'resolution' => $resolution,
'rule' => $version, 'source_acceptance' => 'Fonte oficial aceite pela decisão Tempo de 08/10/2026; não constitui aprovação jurídica desta operação ou publicação da regra.',
],
];
return app(SimulationExecution::class)->createFiscalQaPreview($snapshot, $actor->id, $key);
});
}
}