Files
Investidor-V2/app/Services/EconomicReferenceCatalog.php
nala cf003c8623
tests / ci (push) Canceled after 0s
versao nrgocios 4
2026-10-18 15:57:06 +02:00

129 lines
7.1 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/** Shared read contract. It deliberately has no dependency on the installation controller/service. */
class EconomicReferenceCatalog
{
public const LOT = 'lot-01';
public const SOURCE = 'economic-reference-lot-01';
public const MANIFEST_SHA256 = 'ba1bb3406fd55e39c19fd2a2755f3dfc2a1160b67eee8b3b6e12cd43f4223b04';
public function manifest(): array
{
$file = database_path('data/economic-references-lot-01.json');
abort_unless(is_file($file) && hash_equals(self::MANIFEST_SHA256, hash_file('sha256', $file)), 422, 'Manifesto económico ausente ou diferente da revisão delimitada.');
$manifest = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
abort_unless(($manifest['lot'] ?? '') === self::LOT && count($manifest['activities'] ?? []) === 5
&& count($manifest['chains'] ?? []) === 5 && count($manifest['links'] ?? []) === 4, 422, 'Lote económico fora do âmbito.');
return $manifest;
}
public function schemaAvailable(): bool
{
return Schema::hasColumn('economic_activities', 'metadata');
}
public function publicationState(): string
{
$event = DB::table('audit_events')->where('source_system', self::SOURCE)->whereIn('action', ['publish', 'withdraw'])
->orderByDesc('occurred_at')->orderByDesc('id')->first(['action']);
return $event?->action === 'publish' ? 'published' : ($event ? 'withdrawn' : 'private');
}
public function available(): bool
{
if (! $this->schemaAvailable() || $this->publicationState() !== 'published') { return false; }
return DB::table('reference_catalog_versions')->where('id', $this->manifest()['catalog']['id'])->where('status', 'published')->exists();
}
public function constrain(Builder $query, string $kind): Builder
{
$manifest = $this->manifest();
if ($kind === 'activity') $query->where('id', '!=', SectorOrganization::LEGACY_ACTIVITY_ID);
if (in_array($kind, ['activity', 'chain'], true) && ! $this->available()) {
$query->whereNotIn('id', array_column($manifest[$kind === 'activity' ? 'activities' : 'chains'], 'id'));
}
return $query;
}
/** A withdrawn published reference can be retained verbatim, never introduced as a new choice. */
public function historicalTarget(string $kind, string $id): bool
{
if ($kind === 'activity' && $id === SectorOrganization::LEGACY_ACTIVITY_ID) return DB::table('economic_activities')->where('id', $id)->where('sector_id', 2)->where('status', 'published')->exists();
if (! in_array($kind, ['activity', 'chain'], true) || ! $this->schemaAvailable()) { return false; }
$manifest = $this->manifest();
if (! in_array($id, array_column($manifest[$kind === 'activity' ? 'activities' : 'chains'], 'id'), true)) { return false; }
if (! DB::table('audit_events')->where('source_system', self::SOURCE)->where('action', 'publish')->exists()) { return false; }
return $kind === 'activity'
? DB::table('economic_activities')->where('id', $id)->where('status', 'published')->exists()
: DB::table('value_chains')->where('id', $id)->whereNull('deleted_at')->exists();
}
public function activities(array $historicalIds = []): Collection
{
$rows = DB::table('economic_activities')->where('status', 'published')->get();
$allowed = $this->constrain(DB::table('economic_activities')->where('status', 'published'), 'activity')->pluck('id')->all();
// A used published UUID may still be shown after withdrawal, together with its exact ancestors.
$byId = $rows->keyBy('id');
foreach ($historicalIds as $id) {
$seen = [];
while (isset($byId[$id]) && ! isset($seen[$id])) {
$allowed[] = $id; $seen[$id] = true; $id = $byId[$id]->parent_id;
}
}
$available = $this->available();
return $rows->filter(fn ($r) => in_array($r->id, $allowed, true))->map(function ($row) use ($byId, $available) {
$meta = property_exists($row, 'metadata') ? json_decode($row->metadata, true) : [];
$path = []; $current = $row; $seen = [];
while ($current && ! isset($seen[$current->id])) {
$seen[$current->id] = true;
array_unshift($path, ['id' => $current->id, 'code' => $current->code, 'label' => $current->label_pt_mz]);
$current = $current->parent_id ? ($byId[$current->parent_id] ?? null) : null;
}
$isLot = ($meta['lot'] ?? '') === self::LOT;
return (object) ['id' => $row->id, 'parent_id' => $row->parent_id, 'sector_id' => $row->sector_id,
'label_pt_mz' => $row->label_pt_mz, 'catalog_version_id' => $row->catalog_version_id, 'code' => $row->code,
'level' => $meta['level'] ?? null, 'path' => $path, 'display_label' => implode(' → ', array_column($path, 'code')).' — '.$row->label_pt_mz,
'source_version' => $meta['source_version'] ?? null, 'edition' => $meta['edition'] ?? null,
'historical_only' => $isLot && ! $available];
})->values();
}
public function validHierarchy(int $sectorId, ?string $subsectorId, ?string $activityId, bool $historical = false): bool
{
if (! $subsectorId || ! $activityId) { return false; }
$sector = DB::table('sectors')->where('id', $sectorId)->whereNull('deleted_at')->where('is_active', true)->exists();
$subsector = DB::table('economic_activities')->where('id', $subsectorId)->where('status', 'published')->first();
$activity = DB::table('economic_activities')->where('id', $activityId)->where('status', 'published')->first();
if (! $sector || ! $subsector || ! $activity || $activity->catalog_version_id !== $subsector->catalog_version_id) { return false; }
$manifest = $this->manifest();
if ($activity->catalog_version_id !== $manifest['catalog']['id']) {
$root = $subsector->parent_id ? DB::table('economic_activities')->where('id', $subsector->parent_id)->where('status', 'published')->first() : null;
return $root && (int) $root->sector_id === $sectorId && $activity->parent_id === $subsector->id;
}
if ((! $this->available() && ! $historical) || $sectorId !== (int) $manifest['existing']['sector']['id']
|| $subsector->code !== '01' || $activity->code !== '01120') { return false; }
$current = $activity; $codes = []; $seen = [];
while ($current && ! isset($seen[$current->id])) {
$seen[$current->id] = true; array_unshift($codes, $current->code);
$current = $current->parent_id ? DB::table('economic_activities')->where('id', $current->parent_id)->where('catalog_version_id', $activity->catalog_version_id)->where('status', 'published')->first() : null;
}
return $codes === ['A', '01', '011', '0112', '01120'] && (int) $subsector->sector_id === $sectorId && (int) $activity->sector_id === $sectorId;
}
}