Files
Investidor-V2/app/Services/OrganizationCountryInstallation.php
T
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

119 lines
7.8 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
/** An explicit extension of the existing authenticated resource installer. */
class OrganizationCountryInstallation
{
public const MIGRATION = '2026_10_10_050000_extend_organization_country';
/** @return list<string> */
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<string,mixed> */
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<string, mixed> */
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<string, mixed> $input
* @return array<string, mixed> */
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;
});
}
}