dados do seeder
tests / ci (push) Waiting to run

This commit is contained in:
2026-09-30 13:47:51 +02:00
parent 3395424d82
commit 5e6b32b493
7 changed files with 15592 additions and 3 deletions
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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,
]);
}
}
+1 -1
View File
@@ -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 -1
View File
@@ -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";