Files
Investidor-V2/tools/portal-geo-reconciliation-qa.php
nala 01405cbf8a
tests / ci (push) Canceled after 0s
oportunidades 5
2026-10-10 14:15:28 +02:00

98 lines
5.9 KiB
PHP

<?php
// Read-only snapshot of the guarded QA restore; no active connection or catalogue writes.
use App\Models\User;
use App\Services\TerritoryGeometryImport;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
$root = dirname(__DIR__);
require $root.'/vendor/autoload.php';
require __DIR__.'/portal-qa-environment.php';
$target = portalQaEnvironment();
$app = require $root.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
portalQaAssertTarget($target);
if (($argv[1] ?? '') === 'preview') {
config(['geometry-import.enabled' => true, 'portal.managers' => ['geo-manager-qa@example.invalid']]);
$actor = User::where('email', 'geo-manager-qa@example.invalid')->firstOrFail();
$plans = DB::transaction(function () use ($actor) {
DB::statement('SET TRANSACTION READ ONLY');
return array_map(fn ($level) => app(TerritoryGeometryImport::class)->preview($actor, $level), [1, 2, 3]);
});
foreach ($plans as $plan) {
echo json_encode(['level' => $plan['level'], 'expected' => $plan['expected'], 'resolved' => $plan['resolved'], 'pending' => $plan['pending'], 'scan_ready' => $plan['scan_ready'],
'errors' => array_values(array_filter($plan['rows'], fn ($row) => str_contains($row['status'], 'conflict') || $row['status'] === 'invalid_geometry'))], JSON_THROW_ON_ERROR).PHP_EOL;
}
exit;
}
$snapshot = DB::transaction(function () {
DB::statement('SET TRANSACTION READ ONLY');
return [
'locations' => DB::table('geo_locations')->orderBy('location_type')->orderBy('name')->get(),
'geometries' => DB::table('geo_geometries')->selectRaw("id,external_id,geo_location_id,source_system,properties,data_classification,version_no,deleted_at,encode(ST_AsEWKB(geom),'hex') as geometry_bytes")->orderBy('id')->get(),
];
});
$snapshot['target'] = $target;
$snapshot['captured_at'] = gmdate('c');
$snapshot['catalogue_hash'] = hash('sha256', json_encode($snapshot['locations'], JSON_THROW_ON_ERROR));
$snapshot['geometries_hash'] = hash('sha256', json_encode($snapshot['geometries'], JSON_THROW_ON_ERROR));
$snapshot['active_writes'] = 0;
if (($argv[1] ?? '') === 'verify') {
$baseline = json_decode(file_get_contents(storage_path('app/private/investmoz-v2/portal-audit/geometry-reconciliation-baseline.json')), true, 512, JSON_THROW_ON_ERROR);
$oldLocations = json_encode($baseline['locations'], JSON_THROW_ON_ERROR);
$locationsPreserved = hash_equals(hash('sha256', $oldLocations), $snapshot['catalogue_hash']);
$current = [];
foreach ($snapshot['geometries'] as $row) {
$current[$row->id] = (array) $row;
}
foreach ($baseline['geometries'] as $row) {
if (! isset($current[$row['id']]) || json_encode($row, JSON_THROW_ON_ERROR) !== json_encode($current[$row['id']], JSON_THROW_ON_ERROR)) {
throw new RuntimeException('An original QA geometry changed; preservation proof refused.');
}
unset($current[$row['id']]);
}
if (! $locationsPreserved) {
throw new RuntimeException('The existing QA catalogue changed; preservation proof refused.');
}
$locationTypes = [];
foreach ($snapshot['locations'] as $row) {
$locationTypes[$row->id] = $row->location_type;
}
$new = [];
$counts = [];
foreach ($snapshot['geometries'] as $row) {
if (! $row->deleted_at) {
$type = $locationTypes[$row->geo_location_id];
$counts[$type] = ($counts[$type] ?? 0) + 1;
}
if (isset($current[$row->id])) {
$properties = json_decode($row->properties, true, 512, JSON_THROW_ON_ERROR);
if ($row->data_classification !== 'internal' || $row->deleted_at || ($properties['investment_site'] ?? null) !== false) {
throw new RuntimeException('Unexpected disclosure/lifecycle/purpose of new QA boundary.');
}
$new[] = ['id' => $row->id, 'location_id' => $row->geo_location_id, 'type' => $locationTypes[$row->geo_location_id], 'external_id' => $row->external_id, 'properties' => $properties];
}
}
$imports = DB::transaction(function () {
DB::statement('SET TRANSACTION READ ONLY');
return DB::table('audit_events')->where('source_system', 'geometry-import')->where('action', 'geometry_import_applied')->orderBy('occurred_at')->get(['entity_id', 'actor_user_id', 'occurred_at', 'after_data']);
});
$proof = ['captured_at' => gmdate('c'), 'target' => $target, 'catalogue_count' => count($snapshot['locations']), 'catalogue_preserved' => true,
'catalogue_hash' => $snapshot['catalogue_hash'], 'original_geometries_preserved' => count($baseline['geometries']), 'new_geometries' => $new,
'geometries_by_level' => $counts, 'total_active' => array_sum($counts), 'authenticated_import_events' => $imports,
'published' => false, 'read_only_verification' => true, 'interface_observed' => false, 'active_writes' => 0];
file_put_contents(storage_path('app/private/investmoz-v2/portal-audit/geometry-crosswalk-preservation-qa.json'), json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
echo json_encode(['target' => $target, 'catalogue_preserved' => true, 'original_geometries_preserved' => count($baseline['geometries']), 'new_geometries' => count($new), 'counts' => $counts, 'total' => array_sum($counts), 'published' => false, 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;
exit;
}
$file = storage_path('app/private/investmoz-v2/portal-audit/geometry-reconciliation-baseline.json');
if (is_file($file)) {
$file = dirname($file).'/geometry-reconciliation-snapshot-'.gmdate('Ymd_His').'.json';
}
file_put_contents($file, json_encode($snapshot, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
echo json_encode(['target' => $target, 'locations' => count($snapshot['locations']), 'geometries' => count($snapshot['geometries']), 'catalogue_hash' => $snapshot['catalogue_hash'], 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;