*/ public function files(): array { return ['app/Models/InvestmentZone.php', 'app/Services/InvestmentZones.php', 'app/Services/InvestmentZoneOperationalProfile.php', 'app/Services/InvestmentZonePathContext.php', 'app/Services/InvestmentZoneProfileInstallation.php', 'app/Http/Controllers/InvestmentZoneController.php', 'app/Http/Controllers/InvestorPathController.php', 'app/Services/InvestorPath.php', 'routes/investment-zones.php', 'routes/investor-path.php', 'resources/js/pages/investment/zone.tsx', 'resources/js/pages/investment/zones.tsx', 'resources/js/pages/investor-path/index.tsx', 'resources/js/pages/investor-path/show.tsx', 'resources/js/components/investment-zone-installation.tsx', 'database/migrations/'.InvestmentZoneOperationalProfile::MIGRATION.'.php']; } public function fingerprint(): string { $files = array_map(fn ($p) => [$p, hash_file('sha256', base_path($p))], $this->files()); foreach (File::allFiles(public_path('build')) as $file) { $files[] = ['public/build/'.$file->getRelativePathname(), hash_file('sha256', $file->getPathname())]; } usort($files, fn ($a, $b) => strcmp($a[0], $b[0])); return hash('sha256', json_encode($files, JSON_THROW_ON_ERROR)); } private function authorize(User $actor, bool $write = false): void { abort_unless($actor->hasVerifiedEmail() && app(ScopedPortalAdministration::class)->administrative($actor), 403, 'Gestão autorizada necessária.'); if ($write) { app(PortalProfileInstallation::class)->authorize($actor, true); } } /** * @param array $target * @return array */ private function evidence(array $target, string $code): array { $qa = 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 ($qa) { return ['ready' => true, 'mode' => 'isolated_QA']; } if (app()->environment('testing')) { return ['ready' => false, 'mode' => 'invalid_QA']; } $path = storage_path('app/private/investmoz-v2/park-profile/readiness.json'); $p = is_file($path) ? json_decode(CheckedFile::read($path), true) : []; $date = strtotime($p['verified_at'] ?? ''); $auth = $p['active_authorization'] ?? []; $ready = $target['database'] === 'investemoz' && (int) $target['port'] === 5432 && ! config('database.connections.pgsql.url') && in_array(config('database.connections.pgsql.host'), ['127.0.0.1', 'localhost'], true) && ($p['target']['database'] ?? '') === $target['database'] && (int) ($p['target']['port'] ?? 0) === (int) $target['port'] && ($p['code_hash'] ?? '') === $code && $date !== false && $date <= time() && $date >= time() - 86400 && ($p['restore_verified'] ?? false) && ($p['files_verified'] ?? false) && ($p['qa_passed'] ?? false) && ($p['compatibility_verified'] ?? false) && ($p['restored_boot_verified'] ?? false) && (int) ($p['restore_target']['port'] ?? 0) === 55441 && str_ends_with($p['restore_target']['database'] ?? '', '_test') && ($auth['decision_record'] ?? '') !== '' && ($auth['code_hash'] ?? '') === $code && ($auth['migration'] ?? '') === InvestmentZoneOperationalProfile::MIGRATION; foreach (['dump', 'files'] as $kind) { $file = $p[$kind.'_path'] ?? ''; $ready = $ready && is_file($file) && hash_equals($p[$kind.'_sha256'] ?? '', CheckedFile::sha256($file)); } return ['ready' => $ready, 'mode' => 'active_requires_own_authorization_and_fresh_proof', 'proof_hash' => is_file($path) ? hash_file('sha256', $path) : null]; } /** * @return array */ public function preview(User $actor): array { $this->authorize($actor); $target = (array) DB::selectOne('SELECT current_database() AS database,inet_server_port() AS port'); $code = $this->fingerprint(); $ledgerCount = DB::table('migrations')->where('migration', InvestmentZoneOperationalProfile::MIGRATION)->count(); $ledger = $ledgerCount === 1; $column = Schema::hasColumn('investment_zones', 'editorial_draft'); $shape = DB::selectOne("SELECT data_type,is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name='investment_zones' AND column_name='editorial_draft'"); $correct = $shape && $shape->data_type === 'text' && $shape->is_nullable === 'YES'; $consistent = ($ledger && $column && $correct) || ($ledgerCount === 0 && ! $column); $reasons = []; if (! $consistent) { $reasons[] = 'Ledger e coluna divergentes; reconciliar sem instalação parcial.'; } try { $this->authorize($actor, true); } catch (HttpException $e) { $reasons[] = $e->getMessage(); } $evidence = $this->evidence($target, $code); if (! $evidence['ready']) { $reasons[] = 'Decisão específica e backup/restauro/arranque frescos desta revisão necessários.'; } $state = ['target' => $target, 'migration' => InvestmentZoneOperationalProfile::MIGRATION, 'code_hash' => $code, 'installed' => $ledger && $column, 'consistent' => $consistent, 'ready' => $reasons === [], 'reasons' => $reasons, 'evidence' => $evidence]; return [...$state, 'hash' => hash_hmac('sha256', json_encode([$actor->id, session()->getId(), $state], JSON_THROW_ON_ERROR), config('app.key'))]; } /** * @param array $input * @return array */ public function apply(User $actor, array $input): array { $this->authorize($actor, true); abort_if(array_diff(array_keys($input), ['hash', 'idempotency_key', 'confirm', '_token']) !== [], 422, 'Ator, migration ou caminho arbitrário não permitido.'); $data = Validator::make($input, ['hash' => 'required|string|size:64', 'idempotency_key' => 'required|uuid', 'confirm' => 'required|accepted'])->validate(); return DB::transaction(function () use ($actor, $data) { DB::select('SELECT pg_advisory_xact_lock(161026,130000)'); $requestHash = hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)); $old = DB::table('audit_events')->where('source_system', self::SOURCE)->where('correlation_id', $data['idempotency_key'])->first(); if ($old) { $receipt = json_decode($old->after_data, true); abort_unless((int) $old->actor_user_id === $actor->id && hash_equals($receipt['request_hash'], $requestHash), 409, 'Chave já utilizada com outro ator ou conteúdo.'); return [...$receipt['result'], 'replayed' => true]; } $plan = $this->preview($actor); abort_unless(hash_equals($plan['hash'], $data['hash']), 409, 'Prévia alterada; consulte novamente.'); abort_unless($plan['ready'], 422, implode(' ', $plan['reasons'])); if (! $plan['installed']) { config(['investment-zone-profile.authenticated_installation' => true]); try { abort_unless(Artisan::call('migrate', ['--path' => 'database/migrations/'.InvestmentZoneOperationalProfile::MIGRATION.'.php', '--force' => true]) === 0, 500, 'Falha na instalação nominal.'); } finally { config(['investment-zone-profile.authenticated_installation' => false]); } } abort_unless($this->preview($actor)['installed'], 500, 'Esquema nominal inconsistente.'); $result = ['migration' => $plan['migration'], 'code_hash' => $plan['code_hash'], 'target' => $plan['target'], 'changed' => ! $plan['installed'], 'business_records_changed' => 0, 'replayed' => false, 'receipt' => $data['idempotency_key']]; DB::table('audit_events')->insert(['source_system' => self::SOURCE, 'entity_type' => 'investment-zone-profile', 'entity_id' => null, 'actor_user_id' => $actor->id, 'correlation_id' => $data['idempotency_key'], 'action' => 'schema', 'data_classification' => 'restricted', 'after_data' => json_encode(['request_hash' => $requestHash, 'result' => $result], JSON_THROW_ON_ERROR)]); return $result; }); } }