224 lines
12 KiB
PHP
224 lines
12 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'];
|
|
}
|
|
$data = $request->validate($rules);
|
|
|
|
$versionQuery = DB::table('reference_catalog_versions')->where('catalog_code', $catalog)->where('locale', 'pt-MZ');
|
|
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();
|
|
|
|
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 (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'])) {
|
|
$query->where('sector_id', $data['sector_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']],
|
|
};
|
|
}
|
|
}
|