Files
Investidor-V2/app/Services/EconomicReferenceCatalog.php
T

171 lines
7.8 KiB
PHP

<?php
namespace App\Services;
use App\Support\CheckedFile;
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';
/**
* @return array<array-key, mixed>
*/
public function manifest(): array
{
$file = database_path('data/economic-references-lot-01.json');
abort_unless(is_file($file) && hash_equals(self::MANIFEST_SHA256, CheckedFile::sha256($file)), 422, 'Manifesto económico ausente ou diferente da revisão delimitada.');
$manifest = json_decode(CheckedFile::read($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();
}
/**
* @param array<array-key, mixed> $historicalIds
* @return Collection<int, \stdClass>
*/
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): \stdClass {
$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;
$option = ['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];
$result = new \stdClass;
foreach ($option as $key => $value) {
$result->{$key} = $value;
}
return $result;
})->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;
}
}