Files
Investidor-V2/app/Services/PartnershipInstallation.php
T
nala bdd766c68a
tests / ci (push) Canceled after 0s
resolvemos o problema dos ZEE e ZFI
2026-10-07 19:46:35 +02:00

86 lines
5.7 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class PartnershipInstallation
{
private const MIGRATION = '2026_10_07_160000_extend_internal_partnership_exchange';
public function fingerprint(): string
{
$files = [...(glob(app_path('Services/Partnership*.php')) ?: []), app_path('Http/Controllers/PartnershipExchangeController.php'), app_path('Http/Controllers/PartnershipInstallationController.php'),
app_path('Http/Middleware/PartnershipInstalled.php'), database_path('migrations/'.self::MIGRATION.'.php'), config_path('partnerships.php'), base_path('routes/partnerships.php'),
...(glob(resource_path('js/pages/partnerships/*.tsx')) ?: []), resource_path('js/components/partnership-source-links.tsx'), resource_path('js/components/partnership-search-results.tsx'), resource_path('js/components/simulation-form.tsx'),
app_path('Http/Controllers/PortalServicesController.php'), base_path('routes/web.php'), resource_path('js/pages/portfolio/show.tsx'), resource_path('js/pages/portfolio/shared.tsx'),
...array_map(fn ($name) => resource_path('js/pages/investment/'.$name.'.tsx'), ['business-opportunity', 'catalogue', 'detail', 'profile', 'zone'])];
sort($files);
$hashes = [];
foreach ($files as $file) {
$hashes[str_replace(base_path().DIRECTORY_SEPARATOR, '', $file)] = hash_file('sha256', $file);
}
return hash('sha256', json_encode($hashes, JSON_THROW_ON_ERROR));
}
/** @return array<string,mixed> */
public function preview(): array
{
$identity = DB::selectOne('select current_database() as database, inet_server_port() as port');
$rows = DB::table('portal_partnerships')->orderBy('id')->get(['id', 'user_id', 'title', 'sector', 'province', 'offer', 'seeking', 'status', 'reviewed_by', 'created_at', 'updated_at'])->all();
$hash = hash('sha256', json_encode(['target' => $identity, 'code' => $this->fingerprint(), 'records' => $rows], JSON_THROW_ON_ERROR));
$proof = $this->read('backup-proof.json');
$qa = $this->read('qa-evidence.json');
$verified = ($proof['restore_verified'] ?? false) && ($proof['target']['database'] ?? null) === $identity->database && (int) ($proof['target']['port'] ?? 0) === (int) $identity->port
&& is_file($proof['dump_path'] ?? '') && hash_file('sha256', $proof['dump_path']) === ($proof['sha256'] ?? null);
$qaVerified = ($qa['passed'] ?? false) && ($qa['code_hash'] ?? null) === $this->fingerprint() && str_ends_with($qa['database'] ?? '', '_test') && (int) ($qa['port'] ?? 0) === 55441;
return ['hash' => $hash, 'installed' => Schema::hasTable('partnership_interests'), 'target' => $identity, 'records' => $rows, 'records_hash' => hash('sha256', json_encode($rows, JSON_THROW_ON_ERROR)), 'backupVerified' => $verified, 'qaVerified' => $qaVerified,
'backupDate' => $proof['verified_at'] ?? null, 'changes' => ['Extensão de portal_partnerships com campos opcionais e versão, sem substituir os registos.',
'Entidades próprias de candidaturas, salas, mensagens, referências documentais, marcos, eventos e notificações locais.',
'Nenhuma cópia de contas, organizações, documentos, setores, territórios ou oportunidades de investimento.', 'Estados e autores antigos preservados; publicação nova continua moderada.']];
}
/** @return array<string,mixed> */
private function read(string $file): array
{
$path = config('partnerships.proof_directory', storage_path('app/private/partnership-exchange')).'/'.$file;
if (! is_file($path)) {
return [];
}
$contents = file_get_contents($path);
abort_unless(is_string($contents), 503, 'Evidência privada indisponível.');
return json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
}
public function apply(string $hash, User $actor): void
{
DB::transaction(function () use ($hash, $actor) {
DB::statement('LOCK TABLE portal_partnerships IN ACCESS EXCLUSIVE MODE');
$preview = $this->preview();
abort_unless(hash_equals($preview['hash'], $hash), 409, 'Pré-visualização alterada; reveja a instalação.');
if ($preview['installed']) {
return;
}
abort_unless($preview['backupVerified'] && $preview['qaVerified'], 422, 'Backup/restauro e QA desta versão ainda não estão comprovados.');
config(['partnerships.installation_authorized' => true]);
try {
$migration = require database_path('migrations/'.self::MIGRATION.'.php');
$migration->up();
} finally {
config(['partnerships.installation_authorized' => false]);
}
$rows = DB::table('portal_partnerships')->orderBy('id')->get(['id', 'user_id', 'title', 'sector', 'province', 'offer', 'seeking', 'status', 'reviewed_by', 'created_at', 'updated_at'])->all();
abort_unless(hash_equals($preview['records_hash'], hash('sha256', json_encode($rows, JSON_THROW_ON_ERROR))), 409, 'Os registos antigos não foram preservados.');
DB::table('migrations')->insert(['migration' => self::MIGRATION, 'batch' => (DB::table('migrations')->max('batch') ?? 0) + 1]);
DB::table('audit_events')->insert(['source_system' => 'partnership-exchange', 'entity_type' => 'portal_partnerships', 'actor_user_id' => $actor->id,
'action' => 'partnership_module_installed', 'after_data' => json_encode(['preview_hash' => $hash, 'code_hash' => $this->fingerprint(), 'preserved_records' => count($rows), 'migration' => self::MIGRATION])]);
});
}
}