129 lines
7.1 KiB
PHP
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;
|
|
}
|
|
}
|