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

233 lines
13 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Services\CountryCatalogue;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class ReferenceCatalogController extends Controller
{
private const CATALOGS = ['authorities', 'jurisdictions', 'economic-activities', 'countries'];
public function index(Request $request, string $catalog): JsonResponse
{
$catalog = $this->validatedCatalog($catalog);
if ($catalog === 'countries') {
return $this->countries($request);
}
$rules = [
'q' => ['nullable', 'string', 'max:100'],
'version' => ['nullable', 'integer', 'min:1'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'parent_id' => ['nullable', 'uuid'],
'jurisdiction_id' => ['nullable', 'uuid'],
];
if ($catalog === 'jurisdictions') {
$rules['type'] = ['required', 'in:country,province,district,admin_post,locality'];
$rules['parent_id'][] = 'prohibited_if:type,country';
$rules['parent_id'][] = 'required_if:type,district,admin_post,locality';
}
if ($catalog === 'economic-activities') {
$rules['sector_id'] = ['nullable', 'integer', 'exists:sectors,id'];
$rules['origin'] = ['nullable', 'in:portal,cae'];
}
$data = $request->validate($rules);
$cae = $catalog === 'economic-activities' && ($data['origin'] ?? 'portal') === 'cae';
$versionQuery = DB::table('reference_catalog_versions')->where('catalog_code', $cae ? 'economic-activities-cae' : $catalog)->where('locale', 'pt-MZ');
if ($cae && ! app(\App\Services\EconomicReferenceCatalog::class)->available()) { $versionQuery->whereRaw('false'); }
if (isset($data['version'])) {
$versionQuery->where('version_no', $data['version'])->whereIn('status', ['published', 'retired']);
} else {
$versionQuery->where('status', 'published')
->where(fn (Builder $query) => $query->whereNull('valid_from')->orWhere('valid_from', '<=', today()->toDateString()))
->where(fn (Builder $query) => $query->whereNull('valid_to')->orWhere('valid_to', '>', today()->toDateString()));
}
$version = $versionQuery->orderByDesc('version_no')->first();
if (isset($data['version']) && ! $version) {
abort(404, 'Versão publicada do catálogo não encontrada.');
}
if (! $version) {
return response()->json([
'data' => [],
'links' => ['next' => null, 'previous' => null],
'next_page_url' => null,
'prev_page_url' => null,
'current_page' => 1,
'per_page' => $data['per_page'] ?? 20,
'total' => 0,
'last_page' => 1,
'meta' => ['catalog' => $catalog, 'version' => null, 'locale' => 'pt-MZ', 'status' => 'not_published',
'current_page' => 1, 'per_page' => $data['per_page'] ?? 20, 'total' => 0, 'last_page' => 1],
])->header('Cache-Control', 'public, max-age=60');
}
$query = match ($catalog) {
'authorities' => $this->authorities((string) $version->id, $data),
'jurisdictions' => $this->jurisdictions($data),
'economic-activities' => $this->economicActivities((string) $version->id, $data),
};
$page = $query->orderBy('code')->orderBy('id')->paginate($data['per_page'] ?? 20)->withQueryString();
$items = collect($page->items())->map(fn (object $row): array => $this->present($catalog, (array) $row))->values();
if ($cae) {
$paths = app(\App\Services\EconomicReferenceCatalog::class)->activities()->keyBy('id');
$items = $items->map(fn ($row) => [...$row, 'level' => $paths[$row['id']]->level, 'path' => $paths[$row['id']]->path, 'source_version' => $paths[$row['id']]->source_version, 'edition' => 'not_determined', 'divergence' => '600/579 documentalmente aberta']);
}
return response()->json([
'data' => $items,
'links' => ['next' => $page->nextPageUrl(), 'previous' => $page->previousPageUrl()],
'next_page_url' => $page->nextPageUrl(),
'prev_page_url' => $page->previousPageUrl(),
'current_page' => $page->currentPage(),
'per_page' => $page->perPage(),
'total' => $page->total(),
'last_page' => $page->lastPage(),
'meta' => ['catalog' => $catalog, 'version' => (int) $version->version_no, 'locale' => $version->locale,
'source_version' => $version->source_version, 'valid_from' => $version->valid_from, 'valid_to' => $version->valid_to,
'current_page' => $page->currentPage(), 'per_page' => $page->perPage(), 'total' => $page->total(), 'last_page' => $page->lastPage()],
])->header('Cache-Control', 'public, max-age=300')
->header('ETag', 'W/"catalog-'.substr(hash('sha256', $catalog.'|'.$version->version_no.'|'.json_encode($data, JSON_THROW_ON_ERROR)), 0, 32).'"');
}
private function countries(Request $request): JsonResponse
{
$data = $request->validate(['q' => 'nullable|string|max:100', 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:100', 'parent_id' => 'prohibited', 'version' => 'prohibited']);
$term = strtolower(Str::ascii(trim($data['q'] ?? '')));
$items = collect(app(CountryCatalogue::class)->all())
->filter(fn (array $country) => $term === '' || str_contains(strtolower(Str::ascii(implode(' ', $country))), $term))
->values()->map(fn (array $country) => ['id' => $country['code'], 'value' => $country['code'], 'code' => $country['code'], 'name' => $country['name'], 'label' => $country['name']]);
$page = (int) ($data['page'] ?? 1);
$perPage = (int) ($data['per_page'] ?? 50);
$result = new LengthAwarePaginator($items->forPage($page, $perPage)->values(), $items->count(), $perPage, $page, ['path' => $request->url(), 'query' => $request->query()]);
return response()->json([...$result->toArray(), 'meta' => ['catalog' => 'countries', 'source' => 'Local ISO 3166 catalogue', 'sha256' => hash_file('sha256', resource_path('data/iso3166.tab'))]])
->header('Cache-Control', 'public, max-age=300');
}
/** @param array<string,mixed> $data */
private function authorities(string $versionId, array $data): Builder
{
$query = DB::table('authorities as a')->join('organizations as o', 'o.id', '=', 'a.organization_id')
->leftJoin('geo_locations as g', 'g.id', '=', 'a.jurisdiction_id')
->where('a.catalog_version_id', $versionId)->where('a.status', 'published')
->where('o.data_classification', 'public')->whereNull('o.deleted_at')
->where('a.valid_from', '<=', today()->toDateString())
->where(fn (Builder $builder) => $builder->whereNull('a.valid_to')->orWhere('a.valid_to', '>', today()->toDateString()))
->select(['a.id', 'a.code', 'a.authority_type', 'a.label_pt_mz as name', 'a.organization_id', 'a.jurisdiction_id',
'g.name as jurisdiction_name', 'a.version_no']);
if (! empty($data['jurisdiction_id'])) {
$query->where('a.jurisdiction_id', $data['jurisdiction_id']);
}
if (! empty($data['q'])) {
$this->search($query, $data['q'], ['a.code', 'a.label_pt_mz', 'o.name']);
}
return $query;
}
/** @param array<string,mixed> $data */
private function jurisdictions(array $data): Builder
{
$query = DB::table('geo_locations')->where('source_system', 'cartografia-workbook')
->where('data_classification', 'public')->whereNull('deleted_at')->where('location_type', $data['type'])
->select(['id', 'code', 'name', 'parent_id', 'location_type', 'source_code', 'version_no']);
$parentType = ['province' => 'country', 'district' => 'province', 'admin_post' => 'district', 'locality' => 'admin_post'][$data['type']] ?? null;
if ($data['type'] !== 'country' && ! empty($data['parent_id'])) {
$validParent = DB::table('geo_locations')->where('id', $data['parent_id'])->where('location_type', $parentType)
->where('source_system', 'cartografia-workbook')->where('data_classification', 'public')->whereNull('deleted_at')->exists();
if (! $validParent) {
throw ValidationException::withMessages(['parent_id' => 'Seleccione um território pai activo do nível anterior.']);
}
$query->where('parent_id', $data['parent_id']);
} elseif (in_array($data['type'], ['district', 'admin_post', 'locality'], true)) {
throw ValidationException::withMessages(['parent_id' => 'Seleccione primeiro o território pai.']);
}
if (! empty($data['q'])) {
$this->search($query, $data['q'], ['code', 'name', 'source_code']);
}
return $query;
}
/** @param array<string,mixed> $data */
private function economicActivities(string $versionId, array $data): Builder
{
$query = DB::table('economic_activities')->where('catalog_version_id', $versionId)->where('status', 'published')
->where('valid_from', '<=', today()->toDateString())
->where(fn (Builder $builder) => $builder->whereNull('valid_to')->orWhere('valid_to', '>', today()->toDateString()))
->select(['id', 'code', 'label_pt_mz as name', 'parent_id', 'sector_id', 'version_no']);
if (empty($data['version'])) $query->where('id', '!=', \App\Services\SectorOrganization::LEGACY_ACTIVITY_ID);
if (array_key_exists('parent_id', $data) && $data['parent_id'] !== null) {
$query->where('parent_id', $data['parent_id']);
} else {
$query->whereNull('parent_id');
}
if (! empty($data['sector_id'])) {
$sector = \App\Models\Sector::find($data['sector_id']);
$query->whereIn('sector_id', $sector ? \App\Models\Sector::whereIn('slug', \App\Services\SectorOrganization::slugs($sector->slug))->pluck('id') : []);
}
if (! empty($data['q'])) {
$this->search($query, $data['q'], ['code', 'label_pt_mz']);
}
return $query;
}
/** @param list<literal-string> $columns */
private function search(Builder $query, string $search, array $columns): void
{
$term = '%'.addcslashes($search, '%_\\').'%';
$query->where(function (Builder $nested) use ($columns, $term): void {
foreach ($columns as $index => $column) {
if ($index === 0) {
$nested->whereRaw($column.' ILIKE ?', [$term]);
} else {
$nested->orWhereRaw($column.' ILIKE ?', [$term]);
}
}
});
}
/** @return 'authorities'|'jurisdictions'|'economic-activities'|'countries' */
private function validatedCatalog(string $catalog): string
{
abort_unless(in_array($catalog, self::CATALOGS, true), 404);
return $catalog;
}
/**
* @param 'authorities'|'jurisdictions'|'economic-activities' $catalog
* @param array<string, mixed> $row
* @return array<string,mixed>
*/
private function present(string $catalog, array $row): array
{
return match ($catalog) {
'jurisdictions' => ['id' => $row['id'], 'value' => $row['id'], 'code' => $row['code'], 'name' => $row['name'],
'label' => $row['name'], 'locale' => 'pt-MZ', 'parent_id' => $row['parent_id'], 'type' => $row['location_type'],
'source_code' => $row['source_code'], 'version_no' => $row['version_no']],
'economic-activities' => ['id' => $row['id'], 'value' => $row['code'], 'code' => $row['code'], 'name' => $row['name'],
'label' => $row['name'], 'locale' => 'pt-MZ', 'parent_id' => $row['parent_id'], 'sector_id' => $row['sector_id'],
'version_no' => $row['version_no']],
'authorities' => ['id' => $row['id'], 'value' => $row['code'], 'code' => $row['code'], 'name' => $row['name'],
'label' => $row['name'], 'locale' => 'pt-MZ', 'authority_type' => $row['authority_type'],
'organization_id' => $row['organization_id'], 'jurisdiction_id' => $row['jurisdiction_id'],
'jurisdiction_name' => $row['jurisdiction_name'], 'version_no' => $row['version_no']],
};
}
}