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

78 lines
5.0 KiB
PHP

<?php
namespace App\Services;
use App\Models\Opportunity;
use Illuminate\Support\Facades\DB;
class OpportunityOriginReview
{
public function fingerprint(): string
{
return hash('sha256', implode('', array_map(fn ($file) => hash_file('sha256', base_path($file)), ['app/Services/OpportunityOriginReview.php', 'app/Models/Opportunity.php', 'app/Http/Controllers/OpportunityOriginReviewController.php', 'resources/js/pages/management/opportunity-origin.tsx'])));
}
/** @return array<string,mixed> */
public function state(int $id): array
{
$record = Opportunity::findOrFail($id);
$events = DB::table('audit_events')->where('source_system', 'opportunity-origin-review')->where('after_data->opportunity_id', $id)->orderByDesc('occurred_at')->orderByDesc('id')->get();
$history = $events->map(function ($event) {
$data = json_decode($event->after_data, true);
return ['id' => $event->id, 'classification' => $data['classification'], 'reason' => $data['reason'], 'author' => $event->actor_user_id, 'date' => $event->occurred_at];
})->all();
return ['id' => $id, 'reference' => $record->reference, 'title' => $record->title, 'description' => $record->description, 'classification' => $history[0]['classification'] ?? 'undetermined', 'history' => $history,
'snapshot_hash' => hash('sha256', json_encode([$record->getRawOriginal(), $history[0]['id'] ?? null], JSON_THROW_ON_ERROR))];
}
/** @return array<string,mixed> */
public function preview(int $id, string $classification, string $reason): array
{
abort_unless(in_array($classification, ['real', 'example', 'undetermined'], true) && trim($reason) !== '', 422, 'Classificação e fundamento são necessários.');
return ['current' => $this->state($id), 'classification' => $classification, 'reason' => trim($reason), 'effect' => $classification === 'example' ? 'Separar do catálogo público, API, pesquisa e mapa; consulta autenticada de demonstração preservada.' : 'Elegível para os filtros públicos editoriais existentes; indeterminado não comprova realidade.', 'relationships_changed' => false, 'ready' => $this->ready()];
}
public function ready(): bool
{
$path = config('opportunity_origin.proof_path', storage_path('app/private/investmoz-v2/opportunity-origin-proof.json'));
if (! is_file($path)) {
return false;
}
$proof = json_decode(file_get_contents($path) ?: '{}', true);
$identity = DB::selectOne('SELECT current_database() AS database,inet_server_port() AS port');
return ($proof['restore_verified'] ?? false) && ($proof['reversal_verified'] ?? false) && ($proof['code_hash'] ?? '') === $this->fingerprint()
&& ($proof['target']['database'] ?? '') === $identity->database && (int) ($proof['target']['port'] ?? 0) === (int) $identity->port
&& strtotime($proof['verified_at'] ?? '') >= time() - 86400 && is_file($proof['dump_path'] ?? '') && hash_file('sha256', $proof['dump_path']) === ($proof['sha256'] ?? null);
}
/** @return array<string,mixed> */
public function apply(int $id, string $classification, string $reason, string $snapshotHash, int $actor): array
{
$preview = $this->preview($id, $classification, $reason);
abort_unless($preview['ready'], 422, 'Prova atual de backup/restauro e reversão necessária.');
if (app()->environment('testing')) {
$identity = DB::selectOne('SELECT current_database() AS database,inet_server_port() AS port');
abort_unless(str_ends_with($identity->database, '_test') && (int) $identity->port === 55441, 409, 'QA exige base isolada.');
}
return DB::transaction(function () use ($id, $classification, $reason, $snapshotHash, $actor) {
DB::select('SELECT pg_advisory_xact_lock(91026, ?)', [$id]);
DB::table('opportunities')->where('id', $id)->lockForUpdate()->first();
$current = $this->state($id);
if ($current['classification'] === $classification && ($current['history'][0]['reason'] ?? null) === trim($reason)) {
return ['replayed' => true, 'changed' => 0];
}
abort_unless(hash_equals($current['snapshot_hash'], $snapshotHash), 409, 'Registo/decisão alterado; reveja a pré-visualização.');
DB::table('audit_events')->insert(['source_system' => 'opportunity-origin-review', 'entity_type' => 'opportunity_origin', 'action' => 'origin_classification_decided', 'actor_user_id' => $actor, 'data_classification' => 'internal', 'occurred_at' => DB::raw('clock_timestamp()'),
'before_data' => json_encode(['classification' => $current['classification']], JSON_THROW_ON_ERROR),
'after_data' => json_encode(['opportunity_id' => $id, 'classification' => $classification, 'reason' => trim($reason), 'reference' => $current['reference'], 'relationships_changed' => false], JSON_THROW_ON_ERROR)]);
return ['replayed' => false, 'changed' => 1];
});
}
}