feat(proactive): integrate private supply observations and demonstrate Gurue comparison in QA
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\ProactiveSupply;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
final class ProactiveSupplyController extends Controller
|
||||
{
|
||||
public function index(Request $request,ProactiveSupply $service): JsonResponse
|
||||
{
|
||||
$v=$request->validate(['record_id'=>'nullable|integer|min:1']);
|
||||
$record=isset($v['record_id'])?Opportunity::findOrFail($v['record_id']):null;
|
||||
return response()->json($service->search($request->user(),$request->all(),$record))->header('Cache-Control','private, no-store');
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class OpportunityManagement
|
||||
$this->check($record, $data['etag']);
|
||||
abort_if($record->trashed() || $record->status === 'archived', 409, 'Restaure antes de editar.');
|
||||
}
|
||||
if (array_key_exists('proactive', $data)) $data['proactive'] = app(ProactiveInvestment::class)->draft($data['proactive'], $record);
|
||||
if (array_key_exists('proactive', $data)) $data['proactive'] = app(ProactiveInvestment::class)->draft($data['proactive'], $record, $actor);
|
||||
$before = $record->getAttributes();
|
||||
$previousCanonical = $record->exists ? ($this->values($record)['canonical'] ?? []) : [];
|
||||
$changes = array_intersect_key($data, array_flip([...self::FIELDS, ...self::PRIVATE_DRAFT_FIELDS]));
|
||||
@@ -227,7 +227,7 @@ class OpportunityManagement
|
||||
$record = Opportunity::withTrashed()->whereKey($id)->lockForUpdate()->firstOrFail();
|
||||
abort_if($record->owner_user_id !== null, 403, 'A v1 exige pedido e decisão de arquivo; não concede exclusão ou restauro ao Promotor.');
|
||||
$this->check($record, $data['etag']);
|
||||
if (array_key_exists('proactive', $data)) $data['proactive'] = app(ProactiveInvestment::class)->draft($data['proactive'], $record);
|
||||
if (array_key_exists('proactive', $data)) $data['proactive'] = app(ProactiveInvestment::class)->draft($data['proactive'], $record, $actor);
|
||||
$before = $record->getAttributes();
|
||||
if ($data['action'] === 'restore') {
|
||||
abort_unless($record->trashed() || $record->status === 'archived', 409, 'Registo não está eliminado/arquivado.');
|
||||
|
||||
@@ -16,12 +16,14 @@ final class ProactiveInvestment
|
||||
public const NOTICE = 'A ausência de registo não comprova ausência de oferta. Proximidade não comprova contrato, participação, energia acessível, terra disponível ou benefício fiscal. Necessidade confirmada não comprova viabilidade comercial; hipótese de fornecimento não é encomenda.';
|
||||
public const GROUPS = ['exists' => 'O que existe', 'needs' => 'Necessidades identificadas', 'ideas' => 'Ideias de investimento'];
|
||||
|
||||
public function draft(array $input, ?Opportunity $record = null): array
|
||||
public function draft(array $input, ?Opportunity $record = null, ?\App\Models\User $actor = null): array
|
||||
{
|
||||
$d = Validator::make(['proactive' => $input], [
|
||||
'proactive' => 'required|array:version,kind,evidence,need,idea,relations', 'proactive.version' => 'required|integer|in:1',
|
||||
'proactive' => 'required|array:version,kind,evidence,need,idea,relations,supply_comparisons', 'proactive.version' => 'required|integer|in:1',
|
||||
'proactive.kind' => 'required|in:exists,needs,ideas', 'proactive.evidence' => 'present|array|max:60',
|
||||
'proactive.evidence.*' => 'array:id,category,purpose,statement,interpretation,source,locator,period,scope,coverage,verified_on,access,document_id',
|
||||
'proactive.evidence.*' => 'array:id,category,purpose,statement,interpretation,source,locator,period,scope,coverage,verified_on,access,document_id,observation_id',
|
||||
'proactive.evidence.*.observation_id'=>'sometimes|nullable|string|size:64',
|
||||
'proactive.supply_comparisons'=>'sometimes|array|max:40',
|
||||
'proactive.evidence.*.id' => 'required|uuid|distinct',
|
||||
'proactive.evidence.*.category' => 'required|in:activity,resource,infrastructure,supply,demand',
|
||||
'proactive.evidence.*.purpose' => 'required|in:local,methodology',
|
||||
@@ -47,6 +49,19 @@ final class ProactiveInvestment
|
||||
'proactive.relations.*.reference' => 'required|string|max:4000',
|
||||
'proactive.relations.*.evidence_ids' => 'present|array|max:60', 'proactive.relations.*.evidence_ids.*' => 'uuid|distinct',
|
||||
])->validate()['proactive'];
|
||||
$actor??=auth()->user();
|
||||
if (!empty($d['supply_comparisons'])) $d['supply_comparisons']=app(ProactiveSupply::class)->comparisons($actor,$d['supply_comparisons'],$record);
|
||||
foreach($d['evidence'] as &$proof){
|
||||
if(!empty($proof['observation_id'])){
|
||||
$canonical=app(ProactiveSupply::class)->evidence($actor,$proof['observation_id'],$record);
|
||||
if($proof['access']!=='internal') $this->fail('evidence','A observação da base interna não pode ser reclassificada como pública.');
|
||||
$proof=array_replace($proof,$canonical);
|
||||
}
|
||||
}
|
||||
unset($proof);
|
||||
foreach($d['supply_comparisons']??[] as $comparison){
|
||||
if(!in_array($comparison['observation_id'],array_column($d['evidence'],'observation_id'),true))$this->fail('supply_comparisons','A comparação deve conservar a evidência interna da observação seleccionada.');
|
||||
}
|
||||
$ids = array_column($d['evidence'], 'id');
|
||||
foreach ([$d['need'], ...$d['relations']] as $row) {
|
||||
if (array_diff($row['evidence_ids'], $ids)) $this->fail('evidence', 'Seleccione apenas evidências desta análise.');
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/** Internal observations, never a public business directory or proof of demand. */
|
||||
final class ProactiveSupply
|
||||
{
|
||||
public const LIMITATIONS = 'Informação administrativa histórica e incompleta. Actividade actual, capacidade, procura e compromissos por confirmar. Ausência de registos significa informação insuficiente, nunca ausência de oferta.';
|
||||
|
||||
public function authorize(User $actor, ?Opportunity $record = null): void
|
||||
{
|
||||
$access=app(PortalProfileAccess::class);
|
||||
$internal=collect(OpportunityManagement::CATEGORIES)->contains(fn($type)=>$access->internalAssignment($actor,$type)!==null);
|
||||
abort_unless($access->legacyWriteAllowed($actor)||$internal,403);
|
||||
if ($record?->exists) $access->authorize($actor,'view',$record);
|
||||
}
|
||||
|
||||
private function read(User $actor, callable $callback): mixed
|
||||
{
|
||||
return DB::transaction(function()use($actor,$callback){
|
||||
DB::select("select set_config('app.actor_user_id',?,true),set_config('app.proactive_supply_read','internal',true)",[(string)$actor->id]);
|
||||
return $callback();
|
||||
});
|
||||
}
|
||||
|
||||
public function search(User $actor,array $input,?Opportunity $record=null): array
|
||||
{
|
||||
$this->authorize($actor,$record);
|
||||
$v=Validator::make($input,[
|
||||
'territory_id'=>['required','uuid',Rule::exists('geo_locations','id')->whereNull('deleted_at')],
|
||||
'activity'=>'nullable|string|max:150','sector'=>'nullable|string|max:250','page'=>'nullable|integer|min:1|max:100000',
|
||||
])->validate();
|
||||
if(!Schema::hasTable('proactive_supply_observations'))return ['installed'=>false,'rows'=>[],'total'=>null,'limitations'=>self::LIMITATIONS,'status'=>'Base interna ainda não carregada neste ambiente.'];
|
||||
return $this->read($actor,function()use($v){
|
||||
$ids=[$v['territory_id']];
|
||||
// Canonical descendants, not a name-only cross-province match.
|
||||
$front=$ids;
|
||||
for($depth=0;$depth<5&&$front;$depth++){
|
||||
$front=DB::table('geo_locations')->whereIn('parent_id',$front)->whereNull('deleted_at')->pluck('id')->all();
|
||||
$ids=[...$ids,...$front];
|
||||
}
|
||||
$base=DB::table('proactive_supply_observations as o')->join('proactive_supply_sources as s','s.sha256','=','o.source_hash')->whereIn('o.territory_id',$ids);
|
||||
$sectors=(clone $base)->where('o.sector','<>','')->distinct()->orderBy('o.sector')->pluck('o.sector')->all();
|
||||
if(!empty($v['sector']))$base->where('o.sector',$v['sector']);
|
||||
if(!empty($v['activity']))$base->where(fn($q)=>$q->where('o.cae','ilike','%'.$v['activity'].'%')->orWhereRaw("translate(lower(o.activity),'áàâãéêíóôõúç','aaaaeeiooouc') LIKE ?",['%'.strtolower(\Illuminate\Support\Str::ascii($v['activity'])).'%']));
|
||||
$page=$v['page']??1;
|
||||
// Exact repeated observations are retained in storage, counted separately.
|
||||
$total=(clone $base)->whereNull('o.duplicate_of')->count();
|
||||
$duplicates=(clone $base)->whereNotNull('o.duplicate_of')->count();
|
||||
$rows=(clone $base)->whereNull('o.duplicate_of')->orderBy('o.name')->orderBy('o.id')->offset(($page-1)*25)->limit(25)->get(['o.id','o.entity_key','o.name','o.province','o.district','o.territory_id','o.territory_resolution','o.cae','o.activity','o.sector','o.source_date','o.sheet','o.row_number','o.source_hash','o.identity_conflict','s.name as source_name','s.period as source_period','s.imported_at']);
|
||||
$coverage=DB::table('proactive_supply_observations')->selectRaw("count(*) FILTER (WHERE territory_id IS NULL) AS unmapped, count(*) FILTER (WHERE territory_resolution='province_only_district_unknown') AS province_only")->first();
|
||||
return ['installed'=>true,'rows'=>$rows,'total'=>$total,'duplicate_sightings'=>$duplicates,'page'=>$page,'per_page'=>25,'sectors'=>$sectors,'territory_coverage'=>$coverage,'limitations'=>self::LIMITATIONS,'status'=>$total?'Registos documentais encontrados; não equivalem a empresas únicas ou oferta actual confirmada.':'Informação insuficiente nesta base e nestes filtros.'];
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve server-side source lineage; a caller cannot fabricate an internal observation. */
|
||||
public function comparisons(User $actor,array $input,?Opportunity $record): array
|
||||
{
|
||||
$this->authorize($actor,$record);
|
||||
$v=Validator::make(['rows'=>$input],[
|
||||
'rows'=>'array|max:40','rows.*'=>'array:observation_id,relationship,rationale,limitations',
|
||||
'rows.*.observation_id'=>'required|string|size:64|distinct',
|
||||
'rows.*.relationship'=>'required|in:competition,complementarity,partner,uncertain',
|
||||
'rows.*.rationale'=>'required|string|max:4000','rows.*.limitations'=>'required|string|max:4000',
|
||||
])->validate()['rows'];
|
||||
if(!$v)return [];
|
||||
abort_unless(Schema::hasTable('proactive_supply_observations'),422,'Base interna indisponível.');
|
||||
return $this->read($actor,function()use($v){
|
||||
foreach($v as $row){
|
||||
$o=DB::table('proactive_supply_observations')->where('id',$row['observation_id'])->first();
|
||||
if(!$o)throw \Illuminate\Validation\ValidationException::withMessages(['proactive.supply_comparisons'=>'Registo da oferta indisponível; preserve a referência anterior e confira o ambiente.']);
|
||||
}
|
||||
return $v;
|
||||
});
|
||||
}
|
||||
|
||||
public function evidence(User $actor,string $id,?Opportunity $record): array
|
||||
{
|
||||
$this->authorize($actor,$record);
|
||||
return $this->read($actor,function()use($id){
|
||||
$o=DB::table('proactive_supply_observations as o')->join('proactive_supply_sources as s','s.sha256','=','o.source_hash')->where('o.id',$id)->first(['o.*','s.name as source_name','s.period as source_period']);
|
||||
if(!$o)throw \Illuminate\Validation\ValidationException::withMessages(['proactive.evidence'=>'Observação interna indisponível neste ambiente.']);
|
||||
return ['observation_id'=>$o->id,'category'=>'supply','purpose'=>'local','access'=>'internal','document_id'=>null,
|
||||
'statement'=>'Registo administrativo: '.$o->name.'; CAE '.$o->cae.'; '.($o->activity?:'actividade por classificar').'; '.$o->district.', '.$o->province.'. Não confirma actividade actual.',
|
||||
'source'=>$o->source_name.'; SHA256 '.$o->source_hash,'locator'=>$o->sheet.'!linha '.$o->row_number,
|
||||
'period'=>$o->source_date.'; '.$o->source_period,'scope'=>$o->district.', '.$o->province,
|
||||
'coverage'=>self::LIMITATIONS.($o->identity_conflict?' Identificador partilhado por denominações divergentes: identidade por conferir.':''),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('proactive_supply_sources', function ($t) {
|
||||
$t->char('sha256',64)->primary();
|
||||
$t->text('name');
|
||||
$t->text('period');
|
||||
$t->text('limitations');
|
||||
$t->jsonb('receipt');
|
||||
$t->timestampTz('imported_at');
|
||||
});
|
||||
Schema::create('proactive_supply_observations', function ($t) {
|
||||
$t->char('id',64)->primary();
|
||||
$t->char('source_hash',64);
|
||||
$t->foreign('source_hash')->references('sha256')->on('proactive_supply_sources')->restrictOnDelete();
|
||||
$t->text('sheet');
|
||||
$t->integer('row_number');
|
||||
$t->char('entity_key',64)->index();
|
||||
foreach (['name','province','district','cae','activity','sector','source_date'] as $field) $t->text($field);
|
||||
$t->char('fingerprint',64)->index();
|
||||
$t->char('duplicate_of',64)->nullable();
|
||||
$t->boolean('identity_conflict')->default(false);
|
||||
$t->uuid('territory_id')->nullable()->index();
|
||||
$t->text('territory_resolution');
|
||||
$t->index(['province','district']);
|
||||
$t->unique(['source_hash','sheet','row_number']);
|
||||
});
|
||||
// Technical defence of the private-analysis scope, not a new business role.
|
||||
foreach (['proactive_supply_sources','proactive_supply_observations'] as $table) {
|
||||
DB::statement("ALTER TABLE {$table} ENABLE ROW LEVEL SECURITY");
|
||||
DB::statement("ALTER TABLE {$table} FORCE ROW LEVEL SECURITY");
|
||||
DB::statement("CREATE POLICY {$table}_private_read ON {$table} FOR SELECT USING (current_setting('app.proactive_supply_read',true)='internal' AND NULLIF(current_setting('app.actor_user_id',true),'')::bigint IS NOT NULL)");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::table('proactive_supply_sources')->exists()) throw new RuntimeException('Preserve imported source history before rollback.');
|
||||
Schema::dropIfExists('proactive_supply_observations');
|
||||
Schema::dropIfExists('proactive_supply_sources');
|
||||
}
|
||||
};
|
||||
@@ -1636,3 +1636,45 @@ A falta de prova do caso (titular/rendimento/imposto/inclusão/uso/regra do per
|
||||
Recibo consolidado:`docs/investmoz/simulations/irpc-l8-grouped-delivery-20261028.json`. Cópia remota:`/var/backups/investmoz-irpc-l8-grouped-20261028/installed-receipt.json`. Provas locais:`outputs/irpc-l8-grouped-delivery-20261028/`. Actualização documental segue ramo próprio de recibos,sem alterar novamente a revisão servida.
|
||||
|
||||
Cópia remota do recibo conferida:SHA256`b70fe154f95162f029b5ca712b41962757360d73b0ff07f52b8ce14848d990df`;a matriz existente recebeu saldoL8 nos camposEstado,Disponibilidade activa,Evidências,Critérios restantes eDependências,só em `AUD-SIM-INT02`.
|
||||
|
||||
|
||||
## 28/10/2026 — listas como base interna da oferta: uso não demonstrado
|
||||
|
||||
**Decisão expressa da Tempo e resultado da conferência: a utilização das listas de empresas,hotéis/restaurantes como base comparativa no portal é NÃO DEMONSTRADA.** A entrega do percurso proactivo,os seus campos de evidência e os pilotos privados anteriores mantêm-se aceites;isso não demonstra carregamento,classificação e ligação destas listas ao consumidor de análise. O âmbito é primeiro a análise interna,não a publicação automática de um directório.
|
||||
|
||||
**Fonte e âmbito conferidos:**13ficheiros inventariados nos dois directórios acessíveis de empresas/relatórios,antes da selecção;originais `EMPRESAS_LIST2014–2022`, `Empresas Licenciadas`,cadastro turístico30/10/2024 e `CAE eBAU` localizados e iguais aos hashes aceites. As148557/212632/460linhasXML não vazias dos recibos anteriores são observações dos ficheiros,não empresas únicas nem carga no portal. Empresas têm província/distrito/CAE;isso não prova o mapeamento no portal. O cadastro turístico observado é de alojamento,não um censo completo da restauração. Não foi identificada lista autónoma de restaurantes nestes dois directórios;isso não significa ausência de restaurantes ou de actividades de restauração nas listas empresariais. Contactos e outros campos privados não foram publicados.
|
||||
|
||||
**Destino entregue:**código `569a1cd7`,base `teste_investidor`,198tabelas não sistémicas(174public+24skills_edu),consultadas em transacções READ ONLY com identidade técnicapostgres explicitada. Zero correspondências dos hashes originais entre368ficheiros persistentes e nenhuma correspondência exacta dos nomes/hashes nas referências pesquisadas. Cadastros `existing_businesses` e `investment_company_declarations` têm0registos nesse destino;organizações1,actividades11,documentos60 e recursos59 não são convertidos em oferta empresarial das listas. Esta é uma fotografia técnica,não prova de visibilidade para uma conta operacional,perda de registos ou ausência de oferta no mercado. Registos antigos de `investemoz` e basesQA são ambientes distintos;nenhuma eliminação é declarada. Pesquisa por nomes/hashes em metadados não exclui ficheiros renomeados,arquivados,conteúdo cifrado ou outros acervos.
|
||||
|
||||
**Causa delimitada no consumidor instalado:**`ProactiveInvestment::options` devolve documentos/grupos e referências a projecto,cluster,corredor,parque,cadeia;os hashes dos ficheiros instalados coincidem comGit. O editor recebe `observed_supply` e fontes/localizadores/períodos/cobertura como texto manual;as relações proactivas não admitem entidade empresarial destas listas. Não foi encontrada consulta de oferta carregada por localização/sector/actividade nem ligação de linhas das listas a concorrentes,complementares ou parceiros. A possível existência de cadastros noutro módulo não demonstra consumo por este percurso.
|
||||
|
||||
| Percurso de conferência — Gurué,distrito canónico | Resultado |
|
||||
| --- | --- |
|
||||
| Escolher localização | Referência canónica localizada;prova anterior de selecção territorial preservada,não se declara nova prova de interface. |
|
||||
| Consultar oferta existente das listas | Não demonstrado:consulta/importação/classificação consumida pelo percurso não identificada. A demonstração pára neste ponto. |
|
||||
| Comparar com uma ideia | Não demonstrado sobre conjunto rastreável de registos dessas listas. Campos manuais não equivalem à integração. |
|
||||
| Identificar necessidade | Não demonstrado por este fluxo;ausência de registo não confirma necessidade nem ausência de concorrência. Manter hipótese,período,cobertura e incertezas. |
|
||||
| Preparar oportunidade | Mecanismo privado entregue preservado;nenhuma oportunidade nova foi criada para simular passos anteriores ainda não demonstrados. |
|
||||
|
||||
**Critério de retoma:**lotes/linhas privados rastreáveis e classificados,reutilizando os mecanismos/cadastros existentes;consulta autorizada da oferta por localização/sector/actividade;comparação da ideia com concorrência,complementares e possíveis parceiros,com fontes/datas/cobertura;necessidade fundamentada sem inferir ausência de oferta;e prévia/gravação/recuperação da oportunidade no percurso existente emQA isolada. Só essa conferência pode demonstrar o uso;nenhum novo aprovador organizacional ou directório público é criado.
|
||||
|
||||
Sem imports,normalização,alteração de código,permissões,publicação ou escrita em dados activos. Songo/Gurué privados,aprovações/históricos e entregaL8 preservados. Registo:`docs/investmoz/unification/existing-supply-lists-audit-20261028.json`. Cobertura actualizada nas linhas existentesRF-CLU-003,MASTER-471012ab-0010,M2-d0c06a75-0147/0151 eMASTER-1f0a4cde-0002,sem nova matriz/backlog.
|
||||
|
||||
A base interna de oferta não exige transformar cada empresa numa organização utilizadora,anúncio de venda/parceria,promotor ou cliente confirmado. O cadastro e a consulta analítica são distintos da operação comercial e da divulgação.
|
||||
|
||||
|
||||
## 28/10/2026 — oferta existente ligada à análise proactiva, demonstrada em QA
|
||||
|
||||
Incremento sobre o código entregue `569a1cd7`, ramo `codex/proactive-supply-qa-20261028`: **demonstrado em QA isolada, sem instalação nem importação no activo**. O relatório de conferência anterior permanece histórico: no destino activo, o uso das listas continua não demonstrado. Reutilizam-se a aceitação do percurso, o inventário de 13 ficheiros, os originais, as fontes e as provas anteriores; não se reabre a decisão da Tempo nem se encerra o estudo de mercado nacional.
|
||||
|
||||
Carregadas privadamente **343.041 observações**: 140.847 da lista 2014–2022, 201.917 de empresas licenciadas e 277 do cadastro turístico (duas folhas com estruturas distintas). Os quatro hashes dos originais e do CAE foram conferidos. Pré-visualização, análise Microsoft Defender sem ameaças e repetição idempotente 343.041→343.041 demonstradas. Os 139.591 registos com a mesma projecção administrativa são conservados como repetições; não representam empresas únicas nem duplicados de ficheiros por bytes. As 113.154 observações com denominações divergentes para um identificador partilhado ficam assinaladas, sem fusão automática. Contactos, documentos de identidade e NUIT não são devolvidos pela consulta.
|
||||
|
||||
Consulta interna por localização canónica (incluindo descendentes), sector CAE e actividade; resultados paginados conservam fonte, folha/linha, hash e data original. O sector provém da hierarquia SELF_ID do CAE fornecido, sem inventar correspondência com os sectores de navegação. A selecção permite comparar possível concorrência, complementaridade, parceiro ou relação por determinar, com fundamento e incertezas. O servidor recupera a origem, impede reclassificar a evidência interna como pública e conserva-a no rascunho e no dossier privado. Não transforma a lista em anúncios, promotores, clientes confirmados ou publicação de directório.
|
||||
|
||||
**Gurué:** o operador QA sintético 1021 consultou 410 observações distintas, comparou dois registos reais (serviços agrícolas CAE 01610 e comércio de plantas/sementes/fertilizantes CAE 47732) com a hipótese já preparada de apoio à colheita e qualidade da folha verde. A oferta possível exige verificar sobreposição e colaboração, sem presumir mercado vazio. Rascunho 402 passa de v1 a v2, mantendo integralmente as cinco evidências anteriores, a ideia e o histórico. O mesmo dossier `883c8ec3-59a3-4074-b91c-b7d4a5d88ff0` passa de contexto1 a2 por pré-visualização e confirmação idempotente, preservando título e campos deliberados. A necessidade permanece **hipótese**, sem procura, falta de oferta ou viabilidade confirmadas. Gravação, recarregamento, acesso privado e resultado vazio como **informação insuficiente** demonstrados no navegador. Isto é prova técnica, sem substituir observação/aceitação da equipa.
|
||||
|
||||
**Cobertura e pendências:** 198.112 observações sem correspondência territorial canónica única (incluindo 19 ambiguidades), 325 apenas com província e 80.796 sem designação de actividade CAE correspondente. Mantêm-se os rótulos originais e a incerteza; não há atribuição territorial aproximada nem preenchimento presumido. O cadastro de alojamento não é um censo exaustivo de hotéis/restaurantes. Situação operacional actual, capacidade, preços, procura e compromissos continuam por demonstrar. O incremento nacional é parcial; não encerra os requisitos amplos de mercado, clusters ou liquidação anual. **L8 permanece parcial e inalterado.**
|
||||
|
||||
Sete testes dirigidos/62 asserções, TypeScript e compilação QA passaram. As duas tabelas internas têm RLS forçada; teste com identidade sem privilégios confirma ocultação sem contexto de actor e leitura com contexto autorizado. Conferidos os hashes de todas as linhas anteriores nos sete conjuntos do inventário: só o rascunho402 e o mesmo dossier sofreram o delta intencional; Songo401, projectos, territórios, sectores, documentos e cadeias foram conservados. Provas fiscais aceites reutilizadas, sem repetir as suas suites.
|
||||
|
||||
Actualizados os cinco requisitos existentes `RF-CLU-003`, `MASTER-471012ab-0010`, `M2-d0c06a75-0147`, `M2-d0c06a75-0151` e `MASTER-1f0a4cde-0002`, preservando o restante estado e pendências. Nenhuma matriz ou backlog novo. A entrega activa futura mantém conferência de capacidade/compatibilidade com main, backup/restauro fresco, operação autenticada e delta instalado. Relatório: `docs/investmoz/unification/proactive-existing-supply-qa-20261028.json`.
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"date": "2026-10-28",
|
||||
"status": "not_demonstrated",
|
||||
"audited_use": "companies,hotels and restaurants lists as private supply-comparison basis inside proactive journey",
|
||||
"Tempo_decision": "know existing supply by location/sector/activity;compare ideas for competition,complementarity and possible partners;identify evidenced needs and prepare opportunities;no absence-of-offer inference from absent records;no automatic public directory",
|
||||
"installed_runtime": "569a1cd750f3d2a0b87f130e992e73e070bbd1ab",
|
||||
"scope": {
|
||||
"destination": "teste-investidor.tempo.co.mz",
|
||||
"database": "teste_investidor",
|
||||
"catalogue_tables": 198,
|
||||
"schemas": {
|
||||
"public": 174,
|
||||
"skills_edu": 24
|
||||
},
|
||||
"identity": "postgres owner,read-only transactions;technical metadata inventory,not an authenticated team UI proof",
|
||||
"read_only": true,
|
||||
"RLS_note": "No permissions changed or RLS bypassed for a team operation. Owner inventory is explicitly separated from application/user visibility. Differences from earlier investemoz/QA environments are not declared deletion/loss.",
|
||||
"source_matching_limit": "plaintext metadata/reference matches only; absence does not establish missing encrypted or external records or absence of real market supply"
|
||||
},
|
||||
"source_inventory": {
|
||||
"accessible_files_in_two_source_roots": 13,
|
||||
"all_inventoried_before_selection": true,
|
||||
"known_originals": [
|
||||
{
|
||||
"alias": "companies.xlsx",
|
||||
"file_name": "EMPRESAS_LIST 2014-2022.xlsx",
|
||||
"sha256": "1ba4c4114b16b407e7746712d00d4f681246bbbc1b42351e96a31f7129acad30",
|
||||
"bytes": 26586267,
|
||||
"unchanged": true,
|
||||
"sheet_names": [
|
||||
"EMPRESAS 2014-2022"
|
||||
],
|
||||
"prior_nonempty_XML_rows": 148557,
|
||||
"row_count_is_not_unique_business_count": true,
|
||||
"period": "2014–2022 stated in file name"
|
||||
},
|
||||
{
|
||||
"alias": "licensed.xlsx",
|
||||
"file_name": "Empresas Licenciadas.xlsx",
|
||||
"sha256": "1b5087b464e39e88c42def39655c2391aaaf684b4c832d6a77ff274b98889f00",
|
||||
"bytes": 34940776,
|
||||
"unchanged": true,
|
||||
"sheet_names": [
|
||||
"EMPRESAS"
|
||||
],
|
||||
"prior_nonempty_XML_rows": 212632,
|
||||
"row_count_is_not_unique_business_count": true,
|
||||
"period": "varies by record;reference date not inferred from file modification time"
|
||||
},
|
||||
{
|
||||
"alias": "tourism.xlsx",
|
||||
"file_name": "CADASTRO DE EMPREENDIMENTOS TURISTICOS 3, 4 E 5 ESTRELAS 30.10.24 18,00h.xlsx",
|
||||
"sha256": "f02e086ed4973ef17234aa6cc54b98fbb7a6952ffb16ec4dc4f7d0c72af6400c",
|
||||
"bytes": 73955,
|
||||
"unchanged": true,
|
||||
"sheet_names": [
|
||||
"1. Alojamento",
|
||||
"Folha1"
|
||||
],
|
||||
"prior_nonempty_XML_rows": 460,
|
||||
"row_count_is_not_unique_business_count": true,
|
||||
"period": "30/10/2024 stated in file name;current operating status unconfirmed"
|
||||
},
|
||||
{
|
||||
"alias": "cae.xlsx",
|
||||
"file_name": "CAE eBAU.xlsx",
|
||||
"sha256": "83b4f1200988e99834c2ae3915346fbf237d128308554b67b5a080d4956bf506",
|
||||
"bytes": 75018,
|
||||
"unchanged": true,
|
||||
"sheet_names": [
|
||||
"CAE"
|
||||
],
|
||||
"prior_nonempty_XML_rows": null,
|
||||
"row_count_is_not_unique_business_count": true,
|
||||
"period": "code labels supplied;version/application require source correspondence"
|
||||
}
|
||||
],
|
||||
"duplicates_detected_by_bytes": [],
|
||||
"private_fields_exported": false,
|
||||
"tourism_scope": "workbook labelled accommodation;not treated as an exhaustive restaurant or hotel market census",
|
||||
"restaurant_scope": "separate restaurant dataset not identified in these two source roots;restaurant activities may exist within company registers;not a finding of absence in the market"
|
||||
},
|
||||
"findings": {
|
||||
"originals_available_and_unchanged": true,
|
||||
"loading_into_portal": "not demonstrated in audited destination;no exact known source hash matches among368persistent files;no exact known source references in audited metadata",
|
||||
"normalisation_and_classification": "source company workbooks have province/district/CAE and a supplied CAE code table;mapping of their entities to portal location/sector/activity not demonstrated",
|
||||
"journey_linkage": "not demonstrated;installed ProactiveInvestment.options only returns document links/groups/project-cluster-corridor-park-chain references;no list-backed offer query or company/hotel/restaurant relation consumer in that method",
|
||||
"manual_evidence_path": "already delivered;acceptance preserved. Manual source/locator/period/coverage/observed_supply fields do not establish imported source records or catalogue-backed comparison.",
|
||||
"active_counts_context_only": {
|
||||
"existing_businesses": 0,
|
||||
"organizations": 1,
|
||||
"documents": 60,
|
||||
"economic_activities": 11,
|
||||
"portal_resources": 59
|
||||
},
|
||||
"no_market_absence_inference": true,
|
||||
"no_loss_claim": true
|
||||
},
|
||||
"demonstration_attempt": {
|
||||
"kind": "read-only trace and canonical-location check,not a browser or team end-to-end proof",
|
||||
"location": {
|
||||
"name": "Gurué",
|
||||
"id": "87a8ba3d-b152-560c-a566-7db4531ad536",
|
||||
"type": "district"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step": 1,
|
||||
"action": "choose location",
|
||||
"observed": "canonical Gurué district exists;previous location-selection path acceptance reused;no new manual UI proof claimed",
|
||||
"status": "location_reference_available"
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"action": "consult existing supply from these lists",
|
||||
"observed": "no wired list-backed offer consumer or demonstrated imported/classified enterprise records for this location",
|
||||
"status": "not_demonstrated",
|
||||
"blocks_remaining_demo": true
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"action": "compare with an idea for competitors/complements/partners",
|
||||
"observed": "manual analysis fields exist;no reproducible offer-result set/source-row linkage demonstrated",
|
||||
"status": "not_demonstrated"
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"action": "identify a need with dates and uncertainties",
|
||||
"observed": "cannot infer unmet need from missing catalogue records;hypothesis and unknown coverage remain explicit",
|
||||
"status": "not_demonstrated_for_this_list_backed_flow"
|
||||
},
|
||||
{
|
||||
"step": 5,
|
||||
"action": "prepare opportunity retaining sources/period/coverage/uncertainty",
|
||||
"observed": "existing private preparation mechanism preserved;no new opportunity created to simulate completion of missing steps2–4",
|
||||
"status": "not_demonstrated_for_this_list_backed_flow"
|
||||
}
|
||||
],
|
||||
"end_to_end_criterion_passed": false
|
||||
},
|
||||
"preservation": {
|
||||
"proactive_delivery_preserved": true,
|
||||
"previous_manual_real_source_QA_pilots_preserved": true,
|
||||
"Songo_Gurue": "existing private QA hypotheses unchanged;their manual spreadsheet reading does not establish list integration",
|
||||
"L8_delivery_569a1cd7": "unchanged,remains partial",
|
||||
"application_code_changes": 0,
|
||||
"new_tests": 0,
|
||||
"new_imports_or_normalisation": 0,
|
||||
"new_opportunities": 0,
|
||||
"new_publication": 0,
|
||||
"active_writes": 0,
|
||||
"new_roles_or_grants": 0,
|
||||
"no_new_matrix_or_backlog": true
|
||||
},
|
||||
"next_concrete_work": "Establish traceable private source batches/rows and mappings using existing data/inventory mechanisms;expose authorised location-sector-activity supply query in existing proactive editor;link selected supply records and source coverage to comparison/need/opportunity,then demonstrate save/recover end-to-end in isolated QA. Do not publish a directory or treat unknown coverage as zero supply. Analytical supply records must not require a user organisation,a business-for-sale listing,a real promoter or a confirmed commercial commitment just to be consulted internally.",
|
||||
"matrix_ids": [
|
||||
"RF-CLU-003",
|
||||
"MASTER-471012ab-0010",
|
||||
"M2-d0c06a75-0147",
|
||||
"M2-d0c06a75-0151",
|
||||
"MASTER-1f0a4cde-0002"
|
||||
],
|
||||
"evidence": {
|
||||
"active_source_code_verified": [
|
||||
{
|
||||
"file": "app/Services/ProactiveInvestment.php",
|
||||
"sha256": "957dc455f0153d615577fe182b51969e6d253b33e24ac78547fcccd06215a51c",
|
||||
"installed_matches_Git": true
|
||||
},
|
||||
{
|
||||
"file": "app/Services/OpportunityManagement.php",
|
||||
"sha256": "ebe5535d94ad3c7d607bd70f2ff5dce75d810ab26924ac55ac08f036fd5eee33",
|
||||
"installed_matches_Git": true
|
||||
},
|
||||
{
|
||||
"file": "resources/js/components/proactive-investment.tsx",
|
||||
"sha256": "9c6acadef8d29732efcfcc4bcdfb28ce3be4f19127a626c1e85c6df98fd91ecf",
|
||||
"installed_matches_Git": true
|
||||
},
|
||||
{
|
||||
"file": "app/Services/InvestorContext.php",
|
||||
"sha256": "54be88361214d896640c808d8fe29f93c552a69870d6bb3ecce4de458a51ed48",
|
||||
"installed_matches_Git": true
|
||||
},
|
||||
{
|
||||
"file": "routes/web.php",
|
||||
"sha256": "95930c76977d446b74a422acc4b60869ff6bae5d4567c21cc644484c5f09cbda",
|
||||
"installed_matches_Git": true
|
||||
}
|
||||
],
|
||||
"local_artifacts": {
|
||||
"accessible-source-inventory.json": "2815b778b0a4d1d601fd85c46c9b5c3a19bd7adcdb46cf86f0f28f45717b2afa",
|
||||
"accepted-source-check.json": "06b814ee4e322fb78f081788b061936a324b3edcf6d56a80a82eeabdf2906ec6",
|
||||
"active-audit.json": "06e3cfba8c180e93a86a9dc5db86403220e7c5406229ef166b4e604cc2d8e71c",
|
||||
"consumer-trace.json": "df1fa6fa8dc3c18485b6e83a4f6a688f2d5a2124e9a8f5c75a944ec859e420f3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"date": "2026-10-28",
|
||||
"status": "internal_supply_integration_demonstrated_in_isolated_QA_not_installed",
|
||||
"baseline": "150bef4b897de8d0711c1e2bbeedbcaf5ee241d2b",
|
||||
"baseline_runtime": "569a1cd750f3d2a0b87f130e992e73e070bbd1ab",
|
||||
"branch": "codex/proactive-supply-qa-20261028",
|
||||
"active_runtime_unchanged": "569a1cd750f3d2a0b87f130e992e73e070bbd1ab",
|
||||
"accepted_inventory_reused": "existing-supply-lists-audit-20261028.json;13 accessible originals/variants inventoried before source selection;prior proactive acceptance and fiscal delivery preserved",
|
||||
"sources": [
|
||||
{
|
||||
"alias": "companies.xlsx",
|
||||
"name": "EMPRESAS_LIST 2014-2022.xlsx",
|
||||
"sha256": "1ba4c4114b16b407e7746712d00d4f681246bbbc1b42351e96a31f7129acad30",
|
||||
"bytes": 26586267,
|
||||
"period": "2014–2022; data por registo quando disponível"
|
||||
},
|
||||
{
|
||||
"alias": "licensed.xlsx",
|
||||
"name": "Empresas Licenciadas.xlsx",
|
||||
"sha256": "1b5087b464e39e88c42def39655c2391aaaf684b4c832d6a77ff274b98889f00",
|
||||
"bytes": 34940776,
|
||||
"period": "Data por registo; cobertura temporal global não determinada"
|
||||
},
|
||||
{
|
||||
"alias": "tourism.xlsx",
|
||||
"name": "CADASTRO DE EMPREENDIMENTOS TURISTICOS 3, 4 E 5 ESTRELAS 30.10.24 18,00h.xlsx",
|
||||
"sha256": "f02e086ed4973ef17234aa6cc54b98fbb7a6952ffb16ec4dc4f7d0c72af6400c",
|
||||
"bytes": 73955,
|
||||
"period": "30/10/2024 (nome do exemplar); situação actual não confirmada"
|
||||
},
|
||||
{
|
||||
"alias": "cae.xlsx",
|
||||
"name": "CAE eBAU.xlsx",
|
||||
"sha256": "83b4f1200988e99834c2ae3915346fbf237d128308554b67b5a080d4956bf506",
|
||||
"bytes": 75018,
|
||||
"period": "Versão não indicada"
|
||||
}
|
||||
],
|
||||
"source_rows": {
|
||||
"companies.xlsx": 140847,
|
||||
"licensed.xlsx": 201917,
|
||||
"tourism.xlsx": 277
|
||||
},
|
||||
"catalog": {
|
||||
"records": 343041,
|
||||
"repeated_projection": 139591,
|
||||
"identity_conflicts": 113154,
|
||||
"activity_unclassified": 80796,
|
||||
"territory_unmapped": 198112,
|
||||
"province_only": 325
|
||||
},
|
||||
"duplicate_meaning": "same projected administrative fields including entity identifier, licence/process,date,place,activity,name;all source sightings retained,not a unique-company count or byte-identical-file claim",
|
||||
"classification": {
|
||||
"activity_and_sector": "supplied CAE labels and SELF_ID hierarchy;no invented correspondence to twelve portal navigation sectors",
|
||||
"territory": "unique normalized province+district,otherwise exact unique canonical subordinate name within province;Maputo explicitly distinct from Maputo Cidade;no fuzzy matching;raw source labels retained",
|
||||
"identity": "valid-shaped NUIT only hashed as a possible entity key;different names flagged,not silently merged;unidentified rows remain distinct",
|
||||
"dates": "original date strings and Excel serials retained verbatim;file modification time not a reference date",
|
||||
"tourism": "277 establishment observations across two differently structured sheets;explicit province section preserved;missing district remains unknown;not an exhaustive hotels/restaurants census"
|
||||
},
|
||||
"private_import": {
|
||||
"preview": true,
|
||||
"original_hashes_checked": true,
|
||||
"malware": "Microsoft Defender custom scan on same-hash private copies,exit0,no threats detected,2026-10-28",
|
||||
"idempotency": {
|
||||
"before": 343041,
|
||||
"after": 343041,
|
||||
"sha256": "6a9c820c73a8c535ed3cdfc03db0311edf0300f1f97ade75f0fce3d3c84a02e1",
|
||||
"publication": false,
|
||||
"database": "proactive_investment_20261027_test",
|
||||
"port": 55441
|
||||
},
|
||||
"tables": [
|
||||
"proactive_supply_sources",
|
||||
"proactive_supply_observations"
|
||||
],
|
||||
"RLS": "forced on both;non-owner QA policy proof:0 rows without actor/read context;authorized context recovers row",
|
||||
"contacts_identity_documents_nuit_not_returned_by_query": true,
|
||||
"no_automatic_directory_publication": true,
|
||||
"no_promoters_or_user_organizations_created": true
|
||||
},
|
||||
"proactive_delta": "private authenticated territory query,CAE sector/activity filters,pagination,source/row/date/hash;select existing observations,compare competition/complementarity/possible partner/uncertain with rationale and limitations;server resolves source lineage,keeps list-derived evidence internal;save/reload and private dossier preserve references",
|
||||
"gurue": {
|
||||
"date": "2026-10-28",
|
||||
"QA_only": true,
|
||||
"operator": 1021,
|
||||
"record_id": 402,
|
||||
"dossier_id": "883c8ec3-59a3-4074-b91c-b7d4a5d88ff0",
|
||||
"prior_version": 1,
|
||||
"offer_query_total": 410,
|
||||
"selected_source_records": 2,
|
||||
"comparison_relationships": [
|
||||
"competition",
|
||||
"complementarity"
|
||||
],
|
||||
"existing_five_evidences_preserved": true,
|
||||
"total_evidence": 7,
|
||||
"idea_preserved": true,
|
||||
"need": "hypothesis",
|
||||
"empty_results": "insufficient_information",
|
||||
"save_reload_recovered": true,
|
||||
"existing_dossier_preview_confirm_replay": true,
|
||||
"anonymous_internal_query_denied": true,
|
||||
"no_publication": true,
|
||||
"no_external_contacts": true,
|
||||
"errors": [],
|
||||
"source_version": 2,
|
||||
"context_revision": 2,
|
||||
"deliberate_dossier_title_and_fields_preserved": true,
|
||||
"prior_source_v1_history_retained": true,
|
||||
"songo_401_unchanged": true
|
||||
},
|
||||
"verification": {
|
||||
"delta_tests": 7,
|
||||
"assertions": 62,
|
||||
"types": "passed",
|
||||
"QA_build": "passed",
|
||||
"accepted_prior_proofs": "reused;no fiscal suites repeated",
|
||||
"browser": "authenticated synthetic operator1021,real supplied source rows,save/reload,existing dossier preview-confirm-replay;not team acceptance",
|
||||
"preservation": {
|
||||
"opportunities": {
|
||||
"baseline_rows": 55,
|
||||
"new_rows": 0,
|
||||
"expected_changes": [
|
||||
"402"
|
||||
]
|
||||
},
|
||||
"desk_cases": {
|
||||
"baseline_rows": 20,
|
||||
"new_rows": 0,
|
||||
"expected_changes": [
|
||||
"883c8ec3-59a3-4074-b91c-b7d4a5d88ff0"
|
||||
]
|
||||
},
|
||||
"projects": {
|
||||
"baseline_rows": 19,
|
||||
"new_rows": 0,
|
||||
"expected_changes": []
|
||||
},
|
||||
"geo_locations": {
|
||||
"baseline_rows": 1921,
|
||||
"new_rows": 0,
|
||||
"expected_changes": []
|
||||
},
|
||||
"sectors": {
|
||||
"baseline_rows": 12,
|
||||
"new_rows": 0,
|
||||
"expected_changes": []
|
||||
},
|
||||
"documents": {
|
||||
"baseline_rows": 64,
|
||||
"new_rows": 0,
|
||||
"expected_changes": []
|
||||
},
|
||||
"value_chains": {
|
||||
"baseline_rows": 0,
|
||||
"new_rows": 0,
|
||||
"expected_changes": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"remaining": [
|
||||
"No installation or import in active portal;there the list-backed use remains not demonstrated until delivery.",
|
||||
"198112 observations lack a unique canonical territorial match,including19 ambiguous names;325 have province only. Raw source territory remains available in private originals.",
|
||||
"80796 observations lack an exact supplied CAE activity label;do not impute sector/activity.",
|
||||
"113154 observations have differing names for a shared valid-shaped identifier;identity needs reconciliation,not automatic merger.",
|
||||
"Historical administrative records do not establish current operating status,capacity,prices,demand,contracts or market vacancy.",
|
||||
"National mapping/classification completeness and actual team use remain partial;Gurué proof does not close all market-study/cluster requirements.",
|
||||
"Active delivery requires capability/compatibility review,fresh backup+restore,authenticated operation and installed delta check through the existing Git procedure."
|
||||
],
|
||||
"restriction_origins": {
|
||||
"internal_analysis_no_auto_publication_no_absence_inference": "explicit Tempo task",
|
||||
"preservation_QA_active_separation_backup_restore": "permanent AGENTS instructions",
|
||||
"canonical_unique_matching_hashed_keys_query_limits_RLS": "implementation choices,not new organizational approval requirements"
|
||||
},
|
||||
"proof_files": [
|
||||
"outputs/proactive-supply-20261028/gurue-ui-proof.json",
|
||||
"outputs/proactive-supply-20261028/preservation-proof.json",
|
||||
"outputs/proactive-supply-20261028/tests-receipt.json",
|
||||
"outputs/proactive-supply-20261028/dossier-visual-proof.json"
|
||||
],
|
||||
"L8": "unchanged,partial;this increment has no fiscal calculation change"
|
||||
}
|
||||
@@ -7194,3 +7194,45 @@ A falta de prova do caso (titular/rendimento/imposto/inclusão/uso/regra do per
|
||||
Recibo consolidado:`docs/investmoz/simulations/irpc-l8-grouped-delivery-20261028.json`. Cópia remota:`/var/backups/investmoz-irpc-l8-grouped-20261028/installed-receipt.json`. Provas locais:`outputs/irpc-l8-grouped-delivery-20261028/`. Actualização documental segue ramo próprio de recibos,sem alterar novamente a revisão servida.
|
||||
|
||||
Cópia remota do recibo conferida:SHA256`b70fe154f95162f029b5ca712b41962757360d73b0ff07f52b8ce14848d990df`;a matriz existente recebeu saldoL8 nos camposEstado,Disponibilidade activa,Evidências,Critérios restantes eDependências,só em `AUD-SIM-INT02`.
|
||||
|
||||
|
||||
## 28/10/2026 — listas como base interna da oferta: uso não demonstrado
|
||||
|
||||
**Decisão expressa da Tempo e resultado da conferência: a utilização das listas de empresas,hotéis/restaurantes como base comparativa no portal é NÃO DEMONSTRADA.** A entrega do percurso proactivo,os seus campos de evidência e os pilotos privados anteriores mantêm-se aceites;isso não demonstra carregamento,classificação e ligação destas listas ao consumidor de análise. O âmbito é primeiro a análise interna,não a publicação automática de um directório.
|
||||
|
||||
**Fonte e âmbito conferidos:**13ficheiros inventariados nos dois directórios acessíveis de empresas/relatórios,antes da selecção;originais `EMPRESAS_LIST2014–2022`, `Empresas Licenciadas`,cadastro turístico30/10/2024 e `CAE eBAU` localizados e iguais aos hashes aceites. As148557/212632/460linhasXML não vazias dos recibos anteriores são observações dos ficheiros,não empresas únicas nem carga no portal. Empresas têm província/distrito/CAE;isso não prova o mapeamento no portal. O cadastro turístico observado é de alojamento,não um censo completo da restauração. Não foi identificada lista autónoma de restaurantes nestes dois directórios;isso não significa ausência de restaurantes ou de actividades de restauração nas listas empresariais. Contactos e outros campos privados não foram publicados.
|
||||
|
||||
**Destino entregue:**código `569a1cd7`,base `teste_investidor`,198tabelas não sistémicas(174public+24skills_edu),consultadas em transacções READ ONLY com identidade técnicapostgres explicitada. Zero correspondências dos hashes originais entre368ficheiros persistentes e nenhuma correspondência exacta dos nomes/hashes nas referências pesquisadas. Cadastros `existing_businesses` e `investment_company_declarations` têm0registos nesse destino;organizações1,actividades11,documentos60 e recursos59 não são convertidos em oferta empresarial das listas. Esta é uma fotografia técnica,não prova de visibilidade para uma conta operacional,perda de registos ou ausência de oferta no mercado. Registos antigos de `investemoz` e basesQA são ambientes distintos;nenhuma eliminação é declarada. Pesquisa por nomes/hashes em metadados não exclui ficheiros renomeados,arquivados,conteúdo cifrado ou outros acervos.
|
||||
|
||||
**Causa delimitada no consumidor instalado:**`ProactiveInvestment::options` devolve documentos/grupos e referências a projecto,cluster,corredor,parque,cadeia;os hashes dos ficheiros instalados coincidem comGit. O editor recebe `observed_supply` e fontes/localizadores/períodos/cobertura como texto manual;as relações proactivas não admitem entidade empresarial destas listas. Não foi encontrada consulta de oferta carregada por localização/sector/actividade nem ligação de linhas das listas a concorrentes,complementares ou parceiros. A possível existência de cadastros noutro módulo não demonstra consumo por este percurso.
|
||||
|
||||
| Percurso de conferência — Gurué,distrito canónico | Resultado |
|
||||
| --- | --- |
|
||||
| Escolher localização | Referência canónica localizada;prova anterior de selecção territorial preservada,não se declara nova prova de interface. |
|
||||
| Consultar oferta existente das listas | Não demonstrado:consulta/importação/classificação consumida pelo percurso não identificada. A demonstração pára neste ponto. |
|
||||
| Comparar com uma ideia | Não demonstrado sobre conjunto rastreável de registos dessas listas. Campos manuais não equivalem à integração. |
|
||||
| Identificar necessidade | Não demonstrado por este fluxo;ausência de registo não confirma necessidade nem ausência de concorrência. Manter hipótese,período,cobertura e incertezas. |
|
||||
| Preparar oportunidade | Mecanismo privado entregue preservado;nenhuma oportunidade nova foi criada para simular passos anteriores ainda não demonstrados. |
|
||||
|
||||
**Critério de retoma:**lotes/linhas privados rastreáveis e classificados,reutilizando os mecanismos/cadastros existentes;consulta autorizada da oferta por localização/sector/actividade;comparação da ideia com concorrência,complementares e possíveis parceiros,com fontes/datas/cobertura;necessidade fundamentada sem inferir ausência de oferta;e prévia/gravação/recuperação da oportunidade no percurso existente emQA isolada. Só essa conferência pode demonstrar o uso;nenhum novo aprovador organizacional ou directório público é criado.
|
||||
|
||||
Sem imports,normalização,alteração de código,permissões,publicação ou escrita em dados activos. Songo/Gurué privados,aprovações/históricos e entregaL8 preservados. Registo:`docs/investmoz/unification/existing-supply-lists-audit-20261028.json`. Cobertura actualizada nas linhas existentesRF-CLU-003,MASTER-471012ab-0010,M2-d0c06a75-0147/0151 eMASTER-1f0a4cde-0002,sem nova matriz/backlog.
|
||||
|
||||
A base interna de oferta não exige transformar cada empresa numa organização utilizadora,anúncio de venda/parceria,promotor ou cliente confirmado. O cadastro e a consulta analítica são distintos da operação comercial e da divulgação.
|
||||
|
||||
|
||||
## 28/10/2026 — oferta existente ligada à análise proactiva, demonstrada em QA
|
||||
|
||||
Incremento sobre o código entregue `569a1cd7`, ramo `codex/proactive-supply-qa-20261028`: **demonstrado em QA isolada, sem instalação nem importação no activo**. O relatório de conferência anterior permanece histórico: no destino activo, o uso das listas continua não demonstrado. Reutilizam-se a aceitação do percurso, o inventário de 13 ficheiros, os originais, as fontes e as provas anteriores; não se reabre a decisão da Tempo nem se encerra o estudo de mercado nacional.
|
||||
|
||||
Carregadas privadamente **343.041 observações**: 140.847 da lista 2014–2022, 201.917 de empresas licenciadas e 277 do cadastro turístico (duas folhas com estruturas distintas). Os quatro hashes dos originais e do CAE foram conferidos. Pré-visualização, análise Microsoft Defender sem ameaças e repetição idempotente 343.041→343.041 demonstradas. Os 139.591 registos com a mesma projecção administrativa são conservados como repetições; não representam empresas únicas nem duplicados de ficheiros por bytes. As 113.154 observações com denominações divergentes para um identificador partilhado ficam assinaladas, sem fusão automática. Contactos, documentos de identidade e NUIT não são devolvidos pela consulta.
|
||||
|
||||
Consulta interna por localização canónica (incluindo descendentes), sector CAE e actividade; resultados paginados conservam fonte, folha/linha, hash e data original. O sector provém da hierarquia SELF_ID do CAE fornecido, sem inventar correspondência com os sectores de navegação. A selecção permite comparar possível concorrência, complementaridade, parceiro ou relação por determinar, com fundamento e incertezas. O servidor recupera a origem, impede reclassificar a evidência interna como pública e conserva-a no rascunho e no dossier privado. Não transforma a lista em anúncios, promotores, clientes confirmados ou publicação de directório.
|
||||
|
||||
**Gurué:** o operador QA sintético 1021 consultou 410 observações distintas, comparou dois registos reais (serviços agrícolas CAE 01610 e comércio de plantas/sementes/fertilizantes CAE 47732) com a hipótese já preparada de apoio à colheita e qualidade da folha verde. A oferta possível exige verificar sobreposição e colaboração, sem presumir mercado vazio. Rascunho 402 passa de v1 a v2, mantendo integralmente as cinco evidências anteriores, a ideia e o histórico. O mesmo dossier `883c8ec3-59a3-4074-b91c-b7d4a5d88ff0` passa de contexto1 a2 por pré-visualização e confirmação idempotente, preservando título e campos deliberados. A necessidade permanece **hipótese**, sem procura, falta de oferta ou viabilidade confirmadas. Gravação, recarregamento, acesso privado e resultado vazio como **informação insuficiente** demonstrados no navegador. Isto é prova técnica, sem substituir observação/aceitação da equipa.
|
||||
|
||||
**Cobertura e pendências:** 198.112 observações sem correspondência territorial canónica única (incluindo 19 ambiguidades), 325 apenas com província e 80.796 sem designação de actividade CAE correspondente. Mantêm-se os rótulos originais e a incerteza; não há atribuição territorial aproximada nem preenchimento presumido. O cadastro de alojamento não é um censo exaustivo de hotéis/restaurantes. Situação operacional actual, capacidade, preços, procura e compromissos continuam por demonstrar. O incremento nacional é parcial; não encerra os requisitos amplos de mercado, clusters ou liquidação anual. **L8 permanece parcial e inalterado.**
|
||||
|
||||
Sete testes dirigidos/62 asserções, TypeScript e compilação QA passaram. As duas tabelas internas têm RLS forçada; teste com identidade sem privilégios confirma ocultação sem contexto de actor e leitura com contexto autorizado. Conferidos os hashes de todas as linhas anteriores nos sete conjuntos do inventário: só o rascunho402 e o mesmo dossier sofreram o delta intencional; Songo401, projectos, territórios, sectores, documentos e cadeias foram conservados. Provas fiscais aceites reutilizadas, sem repetir as suas suites.
|
||||
|
||||
Actualizados os cinco requisitos existentes `RF-CLU-003`, `MASTER-471012ab-0010`, `M2-d0c06a75-0147`, `M2-d0c06a75-0151` e `MASTER-1f0a4cde-0002`, preservando o restante estado e pendências. Nenhuma matriz ou backlog novo. A entrega activa futura mantém conferência de capacidade/compatibilidade com main, backup/restauro fresco, operação autenticada e delta instalado. Relatório: `docs/investmoz/unification/proactive-existing-supply-qa-20261028.json`.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,30 +1,34 @@
|
||||
import { createOperationKey } from '@/lib/operation-key';
|
||||
import { Field, control, button } from '@/pages/management/shared';
|
||||
import {ProactiveSupplyBrowser} from './proactive-supply';
|
||||
|
||||
export type ProactiveEvidence = {id:string;category:string;purpose:'local'|'methodology';statement:string;interpretation:string;source:string;locator:string;period:string;scope:string;coverage:string;verified_on:string;access:'internal'|'public';document_id:string};
|
||||
export type ProactiveEvidence = {id:string;observation_id?:string;category:string;purpose:'local'|'methodology';statement:string;interpretation:string;source:string;locator:string;period:string;scope:string;coverage:string;verified_on:string;access:'internal'|'public';document_id:string};
|
||||
export type ProactiveAnalysis = {
|
||||
version:1;kind:'exists'|'needs'|'ideas';evidence:ProactiveEvidence[];
|
||||
need:{state:'hypothesis'|'confirmed';problem:string;observed_supply:string;observed_demand:string;type:string;clients:string;evidence_ids:string[];limitations:string;missing:string;confirmation:string};
|
||||
idea:{solution:string;activity:string;chain_id:string;stage:string;coverage:string;mode:string;resources:string;assumptions:string;conditions:string;pending:string;next_action:string};
|
||||
relations:{id:string;kind:string;target_id:string;relation:string;reference:string;evidence_ids:string[]}[];
|
||||
supply_comparisons?:{observation_id:string;relationship:'competition'|'complementarity'|'partner'|'uncertain';rationale:string;limitations:string}[];
|
||||
};
|
||||
export type ProactiveOptions = {documents?:{id:string;title:string}[];groups:Record<string,string>;references:Record<string,{id:string;name:string}[]>};
|
||||
export type ProactivePublic = {version?:number;group:string;state:string;need:string;proposal:string;basis:string;limitations:string;next_action:string;evidence_ids:string[];chain_id?:string|null;stage?:string|null;notice?:string;resources?:string;assumptions?:string;conditions?:string;relation_ids?:string[];relations?:{kind:string;target_id:string;label:string;relationship:string;basis:string}[];evidence?:Partial<ProactiveEvidence>[]};
|
||||
export type ProactivePublic = {version?:number;group:string;state:string;need:string;proposal:string;basis:string;limitations:string;next_action:string;evidence_ids:string[];chain_id?:string|null;stage?:string|null;notice?:string;resources?:string;assumptions?:string;conditions?:string;relation_ids?:string[];relations?:{kind:string;target_id:string;label:string;relationship:string;basis:string}[];evidence?:Partial<ProactiveEvidence>[];internal_comparisons?:{relationship:string;rationale:string;limitations:string;statement?:string;source?:string;locator?:string;period?:string}[]};
|
||||
export const proactiveNotice = 'Ausência de registo não comprova ausência de oferta. Proximidade não comprova contrato, participação, energia acessível, terra disponível ou benefício fiscal. Necessidade confirmada não comprova viabilidade comercial; hipótese de fornecimento não é encomenda.';
|
||||
export const newProactive = ():ProactiveAnalysis => ({version:1,kind:'ideas',evidence:[],need:{state:'hypothesis',problem:'',observed_supply:'',observed_demand:'',type:'',clients:'',evidence_ids:[],limitations:'',missing:'',confirmation:''},idea:{solution:'',activity:'',chain_id:'',stage:'',coverage:'',mode:'',resources:'',assumptions:'',conditions:'',pending:'',next_action:''},relations:[]});
|
||||
export const newPublicProactive = ():ProactivePublic => ({group:'ideas',state:'hypothesis',need:'',proposal:'',basis:'',limitations:'',next_action:'',evidence_ids:[],chain_id:'',stage:'',resources:'',assumptions:'',conditions:'',relation_ids:[]});
|
||||
|
||||
export function ProactiveInvestmentEditor({value,onChange,options,history=[]}:{value?:ProactiveAnalysis;onChange:(value:ProactiveAnalysis)=>void;options?:ProactiveOptions;history?:{version:number;at:string;state:string;problem:string;confirmation:string}[]}){
|
||||
export function ProactiveInvestmentEditor({value,onChange,options,history=[],territoryId,recordId}:{value?:ProactiveAnalysis;onChange:(value:ProactiveAnalysis)=>void;options?:ProactiveOptions;history?:{version:number;at:string;state:string;problem:string;confirmation:string}[];territoryId?:string;recordId?:number}){
|
||||
if(!value)return <section className="rounded border p-4"><h2 className="font-semibold">Análise territorial e preparação de ideias</h2><p>Comece pelo território e sector existentes, mesmo sem oportunidade publicada, promotor, montante ou coordenadas. O rascunho e a publicação são independentes.</p><button type="button" className={button} onClick={()=>onChange(newProactive())}>Preparar análise e ideia</button></section>;
|
||||
const patch=(p:Partial<ProactiveAnalysis>)=>onChange({...value,...p});
|
||||
const removeEvidence=(id:string)=>{const proof=value.evidence.find(e=>e.id===id);patch({evidence:value.evidence.filter(e=>e.id!==id),need:{...value.need,evidence_ids:value.need.evidence_ids.filter(x=>x!==id)},relations:value.relations.map(r=>({...r,evidence_ids:r.evidence_ids.filter(x=>x!==id)})),...(proof?.observation_id?{supply_comparisons:(value.supply_comparisons??[]).filter(c=>c.observation_id!==proof.observation_id)}:{})});};
|
||||
const text=(label:string,v:string,change:(v:string)=>void)=><Field label={label}><textarea className={control} aria-label={label} value={v??''} onChange={e=>change(e.target.value)}/></Field>;
|
||||
const select=(label:string,v:string,choices:Record<string,string>,change:(v:string)=>void)=><Field label={label}><select className={control} aria-label={label} value={v??''} onChange={e=>change(e.target.value)}><option value="">Por determinar</option>{Object.entries(choices).map(([key,label])=><option key={key} value={key}>{label}</option>)}</select></Field>;
|
||||
const evidenceChoice=(selected:string[],change:(v:string[])=>void)=><fieldset><legend>Evidências que fundamentam esta entrada</legend>{value.evidence.map((e,i)=><label key={e.id} className="block"><input type="checkbox" checked={selected.includes(e.id)} onChange={event=>change(event.target.checked?[...selected,e.id]:selected.filter(id=>id!==e.id))}/> Evidência {i+1}: {e.statement||'Por descrever'} ({e.access==='public'?'Pública autorizada':'Interna'})</label>)}</fieldset>;
|
||||
return <section className="space-y-4 rounded border p-4" aria-label="Análise territorial e ideia">
|
||||
<h2 className="text-xl font-semibold">Território e sector → evidências → necessidade → ideia</h2><details><summary>Como fundamentar esta análise</summary><p>Inventarie actividades, recursos, infra-estruturas e serviços de apoio. Confronte oferta observada com procura documentada e identifique os clientes ou beneficiários. Registe o período, cobertura e limitações de cada observação. Use referências internacionais para orientar as perguntas; a confirmação do caso depende de evidência local.</p></details><p>Reutiliza o sector e a localização principal escolhidos nesta ficha. Guarde pelo botão de rascunho existente. Nenhum montante, promotor ou ponto exacto é exigido.</p><p>{proactiveNotice}</p>
|
||||
{select('Tipo de conteúdo em preparação',value.kind,{exists:'O que existe',needs:'Necessidade identificada',ideas:'Ideia de investimento'},v=>patch({kind:v as ProactiveAnalysis['kind']}))}
|
||||
<ProactiveSupplyBrowser value={value} onChange={onChange} territoryId={territoryId} recordId={recordId}/>
|
||||
<fieldset className="space-y-3 rounded border p-3"><legend>Evidências e proveniência</legend><p>Registe factos e interpretações em campos separados. Listas de empresas, hotéis, restaurantes, contactos e documentos internos permanecem internos; não são copiados para a divulgação.</p>
|
||||
{value.evidence.map((e,i)=>{const change=(p:Partial<ProactiveEvidence>)=>patch({evidence:value.evidence.map(x=>x.id===e.id?{...x,...p}:x)});return <fieldset key={e.id} className="space-y-2 rounded border p-3"><legend>Evidência {i+1}</legend>
|
||||
{value.evidence.map((e,i)=>{const change=(p:Partial<ProactiveEvidence>)=>patch({evidence:value.evidence.map(x=>x.id===e.id?{...x,...p}:x)});if(e.observation_id)return <fieldset key={e.id} className="space-y-2 rounded border p-3"><legend>Evidência interna da oferta {i+1}</legend><p>{e.statement}</p><p>Fonte: {e.source} · {e.locator}</p><p>Período: {e.period} · Abrangência: {e.scope}</p><p>{e.coverage}</p><p>A proveniência é recuperada do original. A data indica consulta documental, sem confirmação de actividade actual.</p>{text(`Interpretação ou hipótese ${i+1}`,e.interpretation,v=>change({interpretation:v}))}<button type="button" onClick={()=>removeEvidence(e.id)}>Retirar evidência {i+1} e a sua comparação deste rascunho</button></fieldset>;return <fieldset key={e.id} className="space-y-2 rounded border p-3"><legend>Evidência {i+1}</legend>
|
||||
{select(`Finalidade da evidência ${i+1}`,e.purpose??'',{local:'Evidência local do caso',methodology:'Referência metodológica — não prova procura local'},v=>change({purpose:v as 'local'|'methodology'}))}
|
||||
{select(`Categoria da evidência ${i+1}`,e.category,{activity:'Actividade',resource:'Recurso',infrastructure:'Infra-estrutura',supply:'Oferta',demand:'Procura'},v=>change({category:v}))}
|
||||
{([['statement','Facto documentado'],['interpretation','Interpretação ou hipótese'],['source','Fonte e exemplar'],['locator','Localizador ou trecho'],['period','Período da evidência'],['scope','Abrangência'],['coverage','Cobertura e limitações']]as const).map(([key,label])=><div key={key}>{text(`${label} ${i+1}`,e[key],v=>change({[key]:v}))}</div>)}
|
||||
@@ -64,7 +68,7 @@ export function ProactiveInvestmentEditor({value,onChange,options,history=[]}:{v
|
||||
|
||||
export function ProactiveSummary({value}:{value?:ProactivePublic|null}){
|
||||
if(!value)return null;
|
||||
return <section className="space-y-2 rounded border p-4" aria-label="Origem proactiva do investimento"><h2 className="text-xl font-semibold">{value.group==='ideas'?'Ideia em preparação':value.group==='needs'?'Necessidade identificada':'O que existe'}</h2><p>{value.group==='exists'?'Registo documental; consultar abrangência, cobertura e limitações das fontes.':value.state==='confirmed'?'Necessidade confirmada no âmbito documentado':'Hipótese analítica; conclusão económica por confirmar'}</p>{([['need','Necessidade'],['proposal','Proposta'],['basis','Fundamento'],['limitations','Limitações'],['next_action','Próximo passo'],['stage','Etapa da cadeia'],['resources','Recursos'],['assumptions','Pressupostos'],['conditions','Condições']]as const).map(([key,label])=>value[key]?<p key={key}><strong>{label}:</strong> {value[key]}</p>:null)}{value.relations?.map(r=><p key={r.kind+r.target_id}>Relação seleccionada: {r.label} · {r.relationship==='documented'?'Documentada':r.relationship==='proximity'?'Proximidade, sem pertença':'Hipótese de relação'}. Fundamento: {r.basis}. Não confirma contrato, acesso ou participação.</p>)}{value.evidence?.map((e,i)=><p key={e.id??i}>Evidência pública: {e.statement}; fonte: {e.source}; localizador: {e.locator}; período: {e.period}; abrangência: {e.scope}; cobertura: {e.coverage}; verificação: {e.verified_on}.</p>)}<p>{value.notice??proactiveNotice}</p><p>Sem disponibilidade de participação, viabilidade comercial ou benefício fiscal presumidos.</p></section>;
|
||||
return <section className="space-y-2 rounded border p-4" aria-label="Origem proactiva do investimento"><h2 className="text-xl font-semibold">{value.group==='ideas'?'Ideia em preparação':value.group==='needs'?'Necessidade identificada':'O que existe'}</h2><p>{value.group==='exists'?'Registo documental; consultar abrangência, cobertura e limitações das fontes.':value.state==='confirmed'?'Necessidade confirmada no âmbito documentado':'Hipótese analítica; conclusão económica por confirmar'}</p>{([['need','Necessidade'],['proposal','Proposta'],['basis','Fundamento'],['limitations','Limitações'],['next_action','Próximo passo'],['stage','Etapa da cadeia'],['resources','Recursos'],['assumptions','Pressupostos'],['conditions','Condições']]as const).map(([key,label])=>value[key]?<p key={key}><strong>{label}:</strong> {value[key]}</p>:null)}{value.internal_comparisons?.map((c,i)=><div key={i} className="rounded border p-3"><strong>Comparação interna da oferta {i+1} · {({competition:'Possível concorrência',complementarity:'Possível complementaridade',partner:'Possível parceiro, sem compromisso',uncertain:'Relação por determinar'} as Record<string,string>)[c.relationship]}</strong><p>{c.statement}</p><p>Fonte: {c.source} · {c.locator} · {c.period}</p><p>{c.rationale}</p><p>Incertezas: {c.limitations}</p></div>)}{value.relations?.map(r=><p key={r.kind+r.target_id}>Relação seleccionada: {r.label} · {r.relationship==='documented'?'Documentada':r.relationship==='proximity'?'Proximidade, sem pertença':'Hipótese de relação'}. Fundamento: {r.basis}. Não confirma contrato, acesso ou participação.</p>)}{value.evidence?.map((e,i)=><p key={e.id??i}>Evidência pública: {e.statement}; fonte: {e.source}; localizador: {e.locator}; período: {e.period}; abrangência: {e.scope}; cobertura: {e.coverage}; verificação: {e.verified_on}.</p>)}<p>{value.notice??proactiveNotice}</p><p>Sem disponibilidade de participação, viabilidade comercial ou benefício fiscal presumidos.</p></section>;
|
||||
}
|
||||
|
||||
export function ProactivePublicationFields({value,onChange,evidence,relations=[],options,reviewed,onReview}:{value:ProactivePublic;onChange:(v:ProactivePublic)=>void;evidence:ProactiveEvidence[];relations?:{id:string;label:string;relationship:string}[];options?:ProactiveOptions;reviewed:boolean;onReview:(v:boolean)=>void}){
|
||||
@@ -86,5 +90,5 @@ export function proactiveView(raw:unknown):ProactivePublic|null{
|
||||
if(typeof p.group==='string'&&typeof p.need==='string')return p as ProactivePublic;
|
||||
const a=raw as ProactiveAnalysis;
|
||||
if(!a.need||typeof a.need.problem!=='string'||!a.idea)return null;
|
||||
return {version:1,group:a.kind,state:a.need.state,need:a.kind==='exists'?'':a.need.problem,proposal:a.kind==='ideas'?a.idea.solution:'',basis:a.need.confirmation||'Fundamento por completar',limitations:[a.need.limitations,a.need.missing,a.idea.assumptions,a.idea.conditions,a.idea.pending].filter(Boolean).join('; '),next_action:a.idea.next_action,evidence_ids:a.need.evidence_ids,chain_id:a.idea.chain_id,stage:a.kind==='ideas'?a.idea.stage:'',notice:'Origem privada autorizada deste dossier. '+proactiveNotice};
|
||||
return {version:1,group:a.kind,state:a.need.state,need:a.kind==='exists'?'':a.need.problem,proposal:a.kind==='ideas'?a.idea.solution:'',basis:a.need.confirmation||'Fundamento por completar',limitations:[a.need.limitations,a.need.missing,a.idea.assumptions,a.idea.conditions,a.idea.pending].filter(Boolean).join('; '),next_action:a.idea.next_action,evidence_ids:a.need.evidence_ids,chain_id:a.idea.chain_id,stage:a.kind==='ideas'?a.idea.stage:'',notice:'Origem privada autorizada deste dossier. '+proactiveNotice,internal_comparisons:(a.supply_comparisons??[]).map(c=>{const e=a.evidence.find(e=>e.observation_id===c.observation_id);return {...c,statement:e?.statement,source:e?.source,locator:e?.locator,period:e?.period};})};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import {useEffect,useState} from 'react';
|
||||
import {button,control,Field} from '@/pages/management/shared';
|
||||
import {createOperationKey} from '@/lib/operation-key';
|
||||
import type {ProactiveAnalysis} from './proactive-investment';
|
||||
|
||||
type Observation={id:string;name:string;province:string;district:string;territory_id:string;cae:string;activity:string;sector:string;source_date:string;sheet:string;row_number:number;source_hash:string;source_name:string;source_period:string;identity_conflict:boolean};
|
||||
type Result={installed:boolean;rows:Observation[];total:number|null;duplicate_sightings?:number;page?:number;per_page?:number;sectors?:string[];territory_coverage?:{unmapped:number;province_only:number};limitations:string;status:string};
|
||||
export function ProactiveSupplyBrowser({value,onChange,territoryId,recordId}:{value:ProactiveAnalysis;onChange:(v:ProactiveAnalysis)=>void;territoryId?:string;recordId?:number}){
|
||||
const [activity,setActivity]=useState('');const [sector,setSector]=useState('');
|
||||
const [result,setResult]=useState<Result|null>(null);const [busy,setBusy]=useState(false);const [error,setError]=useState('');
|
||||
const [query,setQuery]=useState<{territory:string;activity:string;sector:string;page:number}|null>(null);
|
||||
useEffect(()=>{setResult(null);setQuery(null);setSector('');setError('');setBusy(false);},[territoryId]);
|
||||
useEffect(()=>{
|
||||
if(!query)return;const controller=new AbortController();setBusy(true);setError('');
|
||||
const p=new URLSearchParams({territory_id:query.territory,activity:query.activity,sector:query.sector,page:String(query.page)});if(recordId)p.set('record_id',String(recordId));
|
||||
fetch('/minhas-oportunidades/oferta-interna?'+p,{headers:{Accept:'application/json'},credentials:'same-origin',signal:controller.signal})
|
||||
.then(async r=>{if(!r.ok)throw new Error(r.status===403?'Consulta interna fora do âmbito de acesso desta sessão.':'Não foi possível consultar a base interna. A oferta permanece por determinar.');return r.json() as Promise<Result>;})
|
||||
.then(setResult).catch(e=>{if(e.name!=='AbortError'){setResult(null);setError(e.message);}}).finally(()=>{if(!controller.signal.aborted)setBusy(false);});
|
||||
return ()=>controller.abort();
|
||||
},[query,recordId]);
|
||||
const selected=value.supply_comparisons??[];
|
||||
const compare=(row:Observation)=>{
|
||||
if(selected.some(c=>c.observation_id===row.id))return;
|
||||
const proof=value.evidence.find(e=>e.observation_id===row.id);
|
||||
onChange({...value,supply_comparisons:[...selected,{observation_id:row.id,relationship:'uncertain',rationale:'',limitations:'Actividade actual, capacidade, procura e disponibilidade para parceria por confirmar.'}],evidence:proof?value.evidence:[...value.evidence,{
|
||||
id:createOperationKey(),observation_id:row.id,category:'supply',purpose:'local',access:'internal',document_id:'',
|
||||
statement:`Registo administrativo: ${row.name}; CAE ${row.cae}; ${row.activity||'actividade por classificar'}; ${row.district}, ${row.province}. Não confirma actividade actual.`,
|
||||
interpretation:'',source:`${row.source_name}; SHA256 ${row.source_hash}`,locator:`${row.sheet}!linha ${row.row_number}`,
|
||||
period:`${row.source_date}; ${row.source_period}`,scope:`${row.district}, ${row.province}`,
|
||||
coverage:result?.limitations??'Cobertura incompleta; situação actual por confirmar.',verified_on:new Date().toISOString().slice(0,10),
|
||||
}]});
|
||||
};
|
||||
return <fieldset className="space-y-3 rounded border p-3"><legend>Consultar oferta existente — base interna</legend>
|
||||
<p>Consulta a localização escolhida nesta ficha e os seus territórios subordinados. Os filtros por sector e actividade CAE não limitam a análise ao sector da ideia: podem existir concorrentes, complementos e parceiros noutros sectores. Nenhuma empresa é publicada.</p>
|
||||
<Field label="Sector da oferta (classificação CAE)"><select className={control} aria-label="Sector da oferta (classificação CAE)" value={sector} onChange={e=>setSector(e.target.value)}><option value="">Todos os sectores registados</option>{result?.sectors?.map(s=><option key={s}>{s}</option>)}</select></Field>
|
||||
<Field label="Actividade ou código CAE da oferta"><input className={control} aria-label="Actividade ou código CAE da oferta" value={activity} onChange={e=>setActivity(e.target.value)}/></Field>
|
||||
<button type="button" className={button} disabled={!territoryId||busy} onClick={()=>setQuery({territory:territoryId!,activity,sector,page:1})}>{busy?'A consultar…':'Consultar oferta da localização'}</button>
|
||||
{!territoryId&&<p>Escolha a localização para consultar. Sem localização, a oferta permanece por determinar.</p>}
|
||||
{error&&<p role="alert">{error}</p>}
|
||||
{result&&<><p role="status">{result.status}</p><p>{result.limitations}</p>{result.territory_coverage&&<p>Cobertura territorial da base nacional: {result.territory_coverage.unmapped} observações sem correspondência territorial única; {result.territory_coverage.province_only} apenas com província. Estas últimas não comprovam oferta num distrito específico.</p>}{result.total!==null&&<p>{result.total} observações distintas; {result.duplicate_sightings??0} repetições dos mesmos campos administrativos conservadas na origem. Não é uma contagem de empresas únicas.</p>}
|
||||
{result.rows.map(row=><article key={row.id} className="rounded border p-3"><h3 className="font-semibold">{row.name}</h3><p>{row.district}, {row.province} · {row.sector||'Sector por classificar'} · CAE {row.cae||'não indicado'} · {row.activity||'Actividade por classificar'}</p><p>Fonte: {row.source_name} · {row.sheet}, linha {row.row_number} · {row.source_date} · {row.source_period}</p><details><summary>Rastreabilidade do exemplar</summary><p>SHA256: {row.source_hash}</p><p>Observação: {row.id}</p></details>{row.identity_conflict&&<p>Identificador partilhado por denominações divergentes; identidade por conferir. Os registos não foram fundidos.</p>}<button type="button" className={button} disabled={busy||selected.some(c=>c.observation_id===row.id)||selected.length>=40||value.evidence.length>=60} onClick={()=>compare(row)}>Comparar com a ideia</button></article>)}
|
||||
{query&&<div className="flex gap-3"><button type="button" disabled={busy||query.page<=1} onClick={()=>setQuery({...query,page:query.page-1})}>Página anterior</button><span>Página {result.page??1}</span><button type="button" disabled={busy||query.page*25>=(result.total??0)} onClick={()=>setQuery({...query,page:query.page+1})}>Página seguinte</button></div>}
|
||||
</>}
|
||||
{selected.map((c,i)=>{const proof=value.evidence.find(e=>e.observation_id===c.observation_id);const patch=(p:Partial<typeof c>)=>onChange({...value,supply_comparisons:selected.map(x=>x.observation_id===c.observation_id?{...x,...p}:x)});return <fieldset className="space-y-2 rounded border p-3" key={c.observation_id}><legend>Comparação {i+1}</legend><p>{proof?.statement??'Evidência retirada: volte a associar a observação antes de guardar.'}</p><p>{proof?.source} · {proof?.locator}</p><p>Ideia em preparação: {value.idea.solution||'Descreva a ideia no campo existente abaixo.'}</p><Field label={`Relação com a ideia ${i+1}`}><select className={control} value={c.relationship} onChange={e=>patch({relationship:e.target.value as typeof c.relationship})}><option value="uncertain">Relação por determinar</option><option value="competition">Possível concorrência</option><option value="complementarity">Possível complementaridade</option><option value="partner">Possível parceiro — sem compromisso</option></select></Field><Field label={`Fundamento da comparação ${i+1}`}><textarea className={control} value={c.rationale} onChange={e=>patch({rationale:e.target.value})}/></Field><Field label={`Incertezas da comparação ${i+1}`}><textarea className={control} value={c.limitations} onChange={e=>patch({limitations:e.target.value})}/></Field><button type="button" onClick={()=>onChange({...value,supply_comparisons:selected.filter(x=>x.observation_id!==c.observation_id)})}>Retirar comparação {i+1}</button></fieldset>;})}
|
||||
{!!selected.length&&<p>Use estas comparações para fundamentar a necessidade abaixo e seleccione as evidências correspondentes. A fonte administrativa não confirma procura nem insuficiência de oferta. Guarde no rascunho existente; a continuidade para o dossier conserva o percurso já entregue.</p>}
|
||||
</fieldset>;
|
||||
}
|
||||
@@ -485,7 +485,7 @@ export default function OpportunityDraft({
|
||||
<p>Seleccione apenas o nível territorial conhecido; não exige coordenadas, montante ou promotor.</p>
|
||||
{(['province_id','district_id','admin_post_id','locality_id']as const).map((key,index)=><TerritoryLevel key={`proactive-${key}-${path[index-1]?.id??''}`} index={index} parent={path[index-1]?.id??''} selected={path[index]} endpoint="/minhas-oportunidades/territory-options" fieldError={errors[key]} onChange={value=>{const next=[...path.slice(0,index),...(value?[value]:[])];setPath(next);setData(d=>({...d,province_id:next[0]?.id??'',district_id:next[1]?.id??'',admin_post_id:next[2]?.id??'',locality_id:next[3]?.id??''}));}}/>)}
|
||||
</fieldset>}
|
||||
<fieldset disabled={busy || readOnly || unavailable}><ProactiveInvestmentEditor value={data.proactive as ProactiveAnalysis|undefined} onChange={v=>setData(d=>({...d,proactive:v}))} options={proactiveOptions} history={proactiveHistory}/></fieldset>
|
||||
<fieldset disabled={busy || readOnly || unavailable}><ProactiveInvestmentEditor value={data.proactive as ProactiveAnalysis|undefined} onChange={v=>setData(d=>({...d,proactive:v}))} options={proactiveOptions} history={proactiveHistory} territoryId={path.at(-1)?.id} recordId={record?.id}/></fieldset>
|
||||
{!profileMode && (
|
||||
<fieldset
|
||||
disabled={busy || readOnly || unavailable}
|
||||
|
||||
@@ -166,6 +166,7 @@ Route::middleware(['auth', 'verified', EnsurePortalProfileAdministration::class]
|
||||
});
|
||||
|
||||
Route::middleware(['auth', 'verified', EnsurePortalProfiles::class])->group(function () {
|
||||
Route::get('/minhas-oportunidades/oferta-interna', [\App\Http\Controllers\ProactiveSupplyController::class,'index'])->middleware('throttle:60,1');
|
||||
$profiles = PortalProfileController::class;
|
||||
Route::get('/minhas-oportunidades', [$profiles, 'opportunities']);
|
||||
Route::get('/minhas-oportunidades/lotes/agronegocio', [\App\Http\Controllers\CorridorAgribusinessController::class, 'show']);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use App\Services\OpportunityManagement;
|
||||
use App\Services\ProactiveSupply;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Support\ProactiveInvestmentFixture as F;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ProactiveSupplyTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
private User $actor;
|
||||
private string $territory;
|
||||
private string $observation;
|
||||
protected function setUp():void
|
||||
{
|
||||
parent::setUp();F::guard();$this->withoutVite();
|
||||
config(['portal.profile_access_enabled'=>true,'portal.opportunity_specialized_drafts_enabled'=>true,'portal.managers'=>[],'investor-path.enabled'=>true,'suitecrm.enabled'=>false,'investmoz_ai.enabled'=>false]);
|
||||
$this->actor=F::operator();$this->login($this->actor);
|
||||
$input=F::input('Gurué');$this->territory=$input['district_id'];
|
||||
$hash=hash('sha256','SYNTHETIC-QA-SUPPLY-SOURCE');
|
||||
DB::table('proactive_supply_sources')->insert(['sha256'=>$hash,'name'=>'SYNTHETIC-QA-SUPPLY.xlsx','period'=>'2020','limitations'=>'Synthetic, not a real market source','receipt'=>'{}','imported_at'=>now()]);
|
||||
$this->observation=hash('sha256','SYNTHETIC-QA-OBSERVATION');
|
||||
$row=['id'=>$this->observation,'source_hash'=>$hash,'sheet'=>'QA','row_number'=>2,'entity_key'=>hash('sha256','QA-ENTITY'),'name'=>'PRIVATE-UNIQUE-QA-SUPPLIER','province'=>'Zambézia','district'=>'Gurué','cae'=>'33120','activity'=>'SYNTHETIC QA repair','sector'=>'SYNTHETIC QA manufacturing','source_date'=>'2020','fingerprint'=>hash('sha256','QA-FINGERPRINT'),'duplicate_of'=>null,'identity_conflict'=>false,'territory_id'=>$this->territory,'territory_resolution'=>'exact_normalized_province_district'];
|
||||
DB::table('proactive_supply_observations')->insert($row);
|
||||
DB::table('proactive_supply_observations')->insert([...$row,'id'=>hash('sha256','QA-REPEATED-OBSERVATION'),'row_number'=>3,'duplicate_of'=>$this->observation]);
|
||||
}
|
||||
private function login(User $u):void{$this->actingAs($u)->withSession(['portal.mfa_actor'=>$u->id,'portal.mfa_at'=>time(),'portal.access_revision'=>(int)$u->portal_access_revision]);}
|
||||
private function url(string $extra=''):string{return '/minhas-oportunidades/oferta-interna?territory_id='.$this->territory.$extra;}
|
||||
public function test_private_query_filters_supply_and_preserves_source_and_duplicate_count():void
|
||||
{
|
||||
$r=$this->getJson($this->url('&activity=SYNTHETIC%20QA%20repair'))->assertOk()->assertJsonPath('total',1)->assertJsonPath('duplicate_sightings',1)->assertJsonPath('rows.0.source_name','SYNTHETIC-QA-SUPPLY.xlsx')->assertJsonPath('rows.0.row_number',2)->assertJsonPath('rows.0.source_date','2020');
|
||||
$this->assertStringContainsString('no-store',$r->headers->get('Cache-Control'));
|
||||
$this->assertArrayNotHasKey('contact',$r->json('rows.0'));
|
||||
$this->assertArrayNotHasKey('nuit',$r->json('rows.0'));
|
||||
}
|
||||
public function test_no_match_means_insufficient_information_not_absent_market():void
|
||||
{
|
||||
$this->getJson($this->url('&activity=NO-SUCH-SYNTHETIC-ACTIVITY'))->assertOk()->assertJsonPath('total',0)->assertJsonPath('status','Informação insuficiente nesta base e nestes filtros.');
|
||||
}
|
||||
public function test_unassigned_user_cannot_read_internal_register():void
|
||||
{
|
||||
$this->login(User::factory()->create());$this->getJson($this->url())->assertForbidden();
|
||||
$this->getJson('/minhas-oportunidades/oferta-interna?territory_id=not-a-uuid')->assertForbidden();
|
||||
}
|
||||
public function test_comparison_persists_source_lineage_and_rejects_forgery_and_public_reclassification():void
|
||||
{
|
||||
$input=F::input('Gurué');$a=$input['proactive'];$id=(string)\Illuminate\Support\Str::uuid();
|
||||
$a['evidence'][]=['id'=>$id,'interpretation'=>'SYNTHETIC QA possible complement, not a contract','verified_on'=>'2026-10-28',...app(ProactiveSupply::class)->evidence($this->actor,$this->observation,null)];
|
||||
$a['supply_comparisons']=[['observation_id'=>$this->observation,'relationship'=>'complementarity','rationale'=>'SYNTHETIC QA compare servicing with tea-chain idea','limitations'=>'No current capacity or demand proved']];
|
||||
$input['proactive']=$a;
|
||||
$oid=$this->postJson('/minhas-oportunidades/drafts',$input)->assertCreated()->json('id');$o=Opportunity::findOrFail($oid);
|
||||
$saved=app(OpportunityManagement::class)->values($o)['proactive'];$this->assertSame($this->observation,$saved['supply_comparisons'][0]['observation_id']);
|
||||
$this->assertSame('internal',$saved['evidence'][2]['access']);$this->assertStringContainsString('SYNTHETIC-QA-SUPPLY.xlsx',$saved['evidence'][2]['source']);
|
||||
$a['evidence'][2]['source']='Forged source';$a['evidence'][2]['statement']='Forged supply fact';
|
||||
$this->putJson('/minhas-oportunidades/'.$oid.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA canonical source lineage is server-resolved'])->assertOk();
|
||||
$saved=app(OpportunityManagement::class)->values($o->fresh())['proactive'];$this->assertStringContainsString('PRIVATE-UNIQUE-QA-SUPPLIER',$saved['evidence'][2]['statement']);
|
||||
$a['evidence'][2]['access']='public';$this->putJson('/minhas-oportunidades/'.$oid.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA internal list cannot become public'])->assertUnprocessable();
|
||||
$a['evidence'][2]['access']='internal';$a['supply_comparisons'][0]['observation_id']=str_repeat('f',64);
|
||||
$this->putJson('/minhas-oportunidades/'.$oid.'/draft',['etag'=>app(OpportunityManagement::class)->etag($o),'proactive'=>$a,'reason'=>'SYNTHETIC QA unknown supply observation rejected'])->assertUnprocessable();
|
||||
}
|
||||
public function test_sector_filter_does_not_infer_absence_and_comparison_requires_its_evidence():void
|
||||
{
|
||||
$this->getJson($this->url('§or=NO-SUCH-SECTOR'))->assertOk()->assertJsonPath('total',0);
|
||||
$input=F::input('Gurué');$input['proactive']['supply_comparisons']=[['observation_id'=>$this->observation,'relationship'=>'partner','rationale'=>'SYNTHETIC QA prospective partner','limitations'=>'Not contacted, no agreement']];
|
||||
$this->postJson('/minhas-oportunidades/drafts',$input)->assertUnprocessable();
|
||||
}
|
||||
public function test_database_policy_hides_internal_rows_without_actor_context():void
|
||||
{
|
||||
$role='supply_qa_'.bin2hex(random_bytes(6));
|
||||
DB::statement('CREATE ROLE '.$role.' NOLOGIN');
|
||||
DB::statement('GRANT USAGE ON SCHEMA public TO '.$role);
|
||||
DB::statement('GRANT SELECT ON proactive_supply_sources,proactive_supply_observations TO '.$role);
|
||||
DB::select("select set_config('app.actor_user_id','',true),set_config('app.proactive_supply_read','',true)");
|
||||
try{
|
||||
DB::statement('SET LOCAL ROLE '.$role);
|
||||
$this->assertSame(0,DB::table('proactive_supply_observations')->count());
|
||||
$this->assertSame(0,DB::table('proactive_supply_sources')->count());
|
||||
DB::select("select set_config('app.actor_user_id',?,true),set_config('app.proactive_supply_read','internal',true)",[(string)$this->actor->id]);
|
||||
$this->assertNotNull(DB::table('proactive_supply_observations')->where('id',$this->observation)->first());
|
||||
}finally{DB::statement('RESET ROLE');}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Map only unique canonical names within the same province, then export privately."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import unicodedata
|
||||
|
||||
def norm(v):
|
||||
return ' '.join(''.join(c for c in unicodedata.normalize('NFKD',v).casefold() if not unicodedata.combining(c)).split())
|
||||
|
||||
p=argparse.ArgumentParser()
|
||||
p.add_argument('--preview',required=True)
|
||||
p.add_argument('--geography',required=True)
|
||||
a=p.parse_args()
|
||||
root=Path(a.preview)
|
||||
geos=json.loads(Path(a.geography).read_text(encoding='utf-8'))
|
||||
byid={r['id']:r for r in geos}
|
||||
territories={}
|
||||
provinces={}
|
||||
subterritories={}
|
||||
def province_name(v):
|
||||
n=norm(v)
|
||||
return 'maputo provincia' if n=='maputo' else n
|
||||
for g in geos:
|
||||
if g['location_type']=='province':provinces.setdefault(norm(g['name']),[]).append(g['id'])
|
||||
if g['location_type'] not in ('district','admin_post','locality'):continue
|
||||
parent=byid.get(g['parent_id']);visited=set()
|
||||
while parent and parent['location_type']!='province' and parent['id'] not in visited:
|
||||
visited.add(parent['id']);parent=byid.get(parent['parent_id'])
|
||||
if not parent:continue
|
||||
target=territories if g['location_type']=='district' else subterritories
|
||||
target.setdefault((norm(parent['name']),norm(g['name'])),[]).append(g['id'])
|
||||
db=sqlite3.connect(root/'supply-preview.sqlite')
|
||||
db.row_factory=sqlite3.Row
|
||||
resolution={}
|
||||
with (root/'observations.jsonl').open('w',encoding='utf-8',newline='\n') as f:
|
||||
for row in db.execute('select * from records order by source_hash,sheet,row_number'):
|
||||
r=dict(row)
|
||||
key=(province_name(r['province']),norm(r['district']))
|
||||
hits=territories.get(key,[])
|
||||
lower_level=False
|
||||
if not hits:
|
||||
hits=subterritories.get(key,[])
|
||||
lower_level=bool(hits)
|
||||
province_hits=provinces.get(province_name(r['province']),[])
|
||||
province_only=not r['district'] and len(province_hits)==1
|
||||
r['territory_id']=hits[0] if len(hits)==1 else (province_hits[0] if province_only else None)
|
||||
r['territory_resolution']=('exact_normalized_province_subterritory' if lower_level else 'exact_normalized_province_district') if len(hits)==1 else ('province_only_district_unknown' if province_only else ('ambiguous' if hits else 'unresolved'))
|
||||
r['identity_conflict']=bool(r['identity_conflict'])
|
||||
resolution[r['territory_resolution']]=resolution.get(r['territory_resolution'],0)+1
|
||||
f.write(json.dumps(r,ensure_ascii=False)+'\n')
|
||||
result={'rows':db.execute('select count(*) from records').fetchone()[0],'territory_resolution':resolution,'sha256':hashlib.file_digest((root/'observations.jsonl').open('rb'),'sha256').hexdigest(),'mapping':'case/accent/whitespace; exact district in province, otherwise unique named subordinate territory within that province; source label Maputo distinct from Maputo Cidade maps explicitly to Maputo Província; city names retain canonical administrative level, no fuzzy matching','publication':False}
|
||||
(root/'export.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
print(json.dumps(result,ensure_ascii=False))
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Read-only XLSX ingestion into a private, indexed analysis snapshot. No publication.
|
||||
|
||||
Every source row remains addressable. Matching identifiers suggest the same entity;
|
||||
they never imply a current trading status, contract, or an exhaustive census.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import unicodedata
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
|
||||
NS = '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}'
|
||||
SOURCES = {
|
||||
'companies.xlsx': ('1ba4c4114b16b407e7746712d00d4f681246bbbc1b42351e96a31f7129acad30', 'EMPRESAS_LIST 2014-2022.xlsx', '2014–2022; data por registo quando disponível'),
|
||||
'licensed.xlsx': ('1b5087b464e39e88c42def39655c2391aaaf684b4c832d6a77ff274b98889f00', 'Empresas Licenciadas.xlsx', 'Data por registo; cobertura temporal global não determinada'),
|
||||
'tourism.xlsx': ('f02e086ed4973ef17234aa6cc54b98fbb7a6952ffb16ec4dc4f7d0c72af6400c', 'CADASTRO DE EMPREENDIMENTOS TURISTICOS 3, 4 E 5 ESTRELAS 30.10.24 18,00h.xlsx', '30/10/2024 (nome do exemplar); situação actual não confirmada'),
|
||||
'cae.xlsx': ('83b4f1200988e99834c2ae3915346fbf237d128308554b67b5a080d4956bf506', 'CAE eBAU.xlsx', 'Versão não indicada'),
|
||||
}
|
||||
|
||||
def norm(v):
|
||||
return ''.join(c for c in unicodedata.normalize('NFKD', str(v)).casefold() if not unicodedata.combining(c)).strip()
|
||||
|
||||
def digest(v):
|
||||
return hashlib.sha256(v.encode('utf-8')).hexdigest()
|
||||
|
||||
def rows(path):
|
||||
# Shared strings are spooled to a temporary on-disk SQLite lookup, not a huge XML tree.
|
||||
with zipfile.ZipFile(path) as z, sqlite3.connect('') as strings:
|
||||
strings.execute('create table strings(id integer primary key,value text)')
|
||||
if 'xl/sharedStrings.xml' in z.namelist():
|
||||
with z.open('xl/sharedStrings.xml') as f:
|
||||
i = 0
|
||||
for _, e in ET.iterparse(f, events=('end',)):
|
||||
if e.tag == NS+'si':
|
||||
strings.execute('insert into strings values(?,?)', (i, ''.join(t.text or '' for t in e.iter(NS+'t'))))
|
||||
i += 1
|
||||
e.clear()
|
||||
book = ET.fromstring(z.read('xl/workbook.xml'))
|
||||
rels = {r.attrib['Id']: r.attrib['Target'] for r in ET.fromstring(z.read('xl/_rels/workbook.xml.rels'))}
|
||||
for sheet in book.find(NS+'sheets'):
|
||||
target = rels[sheet.attrib['{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id']]
|
||||
target = target.lstrip('/') if target.startswith('/') else 'xl/'+target
|
||||
with z.open(target) as f:
|
||||
for _, e in ET.iterparse(f, events=('end',)):
|
||||
if e.tag != NS+'row':
|
||||
continue
|
||||
result = {}
|
||||
for c in e:
|
||||
col = ''.join(ch for ch in c.attrib.get('r','') if ch.isalpha())
|
||||
v = c.findtext(NS+'v', '')
|
||||
if c.attrib.get('t') == 's':
|
||||
hit = strings.execute('select value from strings where id=?', (int(v),)).fetchone()
|
||||
v = hit[0] if hit else ''
|
||||
elif c.attrib.get('t') == 'inlineStr':
|
||||
v = ''.join(t.text or '' for t in c.iter(NS+'t'))
|
||||
result[col] = ' '.join(v.split())
|
||||
yield sheet.attrib['name'], int(e.attrib['r']), result
|
||||
e.clear()
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('--sources', required=True)
|
||||
p.add_argument('--output', required=True)
|
||||
p.add_argument('--only-tourism', action='store_true', help='Rebuild only tourism in an existing private preview')
|
||||
a = p.parse_args()
|
||||
output = Path(a.output)
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
manifest = {'version':1, 'access':'internal', 'sources':[], 'publication':False,
|
||||
'limitations':'Registos administrativos históricos; cobertura incompleta, actividade actual, capacidade, procura e compromissos não confirmados. Ausência de registo significa informação insuficiente.'}
|
||||
for alias, (expected, name, period) in SOURCES.items():
|
||||
path = Path(a.sources)/alias
|
||||
actual = hashlib.file_digest(path.open('rb'), 'sha256').hexdigest()
|
||||
if actual != expected:
|
||||
raise ValueError('Original changed: '+alias)
|
||||
manifest['sources'].append({'alias':alias,'name':name,'sha256':actual,'bytes':path.stat().st_size,'period':period})
|
||||
db = sqlite3.connect(output/'supply-preview.sqlite')
|
||||
db.executescript('''create table if not exists records(id text primary key, source_hash text, sheet text, row_number integer, entity_key text, name text, province text, district text, cae text, activity text, sector text, source_date text, fingerprint text, duplicate_of text, identity_conflict integer default 0);
|
||||
create index if not exists supply_place on records(province,district);
|
||||
create index if not exists supply_entity on records(entity_key);
|
||||
create index if not exists supply_fingerprint on records(fingerprint);''')
|
||||
labels = {}
|
||||
hierarchy = {}
|
||||
for _, row, c in rows(Path(a.sources)/'cae.xlsx'):
|
||||
if row > 1 and c.get('B') and c.get('F'):
|
||||
labels[c['B']] = c['F']
|
||||
hierarchy[c['A']] = c
|
||||
# Supplied CAE hierarchy labels retained. Sector is the top-level CAE section,
|
||||
# not an invented correspondence to the portal's twelve navigation sectors.
|
||||
sector_by_code = {}
|
||||
for node in hierarchy.values():
|
||||
ancestor=node
|
||||
visited=set()
|
||||
while ancestor.get('D')!='1' and ancestor.get('C') in hierarchy and ancestor['A'] not in visited:
|
||||
visited.add(ancestor['A'])
|
||||
ancestor=hierarchy[ancestor['C']]
|
||||
if ancestor.get('D')=='1':
|
||||
sector_by_code[node['B']]=ancestor['F']
|
||||
counts = {}
|
||||
if a.only_tourism:
|
||||
db.execute('delete from records where source_hash=?',(SOURCES['tourism.xlsx'][0],))
|
||||
for alias in (('tourism.xlsx',) if a.only_tourism else ('companies.xlsx','licensed.xlsx','tourism.xlsx')):
|
||||
source_hash, _, period = SOURCES[alias]
|
||||
count = 0
|
||||
province = ''
|
||||
previous_sheet=''
|
||||
for sheet, row, c in rows(Path(a.sources)/alias):
|
||||
if alias == 'tourism.xlsx':
|
||||
if sheet!=previous_sheet:
|
||||
province=''
|
||||
previous_sheet=sheet
|
||||
heading=norm(c.get('C','')).replace(' ','')
|
||||
provinces={'cidadedemaputo':'MAPUTO CIDADE','maputocidade':'MAPUTO CIDADE','provinciademaputo':'MAPUTO PROVINCIA','maputoprovincia':'MAPUTO PROVINCIA'}
|
||||
provinces.update({'provinciade'+norm(n).replace(' ',''):n for n in ['Gaza','Inhambane','Sofala','Manica','Tete','Zambézia','Nampula','Cabo Delgado','Niassa']})
|
||||
if heading in provinces and not c.get('D'):
|
||||
province=provinces[heading]
|
||||
continue
|
||||
# Explicit province section and ordinal identify an establishment.
|
||||
# Missing licence/room/contact never excludes an authorised record.
|
||||
name = c.get('C','')
|
||||
if not name or not c.get('B','').isdigit():
|
||||
continue
|
||||
district = c.get('E','') if sheet=='Folha1' else ''
|
||||
cae, activity, date, identifier = '', 'Alojamento (âmbito do exemplar; categoria por conferir)', period, ''
|
||||
sector = labels.get('I','Alojamento e restauração — exemplar de alojamento')
|
||||
else:
|
||||
if row == 1:
|
||||
continue
|
||||
old = alias == 'companies.xlsx'
|
||||
name = c.get('L','')
|
||||
if not name:
|
||||
continue
|
||||
province, district = c.get('Y' if old else 'V',''), c.get('X' if old else 'U','')
|
||||
cae = c.get('S' if old else 'O','')
|
||||
activity = labels.get(cae, '')
|
||||
sector = sector_by_code.get(cae,'')
|
||||
date = c.get('D','')
|
||||
# Keep source serial verbatim; Excel origin must be demonstrated before conversion.
|
||||
date = ('Serial Excel original: '+date) if date.replace('.','',1).isdigit() else (date or 'Não indicada')
|
||||
nuit = c.get('K','')
|
||||
identifier = ('nuit:'+nuit) if len(nuit)==9 and nuit.isdigit() and len(set(nuit))>1 else ''
|
||||
rid = digest(source_hash+'|'+sheet+'|'+str(row))
|
||||
entity = digest(identifier) if identifier else digest('unresolved:'+source_hash+'|'+sheet+'|'+str(row))
|
||||
# Same projected administrative observation, not byte-identical rows:
|
||||
# include licence and process identifiers; do not collapse distinct licences.
|
||||
licence = (c.get('G','') if sheet!='Folha1' else '') if alias=='tourism.xlsx' else c.get('A','')
|
||||
process = '' if alias=='tourism.xlsx' else c.get('B','')
|
||||
fp = digest('|'.join(map(norm,[identifier,name,province,district,cae,date,licence,process])))
|
||||
duplicate = db.execute('select id from records where fingerprint=? and id<>? limit 1',(fp,rid)).fetchone()
|
||||
db.execute('insert or ignore into records values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', (rid,source_hash,sheet,row,entity,name,province,district,cae,activity,sector,date,fp,duplicate[0] if duplicate else None,0))
|
||||
count += 1
|
||||
if count % 10000 == 0:
|
||||
db.commit()
|
||||
db.commit()
|
||||
counts[alias] = count
|
||||
print(json.dumps({'source':alias,'rows_processed':count}), flush=True)
|
||||
db.execute('update records set identity_conflict=1 where entity_key in (select entity_key from records group by entity_key having count(distinct lower(name))>1)')
|
||||
db.commit()
|
||||
counts={alias:db.execute('select count(*) from records where source_hash=?',(SOURCES[alias][0],)).fetchone()[0] for alias in ('companies.xlsx','licensed.xlsx','tourism.xlsx')}
|
||||
manifest.update(rows_processed=counts, records=db.execute('select count(*) from records').fetchone()[0],
|
||||
duplicate_sightings=db.execute('select count(*) from records where duplicate_of is not null').fetchone()[0],
|
||||
conflicting_identity_sightings=db.execute('select count(*) from records where identity_conflict=1').fetchone()[0],
|
||||
unclassified_activity=db.execute("select count(*) from records where activity=''").fetchone()[0],
|
||||
unresolved_territory=db.execute("select count(*) from records where district='' or province=''").fetchone()[0])
|
||||
db.close()
|
||||
(output/'preview.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
print(json.dumps({k:v for k,v in manifest.items() if k not in ('sources',)},ensure_ascii=False))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
$root=dirname(__DIR__);
|
||||
$operation='test';
|
||||
if(!in_array($operation,['routes','test'],true)||basename($root)!=='portal-investidor-supply-qa-20261028')throw new RuntimeException('Explicit isolated QA workspace and routes/test operation required.');
|
||||
foreach(['storage/framework/views','storage/framework/cache','storage/framework/sessions','bootstrap/cache','storage/logs','outputs/proactive-supply-20261028','outputs/proactive-investment-20261027']as$dir)if(!is_dir($root.'/'.$dir))mkdir($root.'/'.$dir,0700,true);
|
||||
foreach(['APP_BASE_PATH'=>$root,'APP_ENV'=>'testing','APP_URL'=>'http://127.0.0.1:18768','APP_KEY'=>'base64:'.base64_encode(str_repeat('Q',32)),'DB_CONNECTION'=>'pgsql','DB_HOST'=>'127.0.0.1','DB_PORT'=>'55441','DB_DATABASE'=>'proactive_investment_20261027_test','DB_USERNAME'=>'postgres','DB_PASSWORD'=>'','DB_URL'=>'','CACHE_STORE'=>'array','SESSION_DRIVER'=>'array','QUEUE_CONNECTION'=>'sync','MAIL_MAILER'=>'array'] as $k=>$v){putenv($k.'='.$v);$_ENV[$k]=$_SERVER[$k]=$v;}
|
||||
$loader=require $root.'/vendor/autoload.php';
|
||||
spl_autoload_register(function($class)use($root){foreach(['App\\'=>'app/','Tests\\'=>'tests/','Database\\Factories\\'=>'database/factories/']as$prefix=>$dir){if(str_starts_with($class,$prefix)){$file=$root.'/'.$dir.str_replace('\\','/',substr($class,strlen($prefix))).'.php';if(is_file($file)){require $file;return;}}}},true,true);
|
||||
$app=require $root.'/bootstrap/app.php';
|
||||
(new Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables)->bootstrap($app);
|
||||
(new Illuminate\Foundation\Bootstrap\LoadConfiguration)->bootstrap($app);
|
||||
$c=config('database.connections.pgsql');
|
||||
if($app->configurationIsCached()||$c['host']!=='127.0.0.1'||(int)$c['port']!==55441||$c['database']!=='proactive_investment_20261027_test'||!empty($c['url']))throw new RuntimeException('QA guard');
|
||||
$pdo=new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$c['database'],$c['username'],$c['password']);
|
||||
if($pdo->query('select current_database()')->fetchColumn()!==$c['database'])throw new RuntimeException('QA identity');
|
||||
|
||||
if(($argv[1]??'')==='test')return;
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
config(['portal.profile_access_enabled'=>true,'portal.opportunity_specialized_drafts_enabled'=>true,'investor-path.enabled'=>true,'investor-path.private_preparation'=>true,'suitecrm.enabled'=>false,'investmoz_ai.enabled'=>false,'session.driver'=>'file','session.secure'=>false]);
|
||||
|
||||
Illuminate\Support\Facades\Vite::useBuildDirectory('build-qa-opportunities');
|
||||
config(['session.cookie'=>'proactive_qa_session','session.http_only'=>true]);
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/** Explicit QA-only tooling: no active environment or .env fallback. */
|
||||
require __DIR__.'/proactive-supply-qa-bootstrap.php';
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
$operation=$argv[1]??'';
|
||||
if($operation==='test'){
|
||||
$argv=['phpunit','-c','phpunit.investmoz.xml','tests/Feature/ProactiveSupplyTest.php','tests/Feature/ProactiveInvestmentTest.php'];
|
||||
if(getenv('PROACTIVE_SUPPLY_FILTER')){$argv[]='--filter';$argv[]=getenv('PROACTIVE_SUPPLY_FILTER');}
|
||||
$argc=count($argv);$_SERVER['argv']=$argv;$_SERVER['argc']=$argc;
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';exit;
|
||||
}
|
||||
Tests\Support\ProactiveInvestmentFixture::guard();
|
||||
$out=dirname(__DIR__).'/outputs/proactive-supply-20261028';
|
||||
if($operation==='geography'){
|
||||
file_put_contents($out.'/private/geography.json',json_encode(DB::table('geo_locations')->whereNull('deleted_at')->get(['id','name','parent_id','location_type']),JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR));
|
||||
echo "QA geography exported privately.\n";exit;
|
||||
}
|
||||
if($operation==='schema'){
|
||||
if(!Schema::hasTable('proactive_supply_sources')){
|
||||
(require dirname(__DIR__).'/database/migrations/2026_10_28_180000_create_proactive_supply_observations.php')->up();
|
||||
}
|
||||
echo "QA schema available; no existing table altered.\n";exit;
|
||||
}
|
||||
if($operation==='import'){
|
||||
$dir=$out.'/private-v2';
|
||||
$manifest=json_decode(file_get_contents($dir.'/preview.json'),true,512,JSON_THROW_ON_ERROR);
|
||||
$scan=json_decode(file_get_contents($out.'/private/scan.json'),true,512,JSON_THROW_ON_ERROR);
|
||||
if(($scan['exit_code']??1)!==0||array_diff(array_column($manifest['sources'],'sha256'),$scan['source_hashes']??[]))throw new RuntimeException('Hash-specific malware scan required');
|
||||
$file=$dir.'/observations.jsonl';$hash=hash_file('sha256',$file);
|
||||
if(($argv[2]??'')!==$hash)throw new RuntimeException('Review preview and supply exact JSONL hash');
|
||||
$before=DB::table('proactive_supply_observations')->count();
|
||||
DB::transaction(function()use($manifest,$file,$hash){
|
||||
DB::select("select pg_advisory_xact_lock(hashtext('proactive-supply-import-v1'))");
|
||||
foreach($manifest['sources']as$s){
|
||||
$existing=DB::table('proactive_supply_sources')->where('sha256',$s['sha256'])->first();
|
||||
if($existing && data_get(json_decode($existing->receipt,true),'transport_sha256')!==$hash)throw new RuntimeException('Existing source has a different prepared classification snapshot; preserve it and prepare an explicit mapping delta instead of silently replacing observations.');
|
||||
DB::table('proactive_supply_sources')->insertOrIgnore(['sha256'=>$s['sha256'],'name'=>$s['name'],'period'=>$s['period'],'limitations'=>$manifest['limitations'],'receipt'=>json_encode(['source'=>$s,'preview_rows'=>$manifest['records'],'transport_sha256'=>$hash,'scan'=>'Defender custom scan, no threats, hash-specific receipt','scope'=>'isolated QA, internal analysis only']),'imported_at'=>now()]);
|
||||
}
|
||||
$f=fopen($file,'rb');$batch=[];$n=0;
|
||||
while(($line=fgets($f))!==false){
|
||||
$r=json_decode($line,true,512,JSON_THROW_ON_ERROR);
|
||||
if(!in_array($r['source_hash'],array_column($manifest['sources'],'sha256'),true))throw new RuntimeException('Unknown source');
|
||||
$batch[]=$r;$n++;
|
||||
if(count($batch)===500){DB::table('proactive_supply_observations')->insertOrIgnore($batch);$batch=[];}
|
||||
}
|
||||
fclose($f);if($batch)DB::table('proactive_supply_observations')->insertOrIgnore($batch);
|
||||
if($n!==$manifest['records'])throw new RuntimeException('Incomplete candidate');
|
||||
});
|
||||
$receipt=['before'=>$before,'after'=>DB::table('proactive_supply_observations')->count(),'sha256'=>$hash,'publication'=>false,'database'=>'proactive_investment_20261027_test','port'=>55441];
|
||||
file_put_contents($out.'/import-'.($before?'replay':'first').'.json',json_encode($receipt,JSON_PRETTY_PRINT));echo json_encode($receipt)."\n";exit;
|
||||
}
|
||||
throw new RuntimeException('Use geography, schema, import <reviewed hash>, or test; exclusively isolated QA.');
|
||||
Reference in New Issue
Block a user