314 lines
25 KiB
PHP
314 lines
25 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Jobs\ScanDocumentForMalware;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Storage;
|
||
use Illuminate\Support\Str;
|
||
use RuntimeException;
|
||
use Throwable;
|
||
|
||
class ImportLegalSourceArchive extends Command
|
||
{
|
||
protected $signature = 'documents:import-legal-source-archive {--apply : Copy verified archive files into private storage and register draft metadata}';
|
||
|
||
protected $description = 'Idempotently imports the known local legal-source PDFs as private, unpublished library drafts';
|
||
|
||
public function handle(): int
|
||
{
|
||
$records = $this->records();
|
||
$apply = (bool) $this->option('apply');
|
||
$disk = Storage::disk('local');
|
||
$dispatch = [];
|
||
$failures = 0;
|
||
|
||
foreach ($records as $record) {
|
||
$sourcePath = base_path($record['archive_path']);
|
||
$realSource = realpath($sourcePath);
|
||
$approvedRoots = array_filter([
|
||
realpath(base_path('docs/investmoz/simulations/source-archive')),
|
||
realpath(storage_path('app/private/geo-ine-source-20260928')),
|
||
]);
|
||
if (! $realSource && ($record['optional'] ?? false)) {
|
||
$this->warn($record['external_id'].': approved local source copy is not present; skipped without changing records.');
|
||
|
||
continue;
|
||
}
|
||
$withinApprovedRoot = $realSource && collect($approvedRoots)->contains(fn (string $root) =>
|
||
str_starts_with(strtolower($realSource), strtolower($root.DIRECTORY_SEPARATOR)));
|
||
if (! $realSource || ! $withinApprovedRoot || ! is_file($realSource)) {
|
||
$this->error($record['external_id'].': archive file missing or outside the approved source archive.');
|
||
$failures++;
|
||
|
||
continue;
|
||
}
|
||
|
||
$hash = hash_file('sha256', $realSource);
|
||
$size = filesize($realSource);
|
||
$header = file_get_contents($realSource, false, null, 0, 5);
|
||
if (! is_string($hash) || ! hash_equals($record['expected_sha256'], strtolower($hash)) || $size !== $record['size_bytes'] || $header !== '%PDF-') {
|
||
$this->error($record['external_id'].': checksum, size, or PDF signature differs from the reviewed manifest.');
|
||
$failures++;
|
||
|
||
continue;
|
||
}
|
||
|
||
$identity = hash('sha256', mb_strtolower(implode('|', array_map('trim', [$record['issuer'], $record['document_number'], $record['document_date'], $record['language'], $record['version_label']]))));
|
||
$existing = DB::table('documents')->where('sha256', $hash)->whereNull('deleted_at')->first();
|
||
if ($existing) {
|
||
$storedPath = $disk->path($existing->storage_key);
|
||
$storedReal = realpath($storedPath);
|
||
$storageRoot = realpath($disk->path(''));
|
||
if (! $storedReal || ! $storageRoot || ! str_starts_with(strtolower($storedReal), strtolower($storageRoot.DIRECTORY_SEPARATOR))
|
||
|| ! is_file($storedReal) || ! hash_equals($hash, (string) hash_file('sha256', $storedReal))) {
|
||
$this->error($record['external_id'].': matching hash exists but the stored copy could not be verified; no changes made.');
|
||
$failures++;
|
||
|
||
continue;
|
||
}
|
||
|
||
$resource = DB::table('portal_resources')->where('document_id', $existing->id)->first();
|
||
if (! $resource) {
|
||
$this->error($record['external_id'].': document hash already exists without its expected library record; manual reconciliation required.');
|
||
$failures++;
|
||
|
||
continue;
|
||
}
|
||
if (! hash_equals($identity, (string) $resource->document_identity_key)) {
|
||
$this->error($record['external_id'].': identical bytes exist under a different document identity; manual reconciliation required.');
|
||
$failures++;
|
||
|
||
continue;
|
||
}
|
||
|
||
$missingEvidence = collect($record['associations'])->filter(fn (array $association) => $association['target_type'] === 'requirement')
|
||
->reject(fn (array $association) => DB::table('portal_resource_links')->where('resource_id', $resource->id)
|
||
->where('target_type', 'requirement')->where('target_key', $association['target_key'])->exists())->values();
|
||
if ($apply && $missingEvidence->isNotEmpty()) {
|
||
DB::transaction(function () use ($resource, $missingEvidence): void {
|
||
$locked = DB::table('portal_resources')->where('id', $resource->id)->lockForUpdate()->first();
|
||
$now = now();
|
||
foreach ($missingEvidence as $association) {
|
||
DB::table('portal_resource_links')->insertOrIgnore(['resource_id' => $resource->id, ...$association,
|
||
'created_at' => $now, 'updated_at' => $now]);
|
||
}
|
||
$inserted = DB::table('portal_resource_links')->where('resource_id', $resource->id)->where('target_type', 'requirement')
|
||
->whereIn('target_key', $missingEvidence->pluck('target_key'))->count();
|
||
if ($inserted > 0) {
|
||
$version = (int) $locked->metadata_version + 1;
|
||
DB::table('portal_resources')->where('id', $resource->id)->update(['metadata_version' => $version, 'updated_at' => $now]);
|
||
$links = DB::table('portal_resource_links')->where('resource_id', $resource->id)->orderBy('id')->get();
|
||
$snapshot = DB::table('portal_resources')->where('id', $resource->id)->first();
|
||
DB::table('portal_resource_revisions')->insert(['resource_id' => $resource->id, 'revision_no' => $version,
|
||
'change_type' => 'evidence_linked',
|
||
'snapshot' => json_encode(['resource' => $snapshot, 'associations' => $links], JSON_THROW_ON_ERROR),
|
||
'actor_user_id' => null, 'created_at' => $now]);
|
||
}
|
||
});
|
||
}
|
||
|
||
$this->line($record['external_id'].' | reused/reconciled | document='.$existing->id.' | published='.($resource->published ? 'yes' : 'no'));
|
||
continue;
|
||
}
|
||
|
||
$identityConflict = DB::table('portal_resources')->where('document_identity_key', $identity)->exists();
|
||
if ($identityConflict) {
|
||
$this->error($record['external_id'].': document identity exists with different bytes; manual version reconciliation required.');
|
||
$failures++;
|
||
|
||
continue;
|
||
}
|
||
|
||
$this->line($record['external_id'].' | '.($apply ? 'importing private draft' : 'would import')." | sha256={$hash} | bytes={$size}");
|
||
if (! $apply) {
|
||
continue;
|
||
}
|
||
|
||
$documentId = (string) Str::uuid();
|
||
$storageKey = 'portal-documents/'.$documentId;
|
||
$sourceHandle = fopen($realSource, 'rb');
|
||
if (! is_resource($sourceHandle)) {
|
||
$this->error($record['external_id'].': source could not be opened.');
|
||
$failures++;
|
||
|
||
continue;
|
||
}
|
||
try {
|
||
$stored = $disk->put($storageKey, $sourceHandle);
|
||
} finally {
|
||
fclose($sourceHandle);
|
||
}
|
||
$storedPath = $disk->path($storageKey);
|
||
$storedHash = $stored && is_file($storedPath) ? hash_file('sha256', $storedPath) : false;
|
||
if (! $stored || ! is_string($storedHash) || ! hash_equals($hash, $storedHash)) {
|
||
$disk->delete($storageKey);
|
||
$this->error($record['external_id'].': private copy failed the post-copy checksum; no database record created.');
|
||
$failures++;
|
||
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
$result = DB::transaction(function () use ($record, $documentId, $storageKey, $hash, $size, $identity): array {
|
||
DB::select('SELECT pg_advisory_xact_lock(hashtext(?))', ['legal-source-archive:'.$hash]);
|
||
if (DB::table('documents')->where('sha256', $hash)->whereNull('deleted_at')->exists()) {
|
||
throw new RuntimeException('A matching document was registered concurrently; rerun the import to reconcile it.');
|
||
}
|
||
if (DB::table('portal_resources')->where('document_identity_key', $identity)->exists()) {
|
||
throw new RuntimeException('A matching document identity was registered concurrently; manual reconciliation required.');
|
||
}
|
||
|
||
$now = now();
|
||
DB::table('documents')->insert([
|
||
'id' => $documentId, 'source_system' => 'legal-source-archive', 'external_id' => $record['external_id'],
|
||
'version_no' => 1, 'data_classification' => 'internal', 'file_name' => $record['file_name'],
|
||
'media_type' => 'application/pdf', 'storage_key' => $storageKey, 'sha256' => $hash,
|
||
'status' => 'quarantined', 'size_bytes' => $size, 'issuer' => $record['issuer'],
|
||
'original_source' => $record['source_url'], 'issued_on' => $record['document_date'],
|
||
'uploaded_by' => null, 'rights_status' => 'unconfirmed', 'malware_scan_status' => 'pending',
|
||
'created_at' => $now, 'updated_at' => $now,
|
||
]);
|
||
$resourceId = DB::table('portal_resources')->insertGetId([
|
||
'document_id' => $documentId, 'title' => $record['title'], 'category' => $record['category'] ?? 'legislation',
|
||
'topic' => $record['topic'], 'legislation' => $record['is_legislation'] ?? true, 'essential' => false, 'published' => false,
|
||
'source_reference' => $record['source_reference'], 'approved_by' => null, 'description' => $record['description'],
|
||
'document_type' => $record['document_type'] ?? 'legislation', 'document_number' => $record['document_number'],
|
||
'document_date' => $record['document_date'], 'source_url' => $record['source_url'],
|
||
'language' => $record['language'], 'version_label' => $record['version_label'],
|
||
'territorial_scope' => 'national', 'source_confirmation' => $record['source_confirmation'],
|
||
'legal_status' => $record['legal_status'], 'rights_status' => 'unconfirmed', 'last_reviewed_at' => null,
|
||
'document_identity_key' => $identity, 'metadata_version' => 1, 'created_by' => null,
|
||
'last_reviewed_by' => null, 'independent_review' => false, 'created_at' => $now, 'updated_at' => $now,
|
||
]);
|
||
|
||
foreach ($record['associations'] as $association) {
|
||
$association['review_status'] ??= 'pending';
|
||
DB::table('portal_resource_links')->insertOrIgnore([
|
||
'resource_id' => $resourceId, ...$association, 'created_at' => $now, 'updated_at' => $now,
|
||
]);
|
||
}
|
||
$links = DB::table('portal_resource_links')->where('resource_id', $resourceId)->orderBy('id')->get();
|
||
$resource = DB::table('portal_resources')->where('id', $resourceId)->first();
|
||
DB::table('portal_resource_revisions')->insert([
|
||
'resource_id' => $resourceId, 'revision_no' => 1, 'change_type' => 'created',
|
||
'snapshot' => json_encode(['resource' => $resource, 'associations' => $links], JSON_THROW_ON_ERROR),
|
||
'actor_user_id' => null, 'created_at' => $now,
|
||
]);
|
||
|
||
return ['document_id' => $documentId, 'resource_id' => $resourceId];
|
||
});
|
||
$dispatch[] = $result['document_id'];
|
||
$this->info($record['external_id'].' | created private draft | document='.$result['document_id'].' | resource='.$result['resource_id']);
|
||
} catch (Throwable $exception) {
|
||
$disk->delete($storageKey);
|
||
$this->error($record['external_id'].': import rolled back ('.$exception->getMessage().').');
|
||
$failures++;
|
||
}
|
||
}
|
||
|
||
foreach ($dispatch as $documentId) {
|
||
ScanDocumentForMalware::dispatch($documentId)->afterCommit();
|
||
}
|
||
|
||
$this->line($apply ? 'New malware scans queued: '.count($dispatch).'.' : 'Preview only; no files or database rows were changed.');
|
||
|
||
return $failures > 0 ? self::FAILURE : self::SUCCESS;
|
||
}
|
||
|
||
/** @return array<int, array<string, mixed>> */
|
||
private function records(): array
|
||
{
|
||
$simulatorEvidence = fn (string $basis): array => [
|
||
['target_type' => 'module', 'target_key' => 'simulator', 'relationship' => 'supports', 'applicability' => 'evidence',
|
||
'condition_text' => 'Consulta documental; a fonte não determina por si só a regra fiscal aplicável.', 'basis_reference' => $basis],
|
||
];
|
||
|
||
return [
|
||
[
|
||
'external_id' => 'inm-br250-supplement-2-law-22-2022-v1',
|
||
'archive_path' => 'docs/investmoz/simulations/source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf',
|
||
'file_name' => 'Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf',
|
||
'expected_sha256' => 'ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53', 'size_bytes' => 1834284,
|
||
'title' => 'Lei n.º 22/2022, de 28 de Dezembro — alteração ao Código do IVA (republicação)',
|
||
'document_number' => 'Lei n.º 22/2022', 'document_date' => '2022-12-28', 'issuer' => 'Assembleia da República',
|
||
'language' => 'pt', 'version_label' => '2.º Suplemento do BR n.º 250/2022; diploma alterador, não consolidação integral',
|
||
'topic' => 'iva', 'legal_status' => 'amended', 'source_confirmation' => 'official_confirmed',
|
||
'source_url' => 'https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-suplemento-n%C2%BA-2-de-281222-pag-2252-25-42-br-n%C2%BA-250-boletim-da',
|
||
'source_reference' => 'INM, 2.º Suplemento do BR n.º 250 e catálogo oficial AT, registo Lei 22 IVA 2022. O ficheiro local foi descarregado novamente do endpoint AT em 2026-10-06; tamanho e SHA-256 coincidem exactamente. O artigo 17(1) está citado na revisão documental.',
|
||
'description' => 'Diploma alterador do CIVA, sem afirmar consolidação ou vigência integral actual. O texto oficial está identificado; direitos de reprodução da composição PDF continuam por rever, pelo que o ficheiro permanece privado.',
|
||
'associations' => [...$simulatorEvidence('Lei n.º 22/2022, art. 17(1), conforme revisão documental em docs/investmoz/simulations/primary-text-review-2025-2026.md; consultar alterações posteriores antes de determinar o período.'),
|
||
['target_type' => 'theme', 'target_key' => 'iva', 'relationship' => 'cites', 'applicability' => 'evidence', 'condition_text' => null,
|
||
'basis_reference' => 'Lei n.º 22/2022, art. 17(1); efeito temporal e alterações posteriores devem ser considerados.'],
|
||
['target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_22_2022', 'relationship' => 'cites', 'applicability' => 'evidence', 'review_status' => 'confirmed',
|
||
'condition_text' => 'Evidência documental; a citação não activa nem determina cálculos fiscais.', 'basis_reference' => 'Lei n.º 22/2022, art. 17(1), p. 3 do PDF oficial arquivado; a redacção diz 16%. Conferência da citação concluída; aplicação temporal e aprovação fiscal continuam separadas.']],
|
||
],
|
||
[
|
||
'external_id' => 'inm-br248-supplement-1-2025-v1',
|
||
'archive_path' => 'docs/investmoz/simulations/source-archive/BR-248-I-Suplemento-1-2025.pdf',
|
||
'file_name' => 'BR-248-I-Suplemento-1-2025.pdf',
|
||
'expected_sha256' => 'ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251', 'size_bytes' => 440018,
|
||
'title' => 'Boletim da República n.º 248/2025 — Suplemento n.º 1',
|
||
'document_number' => 'BR n.º 248/2025, Suplemento n.º 1', 'document_date' => '2025-12-29', 'issuer' => 'Imprensa Nacional de Moçambique',
|
||
'language' => 'pt', 'version_label' => 'Suplemento n.º 1; edição arquivada com 21 páginas',
|
||
'topic' => 'fiscalidade', 'legal_status' => 'unknown', 'source_confirmation' => 'official_confirmed',
|
||
'source_url' => 'https://inm.gov.mz/pt-br/content/suplemento-n%C2%BA-1-de-291225-pag-2180-1-20-br-n%C2%BA-248-boletim-da-rep%C3%BAblica-i-serie',
|
||
'source_reference' => 'Ficha INM do Suplemento n.º 1 do BR 248 e catálogo oficial AT. O ficheiro local foi descarregado novamente do endpoint WebDAV AT em 2026-10-06; tamanho e SHA-256 coincidem exactamente. Inclui diplomas fiscais identificados no registo de fontes; artigo, vigência e situação de cada lei devem ser consultados individualmente.',
|
||
'description' => 'Edição que reúne diplomas sobre ICE, Pauta Aduaneira, ISPC, IVA, IRPS e IRPC. O estado de vigência é mantido por confirmar por diploma e dispositivo. O texto oficial está identificado; direitos de reprodução da composição PDF continuam por rever, pelo que o ficheiro permanece privado.',
|
||
'associations' => [...$simulatorEvidence('BR n.º 248/2025, Suplemento n.º 1; esta ligação é evidência documental, não confirmação de vigência ou obrigação.'),
|
||
...array_map(fn (string $theme) => ['target_type' => 'theme', 'target_key' => $theme, 'relationship' => 'cites', 'applicability' => 'evidence', 'condition_text' => null,
|
||
'review_status' => 'pending', 'basis_reference' => 'Associação temática antiga; artigo e alcance por diploma ainda não foram especificados.'],
|
||
['ice', 'aduaneiro', 'ispc', 'iva', 'irps', 'irpc']),
|
||
...array_map(fn (array $item) => ['target_type' => 'requirement', 'target_key' => $item[0], 'relationship' => 'cites',
|
||
'applicability' => 'evidence', 'review_status' => 'confirmed', 'condition_text' => 'Citação localizada no texto primário; não confirma interpretação, vigência integral, elegibilidade nem regra executável.',
|
||
'basis_reference' => $item[1].' — '.$item[2].' BR n.º 248/2025, Suplemento n.º 1, Lei de 29-12-2025.'], [
|
||
['fiscal.ice.lei_7_2025', 'Lei n.º 7/2025 (ICE)', 'arts. 1–5 e tabela anexa; pp. 1–2. Art. 1 prorroga as taxas para 2027; art. 5 indica entrada em vigor em 01-01-2026.'],
|
||
['fiscal.customs.lei_8_2025', 'Lei n.º 8/2025 (Pauta Aduaneira)', 'art. 1 altera as Instruções Preliminares, arts. 21(2), 22 e 27; arts. 2–5 e tabela anexa; pp. 2–7.'],
|
||
['fiscal.ispc.lei_9_2025', 'Lei n.º 9/2025 (ISPC)', 'arts. 1–7 e republicação anexa da Lei n.º 5/2009; pp. 8–11.'],
|
||
['fiscal.iva.lei_10_2025', 'Lei n.º 10/2025 (IVA)', 'arts. 1–4; alterações do CIVA identificadas no diploma, incluindo arts. 18, 19, 21, 23, 25-A, 27 e 32–34; pp. 12–15.'],
|
||
['fiscal.irps.lei_11_2025', 'Lei n.º 11/2025 (IRPS)', 'arts. 1–4; alterações do CIRPS, incluindo arts. 21, 22, 48, 50, 54-A, 57, 65 e 65-A; pp. 16–18.'],
|
||
['fiscal.irpc.lei_12_2025', 'Lei n.º 12/2025 (IRPC)', 'arts. 1–4; alterações do CIRPC, incluindo arts. 3, 5, 20, 62, 67, 75 e aditamento do art. 61-A; pp. 19–20.'],
|
||
]),
|
||
],
|
||
],
|
||
[
|
||
'external_id' => 'inm-br89-series-1-2017-iva-rectification-v1',
|
||
'archive_path' => 'docs/investmoz/simulations/source-archive/BR-89-I-Serie-2017-06-08.pdf',
|
||
'file_name' => 'BR-89-I-Serie-2017-06-08.pdf',
|
||
'expected_sha256' => '11f0cab15eb6e51010952203ffa24fdc533a24fd642e19525fe4f46b17a1f5e5', 'size_bytes' => 9456358,
|
||
'title' => 'Boletim da República n.º 89, I Série (08-06-2017) — aviso de rectificação da Lei n.º 13/2016',
|
||
'document_number' => 'BR n.º 89/2017', 'document_date' => '2017-06-08', 'issuer' => 'Imprensa Nacional de Moçambique',
|
||
'language' => 'pt', 'version_label' => 'Exemplar local de 16 páginas; autenticidade binária pendente',
|
||
'topic' => 'iva', 'legal_status' => 'unknown', 'source_confirmation' => 'identified',
|
||
'source_url' => 'https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-br-n%C2%BA-89-de-080617-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-569',
|
||
'source_reference' => 'Ficha oficial INM identifica o aviso de rectificação da Lei n.º 13/2016 na p. 569. O exemplar local não tem assinatura digital validável; cadeia de custódia e efeito temporal por confirmar, conforme docs/investmoz/simulations/official-source-register.md.',
|
||
'description' => 'Aviso de rectificação ligado ao CIVA de 2016. A identidade do aviso é corroborada pela ficha INM; autenticidade do ficheiro local, anexos corrigidos e efeito temporal permanecem por confirmar. Cópia mantida em privado.',
|
||
'associations' => [...$simulatorEvidence('Ficha INM: aviso de rectificação da Lei n.º 13/2016, p. 569; exemplar local e efeito temporal por confirmar.'),
|
||
['target_type' => 'theme', 'target_key' => 'iva', 'relationship' => 'cites', 'applicability' => 'evidence', 'condition_text' => null, 'review_status' => 'pending',
|
||
'basis_reference' => 'Aviso de rectificação da Lei n.º 13/2016, BR n.º 89/2017, p. 569; exemplar local, anexos corrigidos e efeito temporal ainda não autenticados.'],
|
||
['target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_13_2016_rectification_2017', 'relationship' => 'cites', 'applicability' => 'evidence', 'review_status' => 'pending',
|
||
'condition_text' => 'Relação candidata pendente de conferência da cópia e dos anexos rectificados.',
|
||
'basis_reference' => 'Aviso de rectificação da Lei n.º 13/2016, BR n.º 89/2017, p. 569; ficha INM identifica o aviso, mas a cópia local e os anexos não foram autenticados.']],
|
||
],
|
||
[
|
||
'external_id' => 'ine-codificador-divisao-politico-administrativa-2012-v1',
|
||
'optional' => true,
|
||
'archive_path' => 'storage/app/private/geo-ine-source-20260928/original.pdf',
|
||
'file_name' => 'INE-Codificador-Divisao-Politico-Administrativa-2012.pdf',
|
||
'expected_sha256' => '5c69b6f189a1d3f5307e0cf7dd8224554705158a682637d3105afad615db26e2', 'size_bytes' => 67067,
|
||
'title' => 'Codificador da Divisão Político-Administrativa (INE, 07-06-2012)',
|
||
'document_number' => 'Codificador DPA 2012', 'document_date' => '2012-06-07', 'issuer' => 'Instituto Nacional de Estatística (INE)',
|
||
'language' => 'pt', 'version_label' => 'Edição 2012; actualidade das designações por confirmar',
|
||
'topic' => 'território', 'category' => 'Dados de referência', 'document_type' => 'reference_data',
|
||
'is_legislation' => false, 'legal_status' => 'unknown', 'source_confirmation' => 'official_confirmed',
|
||
'source_url' => 'https://www.ine.gov.mz/documents/20119/44337/Codificador%20da%20Divisao%20Politico%20administrativa.pdf/14aed3d0-9296-c038-dbac-041a5cf40f1d',
|
||
'source_reference' => 'PDF disponibilizado no portal oficial do INE; download efectuado em 2026-10-06, 67.067 bytes. SHA-256 coincidente com o original privado e com a cópia oficial obtida no portal INE. A data impressa é 07-06-2012; a vigência territorial actual e os direitos de reutilização do ficheiro permanecem por confirmar.',
|
||
'description' => 'Documento de referência com códigos e designações político-administrativas na edição indicada. A extracção anterior encontrou colunas com alinhamento ambíguo, sem autorizar correspondências automáticas de postos e distritos; não usar esta edição como catálogo territorial actual nem como fonte de relações não revistas.',
|
||
'territorial_scope' => 'national', 'associations' => [],
|
||
],
|
||
];
|
||
}
|
||
}
|
||
|