*/ 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 $historicalIds * @return Collection */ 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; } }