166 lines
7.0 KiB
PHP
166 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class AdministrativeGeography
|
|
{
|
|
public const ISSUES = [
|
|
'blank_designation' => 'Designação vazia',
|
|
'incomplete_code_path' => 'Percurso de códigos incompleto',
|
|
'duplicate_code_path' => 'Código repetido',
|
|
'missing_parent' => 'Nível superior ausente',
|
|
'ambiguous_parent' => 'Nível superior ambíguo',
|
|
'ancestor_requires_review' => 'Ascendente por rever',
|
|
];
|
|
|
|
/** @var array<string,mixed>|null */
|
|
private ?array $data = null;
|
|
|
|
/** @return array<string,mixed> */
|
|
public function catalogue(): array
|
|
{
|
|
if ($this->data === null) {
|
|
$content = file_get_contents(resource_path('data/geography/cartografia-moz.json'));
|
|
abort_unless(is_string($content) && hash_equals((string) config('geography.catalogue_sha256'), hash('sha256', $content)), 503, 'A integridade do catálogo precisa de verificação.');
|
|
$data = json_decode($content, true, 32, JSON_THROW_ON_ERROR);
|
|
abort_unless(is_array($data) && ($data['manifest']['source_sha256'] ?? null) === config('geography.source_sha256'), 503, 'Catálogo geográfico indisponível.');
|
|
$this->data = $data;
|
|
}
|
|
|
|
return $this->data;
|
|
}
|
|
|
|
public function enabled(): bool
|
|
{
|
|
return config('geography.enabled') === true
|
|
&& Schema::hasColumn('suppliers', 'geography_reference')
|
|
&& Schema::hasColumn('tenants', 'geography_reference');
|
|
}
|
|
|
|
public function availableFor(string $table): bool
|
|
{
|
|
return in_array($table, ['tenants', 'suppliers', 'warehouses', 'equipment'], true)
|
|
&& (! in_array($table, ['warehouses', 'equipment'], true) || config('geography.operational_enabled') === true)
|
|
&& $this->enabled() && Schema::hasColumn($table, 'geography_reference');
|
|
}
|
|
|
|
/** @return array<string,string> */
|
|
public function referenceRules(string $table): array
|
|
{
|
|
return ['geography_reference' => $this->availableFor($table) ? 'sometimes|nullable|array:country,catalogue,record_id' : 'prohibited',
|
|
'geography_reference.catalogue' => 'required_with:geography_reference|string|size:64',
|
|
'geography_reference.record_id' => 'required_with:geography_reference|string|regex:/^[0-9]+$/D|max:20'];
|
|
}
|
|
|
|
/** @param array<string,mixed>|null $reference
|
|
* @return array<string,mixed>|null
|
|
*/
|
|
public function canonicalFor(string $table, ?array $reference): ?array
|
|
{
|
|
abort_unless($this->availableFor($table), 503, 'A localização deste cadastro ainda não está instalada.');
|
|
|
|
return $this->canonical($reference);
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function record(string $id): array
|
|
{
|
|
foreach ($this->catalogue()['records'] as $record) {
|
|
if ($record['id'] === $id) {
|
|
return $record;
|
|
}
|
|
}
|
|
|
|
throw ValidationException::withMessages(['geography_reference' => 'Localização não encontrada no catálogo.']);
|
|
}
|
|
|
|
/** @param array<string,mixed>|null $reference
|
|
* @return array<string,mixed>|null
|
|
*/
|
|
public function canonical(?array $reference): ?array
|
|
{
|
|
abort_unless($this->enabled(), 503, 'A localização administrativa ainda não está disponível.');
|
|
if ($reference === null) {
|
|
return null;
|
|
}
|
|
if (($reference['catalogue'] ?? null) !== $this->catalogue()['manifest']['source_sha256']) {
|
|
throw ValidationException::withMessages(['geography_reference' => 'O catálogo foi alterado. Reabra a ficha antes de guardar.']);
|
|
}
|
|
$record = $this->record((string) ($reference['record_id'] ?? ''));
|
|
if (! $record['selectable']) {
|
|
throw ValidationException::withMessages(['geography_reference' => 'Este registo ou a sua hierarquia precisa de revisão. Escolha um nível superior confirmado.']);
|
|
}
|
|
|
|
return ['country' => 'MZ', 'catalogue' => $reference['catalogue'], 'record_id' => $record['id']];
|
|
}
|
|
|
|
/** @param array<string,mixed>|null $reference
|
|
* @return array<string,mixed>|null
|
|
*/
|
|
public function describe(?array $reference): ?array
|
|
{
|
|
if ($reference === null) {
|
|
return null;
|
|
}
|
|
if (($reference['catalogue'] ?? null) !== $this->catalogue()['manifest']['source_sha256']) {
|
|
return ['reference' => $reference, 'path' => [], 'requires_review' => true];
|
|
}
|
|
$record = $this->record((string) $reference['record_id']);
|
|
$path = [$record];
|
|
while ($record['parent_id'] !== null) {
|
|
$record = $this->record($record['parent_id']);
|
|
array_unshift($path, $record);
|
|
}
|
|
|
|
return ['reference' => $reference, 'path' => array_map(fn ($node) => $this->summary($node), $path), 'requires_review' => ! $path[count($path) - 1]['selectable']];
|
|
}
|
|
|
|
/** @param array<string,mixed> $record
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function summary(array $record): array
|
|
{
|
|
return array_intersect_key($record, array_flip(['id', 'name', 'level', 'parent_id']));
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function children(?string $parent, int $page = 1): array
|
|
{
|
|
if ($parent !== null && ! $this->record($parent)['selectable']) {
|
|
throw ValidationException::withMessages(['parent' => 'Hierarquia pendente de revisão.']);
|
|
}
|
|
$records = array_values(array_filter($this->catalogue()['records'], fn ($record) => $record['selectable'] && $record['parent_id'] === $parent));
|
|
usort($records, fn ($a, $b) => strcmp($a['name'], $b['name']) ?: strcmp($a['id'], $b['id']));
|
|
|
|
return ['data' => array_map(fn ($record) => $this->summary($record), array_slice($records, ($page - 1) * 50, 50)), 'page' => $page, 'has_more' => count($records) > $page * 50, 'catalogue' => $this->catalogue()['manifest']['source_sha256']];
|
|
}
|
|
|
|
/** @return list<string> */
|
|
public function descendants(string $id): array
|
|
{
|
|
$root = $this->record($id);
|
|
if (! $root['selectable']) {
|
|
throw ValidationException::withMessages(['location' => 'Localização pendente de revisão.']);
|
|
}
|
|
$prefix = array_slice($root['codes'], 0, $root['level']);
|
|
|
|
return array_values(array_map(fn ($record) => $record['id'], array_filter($this->catalogue()['records'], fn ($record) => $record['selectable'] && array_slice($record['codes'], 0, count($prefix)) === $prefix)));
|
|
}
|
|
|
|
/** @param array<string,mixed> $filters
|
|
* @return list<array<string,mixed>>
|
|
*/
|
|
public function inconsistencies(array $filters): array
|
|
{
|
|
$search = trim($filters['search'] ?? '');
|
|
$issue = $filters['issue'] ?? '';
|
|
|
|
return array_values(array_filter($this->catalogue()['records'], fn ($record) => ! $record['selectable']
|
|
&& ($issue === '' || in_array($issue, $record['issues'], true))
|
|
&& ($search === '' || $record['id'] === $search || mb_stripos($record['name'], $search) !== false)));
|
|
}
|
|
}
|