Files
Investidor-V2/app/Services/InvestmentZoneProfileInstallation.php

92 lines
7.9 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;
use Symfony\Component\HttpKernel\Exception\HttpException;
/** Nominal schema installation. No imports, backfill, publication, permissions or flags. */
class InvestmentZoneProfileInstallation
{
public const SOURCE='investment-zone-profile-installation';
public function files(): array
{
return ['app/Models/InvestmentZone.php','app/Services/InvestmentZones.php','app/Services/InvestmentZoneOperationalProfile.php',
'app/Services/InvestmentZonePathContext.php','app/Services/InvestmentZoneProfileInstallation.php','app/Http/Controllers/InvestmentZoneController.php',
'app/Http/Controllers/InvestorPathController.php','app/Services/InvestorPath.php','routes/investment-zones.php','routes/investor-path.php',
'resources/js/pages/investment/zone.tsx','resources/js/pages/investment/zones.tsx','resources/js/pages/investor-path/index.tsx',
'resources/js/pages/investor-path/show.tsx','resources/js/components/investment-zone-installation.tsx',
'database/migrations/'.InvestmentZoneOperationalProfile::MIGRATION.'.php'];
}
public function fingerprint(): string
{
$files=array_map(fn($p)=>[$p,hash_file('sha256',base_path($p))],$this->files());
foreach(\Illuminate\Support\Facades\File::allFiles(public_path('build')) as $file){$files[]=['public/build/'.$file->getRelativePathname(),hash_file('sha256',$file->getPathname())];}
usort($files,fn($a,$b)=>strcmp($a[0],$b[0]));
return hash('sha256',json_encode($files,JSON_THROW_ON_ERROR));
}
private function authorize(User $actor,bool $write=false): void
{
abort_unless($actor->hasVerifiedEmail() && app(ScopedPortalAdministration::class)->administrative($actor),403,'Gestão autorizada necessária.');
if($write){app(PortalProfileInstallation::class)->authorize($actor,true);}
}
private function evidence(array $target,string $code): array
{
$qa=app()->environment('testing') && (int)$target['port']===55441 && str_ends_with($target['database'],'_test')
&& config('database.connections.pgsql.host')==='127.0.0.1' && !config('database.connections.pgsql.url');
if($qa){return ['ready'=>true,'mode'=>'isolated_QA'];}
if(app()->environment('testing')){return ['ready'=>false,'mode'=>'invalid_QA'];}
$path=storage_path('app/private/investmoz-v2/park-profile/readiness.json');
$p=is_file($path)?json_decode(file_get_contents($path),true):[];$date=strtotime($p['verified_at'] ?? '');$auth=$p['active_authorization'] ?? [];
$ready=$target['database']==='investemoz' && (int)$target['port']===5432 && !config('database.connections.pgsql.url')
&& in_array(config('database.connections.pgsql.host'),['127.0.0.1','localhost'],true)
&& ($p['target']['database'] ?? '')===$target['database'] && (int)($p['target']['port'] ?? 0)===(int)$target['port']
&& ($p['code_hash'] ?? '')===$code && $date!==false && $date<=time() && $date>=time()-86400
&& ($p['restore_verified'] ?? false) && ($p['files_verified'] ?? false) && ($p['qa_passed'] ?? false) && ($p['compatibility_verified'] ?? false)
&& ($p['restored_boot_verified'] ?? false) && (int)($p['restore_target']['port'] ?? 0)===55441 && str_ends_with($p['restore_target']['database'] ?? '','_test')
&& ($auth['decision_record'] ?? '')!=='' && ($auth['code_hash'] ?? '')===$code && ($auth['migration'] ?? '')===InvestmentZoneOperationalProfile::MIGRATION;
foreach(['dump','files'] as $kind){$file=$p[$kind.'_path'] ?? ''; $ready=$ready && is_file($file) && hash_equals($p[$kind.'_sha256'] ?? '',hash_file('sha256',$file));}
return ['ready'=>$ready,'mode'=>'active_requires_own_authorization_and_fresh_proof','proof_hash'=>is_file($path)?hash_file('sha256',$path):null];
}
public function preview(User $actor): array
{
$this->authorize($actor);$target=(array)DB::selectOne('SELECT current_database() AS database,inet_server_port() AS port');$code=$this->fingerprint();
$ledgerCount=DB::table('migrations')->where('migration',InvestmentZoneOperationalProfile::MIGRATION)->count();$ledger=$ledgerCount===1;$column=Schema::hasColumn('investment_zones','editorial_draft');
$shape=DB::selectOne("SELECT data_type,is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name='investment_zones' AND column_name='editorial_draft'");
$correct=$shape && $shape->data_type==='text' && $shape->is_nullable==='YES';
$consistent=($ledger && $column && $correct) || ($ledgerCount===0 && !$column);$reasons=[];
if(!$consistent){$reasons[]='Ledger e coluna divergentes; reconciliar sem instalação parcial.';}
try{$this->authorize($actor,true);}catch(HttpException $e){$reasons[]=$e->getMessage();}
$evidence=$this->evidence($target,$code);if(!$evidence['ready']){$reasons[]='Decisão específica e backup/restauro/arranque frescos desta revisão necessários.';}
$state=['target'=>$target,'migration'=>InvestmentZoneOperationalProfile::MIGRATION,'code_hash'=>$code,'installed'=>$ledger && $column,
'consistent'=>$consistent,'ready'=>$reasons===[],'reasons'=>$reasons,'evidence'=>$evidence];
return [...$state,'hash'=>hash_hmac('sha256',json_encode([$actor->id,session()->getId(),$state],JSON_THROW_ON_ERROR),config('app.key'))];
}
public function apply(User $actor,array $input): array
{
$this->authorize($actor,true);
abort_if(array_diff(array_keys($input),['hash','idempotency_key','confirm','_token']),422,'Ator, migration ou caminho arbitrário não permitido.');
$data=Validator::make($input,['hash'=>'required|string|size:64','idempotency_key'=>'required|uuid','confirm'=>'required|accepted'])->validate();
return DB::transaction(function()use($actor,$data){
DB::select('SELECT pg_advisory_xact_lock(161026,130000)');$requestHash=hash('sha256',json_encode($data,JSON_THROW_ON_ERROR));
$old=DB::table('audit_events')->where('source_system',self::SOURCE)->where('correlation_id',$data['idempotency_key'])->first();
if($old){$receipt=json_decode($old->after_data,true);abort_unless((int)$old->actor_user_id===$actor->id && hash_equals($receipt['request_hash'],$requestHash),409,'Chave já utilizada com outro ator ou conteúdo.');return [...$receipt['result'],'replayed'=>true];}
$plan=$this->preview($actor);abort_unless(hash_equals($plan['hash'],$data['hash']),409,'Prévia alterada; consulte novamente.');abort_unless($plan['ready'],422,implode(' ',$plan['reasons']));
if(!$plan['installed']){
config(['investment-zone-profile.authenticated_installation'=>true]);
try{abort_unless(Artisan::call('migrate',['--path'=>'database/migrations/'.InvestmentZoneOperationalProfile::MIGRATION.'.php','--force'=>true])===0,500,'Falha na instalação nominal.');}
finally{config(['investment-zone-profile.authenticated_installation'=>false]);}
}
abort_unless($this->preview($actor)['installed'],500,'Esquema nominal inconsistente.');
$result=['migration'=>$plan['migration'],'code_hash'=>$plan['code_hash'],'target'=>$plan['target'],'changed'=>!$plan['installed'],'business_records_changed'=>0,'replayed'=>false,'receipt'=>$data['idempotency_key']];
DB::table('audit_events')->insert(['source_system'=>self::SOURCE,'entity_type'=>'investment-zone-profile','entity_id'=>null,'actor_user_id'=>$actor->id,
'correlation_id'=>$data['idempotency_key'],'action'=>'schema','data_classification'=>'restricted','after_data'=>json_encode(['request_hash'=>$requestHash,'result'=>$result],JSON_THROW_ON_ERROR)]);
return $result;
});
}
}