fix: complete InvesteMoz validation and error corrections

This commit is contained in:
2026-10-29 21:47:33 +02:00
parent 63147312e2
commit daf8623441
718 changed files with 127308 additions and 20959 deletions
+48 -21
View File
@@ -2,18 +2,22 @@
namespace App\Services;
use App\Models\User;
use App\Support\CheckedFile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Validator;
/** Existing session, permission and MFA control; no actor argument or console entry point. */
final class SectorReconciliationExecution
{
public const SOURCE = 'sector-reconciliation-authenticated';
public const OPERATION = 'sector-reconciliation-20261018';
private function actor(Request $request): \App\Models\User
private function actor(Request $request): User
{
abort_if(app()->runningInConsole(), 403, 'A execução exige a sessão HTTP autenticada existente.');
$actor = Auth::guard('web')->user();
@@ -21,7 +25,7 @@ final class SectorReconciliationExecution
abort_unless($actor->hasVerifiedEmail() && $actor->can('manage-portal'), 403);
// Same installation permission and Fortify session MFA used by 140000/150000.
app(PortalProfileInstallation::class)->authorize($actor, true);
abort_unless((int)$request->session()->get('portal.mfa_at', 0) <= time(), 403, 'MFA da sessão inválido.');
abort_unless((int) $request->session()->get('portal.mfa_at', 0) <= time(), 403, 'MFA da sessão inválido.');
return $actor;
}
@@ -29,45 +33,62 @@ final class SectorReconciliationExecution
public function revision(): string
{
$file = database_path('data/sector-reconciliation-execution.json');
$manifest = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
$manifest = json_decode(CheckedFile::read($file), true, 512, JSON_THROW_ON_ERROR);
$files = ['database/data/sector-reconciliation-execution.json' => hash_file('sha256', $file)];
foreach ($manifest['files'] as $path) $files[$path] = hash_file('sha256', base_path($path));
foreach ($manifest['files'] as $path) {
$files[$path] = hash_file('sha256', base_path($path));
}
$sql = database_path('data/sector-reconciliation-20261018.sql');
abort_unless(hash_equals($manifest['sql_sha256'], hash_file('sha256', $sql)), 409, 'SQL diferente da revisão delimitada.');
abort_unless(hash_equals($manifest['sql_sha256'], CheckedFile::sha256($sql)), 409, 'SQL diferente da revisão delimitada.');
$files['database/data/sector-reconciliation-20261018.sql'] = hash_file('sha256', $sql);
foreach (\Illuminate\Support\Facades\File::allFiles(public_path('build')) as $build) $files['public/build/'.$build->getRelativePathname()] = hash_file('sha256', $build->getPathname());
foreach (File::allFiles(public_path('build')) as $build) {
$files['public/build/'.$build->getRelativePathname()] = hash_file('sha256', $build->getPathname());
}
ksort($files);
return hash('sha256', json_encode($files, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
}
/**
* @return array<array-key, mixed>
*/
private function target(): array
{
return (array) DB::connection()->selectOne('SELECT current_database() AS database, inet_server_port() AS port', [], false);
}
/**
* @param array<array-key, mixed> $target
*/
private function readiness(string $revision, array $target): void
{
$p = storage_path('app/private/investmoz-v2/sector-reconciliation/readiness.json');
$proof = is_file($p) ? json_decode(file_get_contents($p), true, 512, JSON_THROW_ON_ERROR) : [];
$proof = is_file($p) ? json_decode(CheckedFile::read($p), true, 512, JSON_THROW_ON_ERROR) : [];
abort_unless(($proof['operation'] ?? '') === self::OPERATION && ($proof['revision'] ?? '') === $revision
&& ($proof['target'] ?? []) === $target && !empty($proof['authorization_record']), 409, 'Préflight/autorização da revisão e destino necessários.');
$isolated = app()->environment('testing') && (int)$target['port'] === 55441
&& ($proof['target'] ?? []) === $target && ! empty($proof['authorization_record']), 409, 'Préflight/autorização da revisão e destino necessários.');
$isolated = app()->environment('testing') && (int) $target['port'] === 55441
&& str_ends_with($target['database'], '_test') && config('database.connections.pgsql.host') === '127.0.0.1'
&& !config('database.connections.pgsql.url');
if (!$isolated) {
&& ! config('database.connections.pgsql.url');
if (! $isolated) {
abort_unless(($proof['restore_verified'] ?? false) && ($proof['restored_boot_verified'] ?? false)
&& strtotime($proof['verified_at'] ?? '') >= time() - 86400 && strtotime($proof['verified_at'] ?? '') <= time(), 409, 'Backup/restauro fresco do procedimento existente necessário.');
foreach (['dump', 'files'] as $kind) abort_unless(is_file($proof[$kind.'_path'] ?? '')
&& hash_equals($proof[$kind.'_sha256'] ?? '', hash_file('sha256', $proof[$kind.'_path'])), 409);
foreach (['dump', 'files'] as $kind) {
abort_unless(is_file($proof[$kind.'_path'] ?? '')
&& hash_equals($proof[$kind.'_sha256'] ?? '', CheckedFile::sha256($proof[$kind.'_path'])), 409);
}
}
}
/**
* @return array<array-key, mixed>
*/
public function preview(Request $request): array
{
$actor = $this->actor($request);
abort_if($request->query->count(), 422, 'Ator, revisão ou opções por URL recusados.');
$revision = $this->revision(); $target = $this->target(); $this->readiness($revision, $target);
abort_if($request->query->count() > 0, 422, 'Ator, revisão ou opções por URL recusados.');
$revision = $this->revision();
$target = $this->target();
$this->readiness($revision, $target);
$state = ['operation' => self::OPERATION, 'revision' => $revision, 'target' => $target,
'actor' => ['id' => $actor->id, 'name' => $actor->name], 'migrations' => [], 'ready' => true];
$state['hash'] = hash_hmac('sha256', json_encode([$actor->id, $request->session()->getId(), $state], JSON_THROW_ON_ERROR), config('app.key'));
@@ -75,6 +96,9 @@ final class SectorReconciliationExecution
return $state;
}
/**
* @return array<array-key, mixed>
*/
public function apply(Request $request): array
{
$actor = $this->actor($request);
@@ -82,7 +106,9 @@ final class SectorReconciliationExecution
$data = Validator::make($request->all(), ['hash' => 'required|string|size:64', 'operation_key' => 'required|uuid', 'confirm' => 'required|accepted'])->validate();
$db = DB::connection();
abort_unless($db->transactionLevel() === 0, 409, 'A operação deve possuir a sua transação.');
$revision = $this->revision(); $target = $this->target(); $this->readiness($revision, $target);
$revision = $this->revision();
$target = $this->target();
$this->readiness($revision, $target);
$requestHash = hash('sha256', json_encode($data, JSON_THROW_ON_ERROR));
$result = $db->transaction(function () use ($db, $actor, $data, $request, $revision, $target, $requestHash) {
@@ -90,8 +116,9 @@ final class SectorReconciliationExecution
$old = $db->table('audit_events')->useWritePdo()->where('source_system', self::SOURCE)->where('correlation_id', $data['operation_key'])->first();
if ($old) {
$saved = json_decode($old->after_data, true, 512, JSON_THROW_ON_ERROR);
abort_unless((int)$old->actor_user_id === $actor->id && ($saved['revision'] ?? '') === $revision
abort_unless((int) $old->actor_user_id === $actor->id && ($saved['revision'] ?? '') === $revision
&& hash_equals($saved['request_hash'], $requestHash), 409);
return [...$saved['receipt'], 'replayed' => true];
}
$plan = $this->preview($request);
@@ -99,13 +126,13 @@ final class SectorReconciliationExecution
$before = $db->selectOne("SELECT pg_backend_pid() AS pid, txid_current()::text AS txid, NULLIF(current_setting('investmoz.actor_user_id',true),'') AS actor", [], false);
abort_unless($before->actor === null, 409, 'Ligação contém um ator anterior; operação recusada.');
// true means SET LOCAL. Same write connection and transaction execute every statement.
$db->selectOne("SELECT set_config('investmoz.actor_user_id',?,true), set_config('investmoz.operation_key',?,true), set_config('investmoz.authorized_revision',?,true)", [(string)$actor->id, $data['operation_key'], $revision], false);
$db->selectOne("SELECT set_config('investmoz.actor_user_id',?,true), set_config('investmoz.operation_key',?,true), set_config('investmoz.authorized_revision',?,true)", [(string) $actor->id, $data['operation_key'], $revision], false);
$oldDataEvent = $db->table('audit_events')->useWritePdo()->where('source_system', self::OPERATION)->exists();
$db->getPdo()->exec(file_get_contents(database_path('data/sector-reconciliation-20261018.sql')));
$db->getPdo()->exec(CheckedFile::read(database_path('data/sector-reconciliation-20261018.sql')));
$scope = $db->selectOne("SELECT pg_backend_pid() AS pid, txid_current()::text AS txid, current_setting('investmoz.actor_user_id') AS actor", [], false);
abort_unless($scope->pid === $before->pid && $scope->txid === $before->txid && (int)$scope->actor === $actor->id, 500);
abort_unless($scope->pid === $before->pid && $scope->txid === $before->txid && (int) $scope->actor === $actor->id, 500);
$receipt = ['operation' => self::OPERATION, 'operation_key' => $data['operation_key'], 'revision' => $revision,
'actor_user_id' => $actor->id, 'target' => $target, 'changed' => !$oldDataEvent, 'opportunity_id' => 2,
'actor_user_id' => $actor->id, 'target' => $target, 'changed' => ! $oldDataEvent, 'opportunity_id' => 2,
'sector_id' => 8, 'replayed' => false, 'connection_pid' => $scope->pid, 'transaction_id' => $scope->txid];
$db->table('audit_events')->insert(['source_system' => self::SOURCE, 'entity_type' => 'sector-reconciliation', 'action' => 'execute',
'actor_user_id' => $actor->id, 'correlation_id' => $data['operation_key'], 'data_classification' => 'internal',