Files
Investidor-V2/app/Jobs/RunSimulation.php
T
nala e65cb26901
tests / ci (push) Canceled after 0s
varias alteracoes
2026-10-06 14:50:12 +02:00

129 lines
7.4 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\Simulation;
use App\Services\SimulationExecution;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class RunSimulation implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 30;
public function __construct(public string $simulationId, public ?int $actorId, public ?string $tokenHash) {}
/** @return list<int> */
public function backoff(): array
{
return [10, 60];
}
public function handle(SimulationExecution $execution): void
{
$shouldRun = DB::transaction(function () use ($execution): bool {
$execution->setContext($this->actorId, $this->tokenHash);
$job = DB::table('simulation_jobs')->where('simulation_id', $this->simulationId)->where('job_type', 'execute')->lockForUpdate()->first();
if (! $job || in_array($job->status, ['succeeded', 'failed'], true)) {
return false;
}
DB::table('simulation_jobs')->where('id', $job->id)->update([
'status' => 'running', 'attempts' => min(3, max((int) $job->attempts + 1, (int) $this->attempts())),
'started_at' => now(), 'updated_at' => now(),
]);
return true;
});
if (! $shouldRun) {
return;
}
DB::transaction(function () use ($execution): void {
$execution->setContext($this->actorId, $this->tokenHash);
$simulation = Simulation::whereNull('deleted_at')->lockForUpdate()->find($this->simulationId);
if (! $simulation || $simulation->status === 'completed') {
return;
}
$job = DB::table('simulation_jobs')->where('simulation_id', $this->simulationId)->where('job_type', 'execute')->lockForUpdate()->first();
if (! $job || in_array($job->status, ['succeeded', 'failed'], true)) {
return;
}
$scenario = DB::table('simulation_scenarios')->where('simulation_id', $simulation->id)->orderBy('code')->first();
if (! $scenario) {
throw new \RuntimeException('Simulation has no scenario.');
}
$input = json_decode($scenario->parameters, true, flags: JSON_THROW_ON_ERROR);
$row = DB::selectOne('select (CAST(? AS numeric) * CAST(? AS numeric) / 100 * (1 - CAST(? AS numeric) / 100))::text as value', [$input['base'], $input['rate'], $input['reduction']]);
$resultId = (string) Str::uuid();
$engineVersion = 'portal-arithmetic-v1+simulation-v'.$simulation->version_no;
$result = ['base' => $input['base'], 'rate_percent' => $input['rate'], 'reduction_percent' => $input['reduction'], 'scenario_kind' => 'custom_planning', 'indicative_total' => $row->value,
'currency' => null, 'legal_interpretation' => false, 'formula_version' => 'portal-arithmetic-v1',
'contract_version' => 1,
'context' => [
'reference_date' => substr((string) $scenario->created_at, 0, 10),
'currency' => null,
'currency_fraction_digits' => null,
'scenario_id' => (string) $scenario->id,
'scenario_version' => (int) $scenario->version_no,
'engine_version' => $engineVersion,
'ruleset_version' => null,
],
'result' => [
'status' => 'calculated',
'value' => (string) $row->value,
'unit' => null,
'explanation' => 'Resultado aritmético determinístico; não constitui cálculo nem conclusão fiscal.',
'missing_inputs' => [],
'source_ids' => [],
]];
$json = json_encode($result, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
$canonicalResult = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$json])->canonical;
$resultHash = hash('sha256', $canonicalResult);
DB::table('simulation_results')->insert([
'id' => $resultId, 'simulation_id' => $simulation->id, 'scenario_id' => $scenario->id, 'engine_version' => $engineVersion,
'result_snapshot' => $json, 'result_sha256' => $resultHash, 'created_at' => now(),
]);
$simulation->update(['status' => 'completed', 'version_no' => $simulation->version_no + 1, 'result_snapshot' => $result, 'error_code' => null]);
DB::table('simulation_jobs')->where('id', $job->id)->update(['status' => 'succeeded', 'finished_at' => now(), 'updated_at' => now()]);
DB::table('simulation_reports')->insert(['id' => (string) Str::uuid(), 'simulation_id' => $simulation->id, 'version_no' => $scenario->version_no,
'status' => 'ready', 'report_snapshot' => $json, 'report_sha256' => $resultHash, 'created_at' => now(), 'updated_at' => now()]);
$sourceJson = json_encode($input, JSON_THROW_ON_ERROR);
$canonicalSource = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$sourceJson])->canonical;
DB::table('simulation_result_sources')->insertOrIgnore(['id' => (string) Str::uuid(), 'result_id' => $resultId,
'source_type' => 'user_input', 'source_id' => $scenario->id, 'source_version' => (string) $scenario->version_no,
'source_snapshot' => $sourceJson, 'source_sha256' => hash('sha256', $canonicalSource), 'created_at' => now()]);
DB::table('simulation_audit_events')->insert(['simulation_id' => $simulation->id, 'actor_user_id' => $simulation->owner_user_id,
'action' => 'simulation.completed', 'correlation_id' => $simulation->id, 'data' => json_encode(['result_sha256' => hash('sha256', $json), 'attempt' => $job->attempts + 1], JSON_THROW_ON_ERROR), 'occurred_at' => now()]);
}, 3);
}
public function failed(\Throwable $exception): void
{
DB::transaction(function (): void {
app(SimulationExecution::class)->setContext($this->actorId, $this->tokenHash);
$job = DB::table('simulation_jobs')->where('simulation_id', $this->simulationId)->where('job_type', 'execute')->lockForUpdate()->first();
if (! $job || $job->status === 'succeeded') {
return;
}
DB::table('simulation_jobs')->where('id', $job->id)->update(['status' => 'failed', 'attempts' => 3, 'finished_at' => now(), 'error_code' => 'execution_failed', 'updated_at' => now()]);
$simulation = Simulation::where('id', $this->simulationId)->lockForUpdate()->first();
if ($simulation) {
if ($simulation->status !== 'failed') {
$simulation->update(['status' => 'failed', 'version_no' => $simulation->version_no + 1, 'error_code' => 'execution_failed']);
}
DB::table('simulation_audit_events')->insert(['simulation_id' => $simulation->id, 'actor_user_id' => $this->actorId,
'action' => 'simulation.failed', 'correlation_id' => $simulation->id, 'data' => json_encode(['error_code' => 'execution_failed'], JSON_THROW_ON_ERROR), 'occurred_at' => now()]);
}
});
}
}