oportunidades 4
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-10 07:03:59 +02:00
parent e662230df7
commit 6ddafc95ba
179 changed files with 22750 additions and 933 deletions
+28 -5
View File
@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Schema;
class VirtualDeskInstallation
{
public const MIGRATIONS = ['2026_10_07_190000_create_internal_virtual_desk', '2026_10_08_120000_extend_desk_investor_guidance'];
public const MIGRATIONS = ['2026_10_07_190000_create_internal_virtual_desk', '2026_10_08_120000_extend_desk_investor_guidance', '2026_10_09_190000_prepare_investor_path'];
public function fingerprint(): string
{
@@ -20,6 +20,17 @@ class VirtualDeskInstallation
resource_path('js/lib/business-financial-plan.ts'), resource_path('js/lib/financial-calculators.ts'), resource_path('js/lib/project-dossier.ts'),
...array_map(fn ($name) => database_path('migrations/'.$name.'.php'), self::MIGRATIONS),
...(glob(resource_path('js/pages/desk/*.tsx')) ?: []), resource_path('js/components/investor-guidance.tsx'), resource_path('js/components/journey-companion.tsx'), resource_path('js/components/desk-internal-operations.tsx'), resource_path('js/layouts/portal-navigation.tsx'), resource_path('js/layouts/app/app-sidebar-layout.tsx'), resource_path('js/pages/investment/shared.tsx'), resource_path('js/lib/investor-guidance-finance.ts')];
$files = [...$files, app_path('Services/InvestorPath.php'), app_path('Services/InvestorPathDefinition.php'),
app_path('Services/SimulationFormDrafts.php'), app_path('Http/Requests/StoreSimulationFormDraftRequest.php'),
app_path('Http/Controllers/InvestorPathController.php'), app_path('Http/Controllers/DashboardController.php'),
base_path('routes/investor-path.php'), base_path('routes/web.php'), config_path('investor-path.php'),
resource_path('js/pages/dashboard.tsx'), resource_path('js/components/simulation-form.tsx'), resource_path('js/lib/investor-path.ts'),
database_path('migrations/2026_10_01_000200_create_simulation_execution_core.php'),
resource_path('js/components/investment-map-panel.tsx'), resource_path('js/pages/investment/detail.tsx'), resource_path('js/pages/partnerships/catalogue.tsx'),
...(glob(resource_path('js/pages/investor-path/*.tsx')) ?: [])];
$files = [...$files, app_path('Services/VerifiedDocumentBinary.php'), app_path('Services/TerritoryGeometryImport.php'), app_path('Http/Controllers/TerritoryGeometryImportController.php'), config_path('geometry-import.php'), resource_path('js/pages/management/territory-geometry-import.tsx'),
base_path('bootstrap/app.php'), app_path('Providers/FortifyServiceProvider.php'), config_path('fortify.php'),
resource_path('js/pages/auth/login.tsx'), resource_path('js/pages/auth/two-factor-challenge.tsx')];
sort($files);
return hash('sha256', json_encode(array_map(fn ($file) => hash_file('sha256', $file), $files), JSON_THROW_ON_ERROR));
@@ -57,20 +68,29 @@ class VirtualDeskInstallation
foreach (['desk_cases', 'desk_requests', 'desk_documents', 'desk_messages', 'tasks', 'investor_accounts', 'investment_submissions'] as $table) {
$counts[$table] = Schema::hasTable($table) ? DB::table($table)->count() : null;
}
$installed = app(VirtualDesk::class)->installed();
$pathInstalled = Schema::hasColumn('investor_journeys', 'path_definition_id') && Schema::hasColumn('journey_steps', 'path_detail');
$installed = app(VirtualDesk::class)->installed() && $pathInstalled
&& DB::table('migrations')->where('migration', '2026_10_09_190000_prepare_investor_path')->exists();
$pathQa = $read('investor-path-qa-evidence.json');
$pathQaVerified = ($pathQa['code_hash'] ?? null) === $code && ($pathQa['api_persistence_passed'] ?? false)
&& ($pathQa['integrated_ui_ab_passed'] ?? false) && str_ends_with($pathQa['database'] ?? '', '_test') && (int) ($pathQa['port'] ?? 0) === 55441;
$privileges = DB::selectOne("select has_schema_privilege(current_user, 'public', 'CREATE') as can_create,
pg_has_role(current_user, (select tableowner from pg_tables where schemaname='public' and tablename='tasks'), 'USAGE') as can_alter_tasks");
$databaseReady = (bool) ($privileges->can_create && $privileges->can_alter_tasks);
return ['hash' => hash('sha256', json_encode([$identity, $code, $counts, $backupVerified, $qaVerified, $compatibilityVerified, $databaseReady], JSON_THROW_ON_ERROR)), 'code_hash' => $code,
'installed' => $installed, 'target' => $identity, 'counts' => $counts, 'backupVerified' => $backupVerified, 'qaVerified' => $qaVerified,
return ['hash' => hash('sha256', json_encode([$identity, $code, $counts, $backupVerified, $qaVerified, $compatibilityVerified, $databaseReady, $pathQaVerified, $pathInstalled, (bool) config('investor-path.enabled')], JSON_THROW_ON_ERROR)), 'code_hash' => $code,
'installed' => $installed, 'baseDeskInstalled' => app(VirtualDesk::class)->installed(), 'pathInstalled' => $pathInstalled,
'pathEnabled' => (bool) config('investor-path.enabled'), 'pathQaVerified' => $pathQaVerified,
'target' => $identity, 'counts' => $counts, 'backupVerified' => $backupVerified, 'qaVerified' => $qaVerified,
'compatibilityVerified' => $compatibilityVerified, 'backupDate' => $backup['verified_at'] ?? null,
'databaseReady' => $databaseReady,
'scanner' => app(MalwareScanner::class)->health(),
'pendingMigrations' => array_values(array_filter(self::MIGRATIONS, fn ($name) => ! DB::table('migrations')->where('migration', $name)->exists())),
'rollback' => 'Reversão vazia ensaiada em cópia isolada. Com dados novos, preservar/exportar pedidos e tarefas; o down recusa eliminar dados. Restauro integral só numa janela autorizada, após reconciliar alterações posteriores ao backup.',
'changes' => ['Completar o Balcão Virtual existente, preservando pedidos anteriores.', 'Acrescentar associações opcionais ao investidor, projecto e percurso existentes.',
'Reutilizar tasks do CRM para acções expressamente assumidas; sem envio externo.', 'Nenhuma publicação de serviço, documento ou candidatura automática.']];
'Reutilizar tasks do CRM para acções expressamente assumidas; sem envio externo.', 'Nenhuma publicação de serviço, documento ou candidatura automática.',
'Preparar duas colunas nas entidades existentes e uma versão publicada do percurso de 19 etapas. Não converter dossiers existentes.',
'Não habilitar a navegação por instalação: configuração INVESTOR_PATH_ENABLED permanece uma acção separada.']];
}
public function apply(string $hash, User $actor): void
@@ -84,7 +104,9 @@ class VirtualDeskInstallation
return;
}
abort_unless($preview['backupVerified'] && $preview['qaVerified'] && $preview['compatibilityVerified'] && $preview['databaseReady'], 422, 'Backup/restauro atualizado, compatibilidade/reversão, permissões da base e QA desta versão continuam necessários.');
abort_unless($preview['pathQaVerified'], 422, 'Demonstração A/B na interface e evidência API/persistência desta versão ainda necessárias.');
config(['virtual-desk.installation_authorized' => true]);
config(['investor-path.authenticated_installation' => true]);
try {
foreach (self::MIGRATIONS as $name) {
if (! DB::table('migrations')->where('migration', $name)->exists()) {
@@ -94,6 +116,7 @@ class VirtualDeskInstallation
}
} finally {
config(['virtual-desk.installation_authorized' => false]);
config(['investor-path.authenticated_installation' => false]);
}
DB::table('audit_events')->insert(['source_system' => 'virtual-desk', 'entity_type' => 'desk_cases', 'actor_user_id' => $actor->id,
'action' => 'desk_installed', 'after_data' => json_encode(['preview_hash' => $hash, 'code_hash' => $preview['code_hash'], 'prior_counts' => $preview['counts']])]);