File diff suppressed because it is too large
Load Diff
@@ -17,12 +17,13 @@ class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->call(ProductionReferenceSeeder::class);
|
||||
|
||||
if (! app()->environment(['local', 'testing'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->call(InvestMozReferenceSeeder::class);
|
||||
$this->call(InvestmentReferenceSeeder::class);
|
||||
|
||||
// User::factory(10)->create();
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
/** Seeds version-controlled reference data and draft portfolio proposals only. */
|
||||
class ProductionReferenceSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
SectorSeeder::class,
|
||||
InvestmentAreaSeeder::class,
|
||||
InvestmentReferenceSeeder::class,
|
||||
TerritoryReferenceSeeder::class,
|
||||
InvestmentPortfolioSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class SectorSeeder extends Seeder
|
||||
];
|
||||
|
||||
foreach ($sectors as $index => $sector) {
|
||||
Sector::updateOrCreate(
|
||||
Sector::firstOrCreate(
|
||||
['slug' => $sector['slug']],
|
||||
[...$sector, 'is_active' => true, 'sort_order' => $index + 1, 'published_at' => now()]
|
||||
);
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use RuntimeException;
|
||||
|
||||
class TerritoryReferenceSeeder extends Seeder
|
||||
{
|
||||
private const SOURCE_CHECKSUM = '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff';
|
||||
|
||||
private const RECORDS_CHECKSUM = '630b574a3bfc5fa80dfb8d68314d7a2c413c86c3b1aa9e71603b393523b81c3c';
|
||||
|
||||
private const EXPECTED_COUNTS = [
|
||||
'country' => 1,
|
||||
'province' => 11,
|
||||
'district' => 148,
|
||||
'admin_post' => 437,
|
||||
'locality' => 1324,
|
||||
];
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$data = json_decode(File::get(database_path('data/territory-locations.json')), true, 512, JSON_THROW_ON_ERROR);
|
||||
if (! is_array($data) || ! is_array($data['records'] ?? null)) {
|
||||
throw new RuntimeException('Territory reference dataset is malformed.');
|
||||
}
|
||||
|
||||
$records = $data['records'];
|
||||
|
||||
$actualCounts = is_array($records) ? array_count_values(array_column($records, 'location_type')) : [];
|
||||
ksort($actualCounts);
|
||||
$expectedCounts = self::EXPECTED_COUNTS;
|
||||
ksort($expectedCounts);
|
||||
|
||||
if (($data['source_system'] ?? null) !== 'cartografia-workbook'
|
||||
|| ($data['source_sha256'] ?? null) !== self::SOURCE_CHECKSUM
|
||||
|| ($data['source_version'] ?? null) !== 3
|
||||
|| ! is_string($data['records_sha256'] ?? null)
|
||||
|| ! hash_equals(self::RECORDS_CHECKSUM, $data['records_sha256'])
|
||||
|| ! hash_equals(self::RECORDS_CHECKSUM, hash('sha256', json_encode($records, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)))
|
||||
|| ! is_array($records)
|
||||
|| $actualCounts !== $expectedCounts) {
|
||||
throw new RuntimeException('Territory reference dataset failed its source or completeness checks.');
|
||||
}
|
||||
|
||||
$counts = DB::transaction(function () use ($records): array {
|
||||
DB::select('SELECT pg_advisory_xact_lock(280926, 150000)');
|
||||
|
||||
$seen = [];
|
||||
$types = [];
|
||||
$counts = ['created' => 0, 'unchanged' => 0, 'made_public' => 0];
|
||||
|
||||
foreach ($records as $record) {
|
||||
$this->validateRecord($record, $seen, $types);
|
||||
|
||||
$externalId = self::SOURCE_CHECKSUM.':'.$record['id'];
|
||||
$existing = DB::table('geo_locations')->where('id', $record['id'])->first();
|
||||
$externalMatch = DB::table('geo_locations')
|
||||
->where('source_system', 'cartografia-workbook')
|
||||
->where('external_id', $externalId)
|
||||
->first();
|
||||
|
||||
if ($externalMatch && (! $existing || $externalMatch->id !== $existing->id)) {
|
||||
throw new RuntimeException('Territory external identifier collision: '.$record['id']);
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
if ($existing->source_system !== 'cartografia-workbook' || $existing->external_id !== $externalId) {
|
||||
throw new RuntimeException('Territory ID belongs to another source: '.$record['id']);
|
||||
}
|
||||
|
||||
foreach (['name', 'parent_id', 'location_type', 'code', 'source_code'] as $field) {
|
||||
if ($record[$field] !== $existing->{$field}) {
|
||||
throw new RuntimeException('Territory reference differs from packaged source: '.$record['id']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($existing->deleted_at !== null) {
|
||||
throw new RuntimeException('Packaged territory reference is archived: '.$record['id']);
|
||||
}
|
||||
|
||||
if ($existing->data_classification === 'internal') {
|
||||
DB::table('geo_locations')->where('id', $record['id'])->update([
|
||||
'data_classification' => 'public',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$counts['made_public']++;
|
||||
} elseif ($existing->data_classification === 'public') {
|
||||
$counts['unchanged']++;
|
||||
} else {
|
||||
throw new RuntimeException('Territory reference has an unexpected classification: '.$record['id']);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('geo_locations')->insert([
|
||||
...$record,
|
||||
'source_system' => 'cartografia-workbook',
|
||||
'external_id' => $externalId,
|
||||
'version_no' => 1,
|
||||
'data_classification' => 'public',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
'deleted_at' => null,
|
||||
]);
|
||||
$counts['created']++;
|
||||
}
|
||||
|
||||
return $counts;
|
||||
});
|
||||
|
||||
$this->command?->info(sprintf(
|
||||
'Referências territoriais: %d criadas, %d já existentes, %d classificadas como públicas.',
|
||||
$counts['created'],
|
||||
$counts['unchanged'],
|
||||
$counts['made_public'],
|
||||
));
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $record
|
||||
* @param array<string, bool> $seen
|
||||
* @param array<string, string> $types
|
||||
*/
|
||||
private function validateRecord(array $record, array &$seen, array &$types): void
|
||||
{
|
||||
foreach (['id', 'name', 'location_type', 'code', 'source_code'] as $field) {
|
||||
if (! isset($record[$field]) || $record[$field] === '') {
|
||||
throw new RuntimeException('Territory reference is missing '.$field.'.');
|
||||
}
|
||||
}
|
||||
|
||||
$type = $record['location_type'];
|
||||
$expectedParent = [
|
||||
'country' => null,
|
||||
'province' => 'country',
|
||||
'district' => 'province',
|
||||
'admin_post' => 'district',
|
||||
'locality' => 'admin_post',
|
||||
][$type] ?? false;
|
||||
|
||||
if ($expectedParent === false || isset($seen[$record['id']])) {
|
||||
throw new RuntimeException('Territory reference has an invalid type or duplicate ID.');
|
||||
}
|
||||
|
||||
if ($expectedParent === null) {
|
||||
if ($record['parent_id'] !== null) {
|
||||
throw new RuntimeException('Country reference cannot have a parent.');
|
||||
}
|
||||
} elseif (! $record['parent_id'] || ($types[$record['parent_id']] ?? null) !== $expectedParent) {
|
||||
throw new RuntimeException('Territory reference has a missing or invalid parent: '.$record['id']);
|
||||
}
|
||||
|
||||
$seen[$record['id']] = true;
|
||||
$types[$record['id']] = $type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Seed de referências para produção
|
||||
|
||||
## O que é carregado
|
||||
|
||||
`DatabaseSeeder` chama `ProductionReferenceSeeder` em qualquer ambiente. O seeder carrega:
|
||||
|
||||
- setores e áreas de investimento versionados;
|
||||
- opções de referência dos formulários;
|
||||
- 1.921 referências territoriais com origem e checksum validados: 1 país, 11 províncias, 148 distritos, 437 postos administrativos e 1.324 localidades;
|
||||
- projectos, clusters, corredores e fichas documentais da carteira versionada, mantidos como rascunhos.
|
||||
|
||||
O import é transaccional e repetível. Não substitui nomes, relações ou códigos quando encontra um registo territorial com o mesmo identificador mas conteúdo diferente; nesse caso aborta a transacção para revisão. Só promove de `internal` para `public` as referências pertencentes ao mesmo conjunto e com conteúdo idêntico, para disponibilizar os dropdowns autorizados.
|
||||
|
||||
Os ficheiros-fonte versionados são `database/data/territory-locations.json`, `database/data/investment-reference-options.json` e `database/data/investment-portfolio.json`. Os 69 registos territoriais antigos arquivados ficam fora do conjunto activo. O Excel original não é necessário no servidor.
|
||||
|
||||
## Aplicação
|
||||
|
||||
Depois de publicar o código e configurar PostgreSQL/PostGIS no destino, criar primeiro um backup recuperável e executar:
|
||||
|
||||
```sh
|
||||
php artisan migrate --force
|
||||
php artisan db:seed --force
|
||||
```
|
||||
|
||||
Em produção, `DatabaseSeeder` termina depois de `ProductionReferenceSeeder`; não cria utilizadores de teste nem o investidor/projecto de exemplo. `SectorSeeder` cria apenas sectores em falta e os importadores da carteira preservam registos editados manualmente.
|
||||
|
||||
## Dados que não são seed
|
||||
|
||||
O conteúdo operacional da base (utilizadores, investidores, submissões, pedidos, sessões, auditoria, cache e outros registos criados pelos utilizadores) não deve ser convertido em PHP/JSON versionado nem copiado por `db:seed`. Deve ser transferido entre ambientes por backup/restauro ou por um plano de migração de dados com mapeamento, validação de privacidade e reconciliação de chaves. As oportunidades de demonstração de `OpportunitySeeder` também não são executadas em produção.
|
||||
|
||||
O seeder não foi executado contra produção durante a preparação deste pacote. A aplicação acima deve ocorrer no destino depois do backup, com revisão do relatório de colisões caso o conteúdo já existente divirja.
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link, router, useForm, usePage } from "@inertiajs/react";
|
||||
+import { Link, router, useForm, usePage } from "@inertiajs/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Map from "@/components/interactive-investment-map";
|
||||
import { TerritoryLevel, type TerritoryOption } from "@/components/investment-territory";
|
||||
|
||||
Reference in New Issue
Block a user