@@ -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']])]);
|
||||
|
||||
Reference in New Issue
Block a user