*/ public function files(): array { return ['app/Services/OrganizationCountry.php', 'app/Services/OrganizationCountryInstallation.php', 'app/Providers/AppServiceProvider.php', 'app/Services/CountryCatalogue.php', 'app/Services/PortalProfileAccess.php', 'app/Services/PortalProfileInstallation.php', 'app/Http/Controllers/InvestmentWorkspaceController.php', 'app/Http/Controllers/InvestmentSubmissionController.php', 'app/Http/Controllers/OpportunityResourceController.php', 'app/Models/User.php', 'config/portal.php', 'routes/web.php', 'routes/investment.php', 'database/migrations/'.self::MIGRATION.'.php', 'resources/js/components/country-select.tsx', 'resources/js/pages/investment/profile.tsx', 'resources/js/pages/investment/submission.tsx', 'resources/js/pages/management/opportunity-install.tsx', 'resources/data/iso3166.tab', 'config/portal_profiles.php', 'composer.json', 'composer.lock']; } public function fingerprint(): string { return hash('sha256', json_encode(array_map(fn ($file) => [$file, hash_file('sha256', base_path($file))], $this->files()), JSON_THROW_ON_ERROR)); } /** Separate bounded evidence: partial opportunity modules are not an installation dependency. * @return array */ public function evidence(object $identity): array { $path = storage_path('app/private/investmoz-v2/organization-country-installation/readiness.json'); $bytes = is_file($path) ? file_get_contents($path) : false; $p = is_string($bytes) ? json_decode($bytes, true) : []; $p = is_array($p) ? $p : []; $target = get_object_vars($identity); $ready = ($p['code_hash'] ?? '') === $this->fingerprint() && isset($target['database'], $target['port']) && ($p['target']['database'] ?? '') === $target['database'] && (int) ($p['target']['port'] ?? 0) === (int) $target['port'] && strtotime($p['verified_at'] ?? '') >= time() - 86400 && ($p['restore_verified'] ?? false) && ($p['files_verified'] ?? false) && ($p['qa_passed'] ?? false) && ($p['compatibility_verified'] ?? false) && ($p['reversal_verified'] ?? false) && is_file($p['dump_path'] ?? '') && hash_file('sha256', $p['dump_path']) === ($p['dump_sha256'] ?? '') && is_file($p['files_path'] ?? '') && hash_file('sha256', $p['files_path']) === ($p['files_sha256'] ?? ''); return ['ready' => $ready, 'backupDate' => $p['verified_at'] ?? null]; } /** @return array */ public function preview(User $actor): array { app(PortalProfileInstallation::class)->authorize($actor); $identity = DB::selectOne('SELECT current_database() AS database, inet_server_port() AS port'); $evidence = $this->evidence($identity); $columns = array_map(fn ($column) => Schema::hasColumn('organizations', $column), ['incorporation_country_code', 'incorporation_country_provenance']); $ledger = DB::table('migrations')->where('migration', self::MIGRATION)->exists(); $installed = $ledger && ! in_array(false, $columns, true); $consistent = ($ledger && ! in_array(false, $columns, true)) || (! $ledger && ! in_array(true, $columns, true)); $dependencies = array_reduce(['organizations', 'memberships', 'audit_events'], fn ($ok, $table) => $ok && Schema::hasTable($table), true); $plan = ['migration' => self::MIGRATION, 'installed' => $installed, 'consistent' => $consistent, 'ready' => $consistent && $dependencies && $evidence['ready'], 'target' => $identity, 'code_hash' => $this->fingerprint(), 'backupDate' => $evidence['backupDate'], 'dependencies' => ['existing_organizations_memberships_audit' => $dependencies], 'changes' => ['Duas colunas opcionais no cadastro mestre organizations; zero tabelas, organizações, contas ou atribuições.', 'Preserva source_system, external_id, valores anteriores, versões e submissões; reutilização explícita, sem inferência por nome.', 'Ação independente de 030000, 040000 e 190000; não ativa flags, cálculos, publicação nem o percurso.'], 'rollback' => 'Desabilitar o consumidor mantém os dados. Não remover colunas com histórico; ensaiar a reversão de código e, se necessário, restauro no clone verificado.']; $plan['hash'] = hash('sha256', json_encode($plan, JSON_THROW_ON_ERROR)); return $plan; } /** @param array $input * @return array */ public function apply(User $actor, array $input): array { app(PortalProfileInstallation::class)->authorize($actor, true); $data = Validator::make($input, ['hash' => 'required|string|size:64', 'confirm' => 'required|accepted', 'idempotency_key' => 'required|uuid', 'reason' => 'required|string|min:20|max:1000'])->validate(); return DB::transaction(function () use ($actor, $data) { DB::select('SELECT pg_advisory_xact_lock(101026,50000)'); $requestHash = hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)); $previous = DB::table('audit_events')->where('source_system', 'organization-country-installation')->where('actor_user_id', $actor->id)->where('correlation_id', $data['idempotency_key'])->first(); if ($previous) { $receipt = json_decode($previous->after_data, true, 512, JSON_THROW_ON_ERROR); abort_unless(hash_equals($receipt['request_hash'], $requestHash), 409, 'Chave de repetição reutilizada com outro conteúdo.'); return $receipt['result']; } $plan = $this->preview($actor); abort_unless(hash_equals($plan['hash'], $data['hash']), 409, 'Plano alterado; consulte a pré-visualização.'); abort_unless($plan['consistent'], 409, 'Ledger/schema incoerentes. Recuperar antes de instalar.'); $changed = ! $plan['installed']; if ($changed) { abort_unless($plan['ready'], 422, 'Exige provas atuais da revisão, alvo, backup/restauro e compatibilidade.'); config(['organization_country.authenticated_installation' => true]); try { abort_unless(Artisan::call('migrate', ['--path' => 'database/migrations/'.self::MIGRATION.'.php', '--force' => true]) === 0, 500); } finally { config(['organization_country.authenticated_installation' => false]); } abort_unless($this->preview($actor)['installed'], 500, 'Verificação pós-instalação falhou.'); } $result = ['installed' => true, 'changed' => $changed, 'migration' => self::MIGRATION, 'new_columns' => $changed ? 2 : 0, 'new_tables' => 0, 'new_organizations' => 0, 'flags_changed' => false]; DB::table('audit_events')->insert(['source_system' => 'organization-country-installation', 'entity_type' => 'installation', 'action' => 'install', 'actor_user_id' => $actor->id, 'correlation_id' => $data['idempotency_key'], 'data_classification' => 'restricted', 'after_data' => json_encode(['request_hash' => $requestHash, 'reason' => $data['reason'], 'result' => $result, 'code_hash' => $plan['code_hash']], JSON_THROW_ON_ERROR)]); return $result; }); } }