Compare commits
345
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c0cb25cea | ||
|
|
3dda7130ee | ||
|
|
6af7092e83 | ||
|
|
49b448eb47 | ||
|
|
fa32a2b9e1 | ||
|
|
0f4df8a4d1 | ||
|
|
b189d0fa43 | ||
|
|
dbeaf886ee | ||
|
|
7ad187f9f0 | ||
|
|
4708662532 | ||
|
|
46944402da | ||
|
|
de36f02880 | ||
|
|
9e2d90c880 | ||
|
|
b7a745e64f | ||
|
|
3aa851a467 | ||
|
|
109cb0fe9e | ||
|
|
f7b778cdce | ||
|
|
508aeb5205 | ||
|
|
acbd1f3315 | ||
|
|
7a380b7690 | ||
|
|
dde20c3348 | ||
|
|
2c2a2f2010 | ||
|
|
855ab066c4 | ||
|
|
819ae791e9 | ||
|
|
fc7e590d4e | ||
|
|
56c948ec45 | ||
|
|
940d69b82a | ||
|
|
7febb6f7be | ||
|
|
ada9629bb6 | ||
|
|
6e1dc4104c | ||
|
|
4f28fac5d0 | ||
|
|
9019f766f2 | ||
|
|
e38aa74920 | ||
|
|
fab72d2e0c | ||
|
|
7b6a95a718 | ||
|
|
9536eb710b | ||
|
|
8b3ec2b04d | ||
|
|
a665ee9c97 | ||
|
|
2b130a66ec | ||
|
|
4e550c3892 | ||
|
|
34bbcfa7e4 | ||
|
|
0807201517 | ||
|
|
f059622562 | ||
|
|
a48e974979 | ||
|
|
1a5f72209e | ||
|
|
c309215d0f | ||
|
|
7986d02405 | ||
|
|
91c4063338 | ||
|
|
df43aeb1e5 | ||
|
|
de4b25bbd7 | ||
|
|
5f088e3411 | ||
|
|
e4c490b323 | ||
|
|
86970cd507 | ||
|
|
2de67bebf9 | ||
|
|
e8b5ae43ad | ||
|
|
0052cd42d2 | ||
|
|
7e6e462e12 | ||
|
|
7a3ac43224 | ||
|
|
92eaa93570 | ||
|
|
d84883b801 | ||
|
|
5999e35fcb | ||
|
|
4cacef23dd | ||
|
|
ce9d649fbf | ||
|
|
a5b1ae6657 | ||
|
|
f640e19370 | ||
|
|
4ef7be3d6f | ||
|
|
019bcbad6c | ||
|
|
0ebb20c86a | ||
|
|
b0818c0163 | ||
|
|
ec786f4ae2 | ||
|
|
6c12aabb3a | ||
|
|
cd259623ae | ||
|
|
9b95e5e7a8 | ||
|
|
1b67fa85ff | ||
|
|
63cc336787 | ||
|
|
af7d250320 | ||
|
|
2b592ded66 | ||
|
|
5837f34a2c | ||
|
|
2fdca86367 | ||
|
|
833b4e5cef | ||
|
|
67a96085c8 | ||
|
|
b0e01ef737 | ||
|
|
2a7254406d | ||
|
|
4601bf64a0 | ||
|
|
6cffa1188f | ||
|
|
4853bc7284 | ||
|
|
03c26d6613 | ||
|
|
3f4e71c3f3 | ||
|
|
ec33fb5be2 | ||
|
|
aac3f46306 | ||
|
|
8f14f5dde2 | ||
|
|
fb7248661d | ||
|
|
0682ef74e2 | ||
|
|
21ce03f65c | ||
|
|
6f1cf7b7a0 | ||
|
|
a4329bb4bf | ||
|
|
90b31c677f | ||
|
|
33cc971a12 | ||
|
|
cd530b248b | ||
|
|
2087134143 | ||
|
|
67b8b58149 | ||
|
|
adb849d4fc | ||
|
|
5d5f3b4148 | ||
|
|
9c2b2c6560 | ||
|
|
acc633bf08 | ||
|
|
a89f9eca8c | ||
|
|
7e5710ebbd | ||
|
|
6f90fcb9d4 | ||
|
|
e3caf55361 | ||
|
|
0bef805ede | ||
|
|
70b4a640ee | ||
|
|
0460c85330 | ||
|
|
9f053ac9d3 | ||
|
|
6234d34392 | ||
|
|
92a7418cb6 | ||
|
|
76be72c38c | ||
|
|
3bcd98e78e | ||
|
|
9600880169 | ||
|
|
df7cb7fa47 | ||
|
|
287f2f8321 | ||
|
|
4672789f2b | ||
|
|
15894f4b04 | ||
|
|
e62b7dfdc2 | ||
|
|
9bd2e062f4 | ||
|
|
948621a65c | ||
|
|
50463ca716 | ||
|
|
4193056bdd | ||
|
|
20bfeb4f71 | ||
|
|
1dda6ea258 | ||
|
|
0a80f1c457 | ||
|
|
eb9294ef42 | ||
|
|
2359a88dba | ||
|
|
69b6c49a60 | ||
|
|
163e090ea5 | ||
|
|
9b26217dec | ||
|
|
ebf5e53c3c | ||
|
|
9783c54b2c | ||
|
|
0f9f7e3409 | ||
|
|
78c9f4a356 | ||
|
|
7996c35933 | ||
|
|
29e708ba2d | ||
|
|
f9fb04f947 | ||
|
|
77d1290258 | ||
|
|
946fc08906 | ||
|
|
7ecf9ec79a | ||
|
|
536559d651 | ||
|
|
c1314cdf90 | ||
|
|
ff8848d32d | ||
|
|
1565aa4ffb | ||
|
|
4e4bf6b859 | ||
|
|
5ec2f198a3 | ||
|
|
e32690799f | ||
|
|
2d889a211b | ||
|
|
50fdd97916 | ||
|
|
9842dcbb0e | ||
|
|
e384418ecc | ||
|
|
0eb37ef59b | ||
|
|
2ed62165b7 | ||
|
|
d46b794ab2 | ||
|
|
d65d3f5c4e | ||
|
|
8e5565d5e0 | ||
|
|
72e56f7ae5 | ||
|
|
4cebd61765 | ||
|
|
1efb32c244 | ||
|
|
f75f36f0e3 | ||
|
|
a23f1dcb6e | ||
|
|
e3601c124d | ||
|
|
615f78e79f | ||
|
|
965da26426 | ||
|
|
e99ea1d013 | ||
|
|
6f056359df | ||
|
|
0cd7e9fb62 | ||
|
|
689a4c93c3 | ||
|
|
d3e9f3732c | ||
|
|
69f2e365bf | ||
|
|
4e97cd05ea | ||
|
|
1f7fc13872 | ||
|
|
0eb2c485cc | ||
|
|
2e21ca4500 | ||
|
|
fae8162c58 | ||
|
|
d03101bb69 | ||
|
|
38faeac685 | ||
|
|
3be371c823 | ||
|
|
686328a085 | ||
|
|
40a4ddcbde | ||
|
|
e50733ba1d | ||
|
|
8df721ac51 | ||
|
|
25cae1d98b | ||
|
|
679203d4b1 | ||
|
|
2fccbc8ee8 | ||
|
|
19b31d51c5 | ||
|
|
b1c8e0e343 | ||
|
|
117f2a70a8 | ||
|
|
a153d0782f | ||
|
|
0b0e002d15 | ||
|
|
b641887274 | ||
|
|
f946364baa | ||
|
|
3846be8fc6 | ||
|
|
b898955603 | ||
|
|
7b54efb0fc | ||
|
|
7a58fa2435 | ||
|
|
0f0d2e566f | ||
|
|
82e010c44f | ||
|
|
6eefe4b332 | ||
|
|
0dc2d64b20 | ||
|
|
bda6d1e3a0 | ||
|
|
ac948bd8b7 | ||
|
|
908c8afc54 | ||
|
|
643ba6ed72 | ||
|
|
ccdecbd8d7 | ||
|
|
da798dd1d8 | ||
|
|
0304612093 | ||
|
|
d76078a81f | ||
|
|
c4cc7d83e7 | ||
|
|
744682835a | ||
|
|
1fe742561f | ||
|
|
8882dc54d0 | ||
|
|
eb9e0dbc60 | ||
|
|
e45a1394fa | ||
|
|
72f4187d3b | ||
|
|
a5ac8ca1c9 | ||
|
|
fe69a16eea | ||
|
|
7696af6814 | ||
|
|
d4eae917ee | ||
|
|
b297058d11 | ||
|
|
48146089cd | ||
|
|
2440cb448a | ||
|
|
9dbfdd67ad | ||
|
|
b1928b61e5 | ||
|
|
541ea70484 | ||
|
|
9a56d98242 | ||
|
|
38b49bd5fa | ||
|
|
9b0f437073 | ||
|
|
8171c7992e | ||
|
|
30760cab9b | ||
|
|
33d18b9efc | ||
|
|
0aac0d1555 | ||
|
|
3b788ec009 | ||
|
|
db4ff64e87 | ||
|
|
c28e290b42 | ||
|
|
8d127799cd | ||
|
|
d32ad1e0a6 | ||
|
|
122f4f04b9 | ||
|
|
dacda11c07 | ||
|
|
490527b92c | ||
|
|
4a35d63cd3 | ||
|
|
0e644b5524 | ||
|
|
675cf38f67 | ||
|
|
89ab288334 | ||
|
|
5030116ee5 | ||
|
|
8719e916ea | ||
|
|
689e7cbe8b | ||
|
|
d89912e579 | ||
|
|
544068e8cc | ||
|
|
e8c39a1b54 | ||
|
|
1655f57e69 | ||
|
|
d18ff0f049 | ||
|
|
355656b5c8 | ||
|
|
cd10cccbf4 | ||
|
|
d044ef1efc | ||
|
|
65b87819a0 | ||
|
|
be62783b98 | ||
|
|
98fc16a3fb | ||
|
|
276d833a4b | ||
|
|
5d45541a11 | ||
|
|
066707ece6 | ||
|
|
c1f4a71963 | ||
|
|
cf2ddf21a1 | ||
|
|
3fd5128c62 | ||
|
|
cdf8fcfba4 | ||
|
|
8036b9b071 | ||
|
|
6985f82e9c | ||
|
|
2856240cf4 | ||
|
|
32144aa2f3 | ||
|
|
1afdfee8c0 | ||
|
|
bc84fbdb3e | ||
|
|
b6a626c92d | ||
|
|
55136b446d | ||
|
|
4a01e9e2b7 | ||
|
|
ad4160f040 | ||
|
|
af9261809c | ||
|
|
d8224ba84b | ||
|
|
088f2c2cf4 | ||
|
|
603a88e1d3 | ||
|
|
43cd0be7cb | ||
|
|
89a9c3d994 | ||
|
|
4098192a05 | ||
|
|
d242482042 | ||
|
|
6b544548d3 | ||
|
|
c3f7c3ad10 | ||
|
|
445aa7e69d | ||
|
|
7c1f532134 | ||
|
|
115c773162 | ||
|
|
ec4b2f2af2 | ||
|
|
3d877ec745 | ||
|
|
6948ed62fc | ||
|
|
7d798579a6 | ||
|
|
5bda74aedc | ||
|
|
f2150b1912 | ||
|
|
7ca94967f3 | ||
|
|
e1f010f941 | ||
|
|
0025309b98 | ||
|
|
a0e80db4ec | ||
|
|
7226287526 | ||
|
|
f1ac82ae4d | ||
|
|
98474777f6 | ||
|
|
3848f29bea | ||
|
|
203492997e | ||
|
|
165c2f6171 | ||
|
|
4ad1bf402b | ||
|
|
e12ef6ad42 | ||
|
|
c641b267ac | ||
|
|
e2b0fb4b2d | ||
|
|
7d08c2c664 | ||
|
|
761035aefb | ||
|
|
20a4099d10 | ||
|
|
e03de682ca | ||
|
|
37a8eb015a | ||
|
|
65eddf6bc7 | ||
|
|
fb4e7d8970 | ||
|
|
bc93ccdb9e | ||
|
|
7ccf62adec | ||
|
|
f4d0970917 | ||
|
|
e46f973159 | ||
|
|
1bcf34c0c0 | ||
|
|
ef0c2acd36 | ||
|
|
30aed7cd12 | ||
|
|
95d4cf44d7 | ||
|
|
73c046e104 | ||
|
|
15d82611b6 | ||
|
|
8286d7504d | ||
|
|
aa2b3d05b0 | ||
|
|
97d51db60a | ||
|
|
a1b553372e | ||
|
|
f80d30f478 | ||
|
|
916819eb35 | ||
|
|
1d921c9d03 | ||
|
|
7b1c35793c | ||
|
|
1ec318561c | ||
|
|
9ac623a433 | ||
|
|
5f4eb5cfaa | ||
|
|
042ffbde44 | ||
|
|
064296e1fb | ||
|
|
a1fbda06ad | ||
|
|
52fbb1468a |
@@ -0,0 +1,16 @@
|
||||
.git
|
||||
.idea
|
||||
.agents
|
||||
.codex
|
||||
node_modules
|
||||
vendor
|
||||
public/build
|
||||
storage/logs
|
||||
storage/framework/cache
|
||||
storage/framework/sessions
|
||||
storage/framework/views
|
||||
tests
|
||||
Doc
|
||||
docs
|
||||
*.zip
|
||||
.phpunit.result.cache
|
||||
@@ -1,5 +1,13 @@
|
||||
# Instruções permanentes do projeto InvesteMoz
|
||||
|
||||
## Percurso adaptativo — decisão expressa da Tempo, 23/10/2026
|
||||
|
||||
Carteira/projectos âncora e percurso adaptativo têm diagnósticos distintos, mas fazem parte da mesma experiência e responsabilidade de correcção. Separar o diagnóstico não autoriza adiamento, desligamento do percurso ou perda de contexto entre corredor/localização, oportunidades, associações documentadas a âncoras/clusters, dossier e simulador. A reposição prioritária só se considera demonstrada com causa delimitada e disponibilidade recuperada, reutilizando o inventário e os recibos existentes; melhorias noutro ponto não a comprovam. Consultar os registos de reposição antes de voltar a declarar causa desconhecida ou registos desaparecidos.
|
||||
|
||||
Apresentar apenas o necessário ao investimento em preparação, com entrada por ideia, oportunidade, sector ou localização, reutilizando informação válida e autorizada da oportunidade, perfil, organização, dossier e fontes anteriores. As etapas são adaptáveis, não seis formulários obrigatórios. Não sobrescrever edições deliberadas; explicar divergências e impactos antes da confirmação. Conservar escolhas ao avançar, regressar, actualizar e autenticar-se. Seleccionar opções únicas e pedir apenas dados em falta ou desactualizados.
|
||||
|
||||
Ocultar campos e acções sem aplicação, conservando explicação breve e histórico; desconhecido não significa não aplicável. Na selecção múltipla, partilhar dados comuns sem perder os requisitos específicos de cada oportunidade. Consulta pública livre; autenticação no momento de manifestar interesse ou iniciar operação privada. Relatórios e exportações devem reflectir o contexto e as etapas efectivamente aplicáveis. O utilizador deve perceber onde está, o que falta e a próxima acção. Aperfeiçoar os componentes existentes por incrementos e registar cobertura na matriz existente; esta instrução não declara concluída a sua implementação transversal.
|
||||
|
||||
## Regras permanentes do InvesteMoz — decisão da Tempo, 12/10/2026
|
||||
|
||||
1. O portal InvesteMoz é propriedade da Tempo, Lda.
|
||||
@@ -39,3 +47,20 @@ Referenciar estas orientações nos requisitos correspondentes da matriz existen
|
||||
- Preservar o ficheiro original, a proveniência, o identificador exacto, a edição/data, o hash e o histórico. Não marcar autenticidade, vigência, direitos ou scan como confirmados sem evidência.
|
||||
- Para fontes fiscais, inventariar também as fontes não analisadas e os diplomas/artigos sem correspondência documental. Parâmetros ausentes ou ambiguidades permanecem indeterminados; não usar zero nem a taxa mais recente como substituto.
|
||||
- Antes de aplicar mudanças em dados activos, confirmar backup/restauro e a acção autenticada que continua necessária. Testes e migrações devem usar exclusivamente uma base QA isolada com guarda contra a base activa.
|
||||
|
||||
|
||||
## Execução das fontes e decisões — orientação permanente da Tempo, 22/10/2026
|
||||
|
||||
A legislação, os formulários e os procedimentos oficiais disponibilizados são fontes válidas para implementar os requisitos. As decisões e procedimentos da Tempo orientam a sua utilização no portal. Traduzir essas fontes em campos, regras, documentos, cálculos e percursos executáveis com rastreabilidade, sem exigir nova validação jurídica genérica ou criar aprovadores, restrições ou condições por interpretação do Codex.
|
||||
|
||||
Uma divergência concreta deve identificar os trechos, períodos e comportamento afectados; prosseguir com os restantes requisitos. Uma decisão de implementação em falta deve ser apresentada especificamente à Tempo, sem a substituir por regra inventada. Rever e remover bloqueios anteriores sem fundamento numa fonte aplicável ou decisão expressa da Tempo, preservando dados e histórico. A rastreabilidade explica a execução e não constitui uma autoridade adicional. Mantêm-se os limites autorizados de QA e das alterações no activo.
|
||||
|
||||
## Preservação do concluído — pedido expresso da Tempo, 23/10/2026
|
||||
|
||||
Antes de avançar com novas rondas de testes/validações, inventariar na matriz e no manual existentes as funcionalidades concluídas e as funcionalidades desaparecidas, e repor as perdas confirmadas. Preservar as aprovações, dados, originais, relações, versões, histórico e acessos já concluídos. Não transformar uma entrega anterior em funcionalidade nova nem exigir novamente a decisão organizacional já registada.
|
||||
|
||||
Comparar código, schema, dados, configuração e permissões entre o último estado demonstrado e o destino. Distinguir eliminação, perda de visibilidade, diferença de ambiente, pendência real e estado documental desactualizado; uma página vazia ou a falta de nova prova não apagam a aceitação anterior. Reutilizar as provas aceites e conferir apenas o delta da reposição. Cada entrega seguinte deve explicar qualquer redução do âmbito previamente disponível, em vez de a aceitar como consequência normal da implementação.
|
||||
|
||||
Transporte privado não equivale à revogação de uma divulgação anterior aprovada. Reconciliar a decisão anterior de forma delimitada e auditada, sem publicar automaticamente conteúdos novos, reviver exemplares arquivados ou sobrescrever edições posteriores. Mantêm-se backup/restauro, autenticação e os limites concretos de cada tarefa.
|
||||
|
||||
Escolha técnica de verificação: comparar dados protegidos por segurança por linha (RLS) com identidades e contexto de acesso equivalentes. Uma leitura da aplicação sem actor pode ocultar registos que o utilizador do backup consegue ler; confirmar essa diferença antes de declarar perda ou propor reposição. Esta orientação não altera permissões do portal nem autoriza contornar RLS numa operação da equipa.
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
+342
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Services\SimulationFormDrafts;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SimulationOfferDocumentController extends Controller
|
||||
{
|
||||
public function store(Request $request, string $id, SimulationFormDrafts $drafts)
|
||||
{
|
||||
abort_unless(Str::isUuid($id), 404);
|
||||
$drafts->find($id, $request);
|
||||
// Same private document infrastructure and technical file limit as investment attachments.
|
||||
$request->validate(['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,image/png,image/jpeg', 'max:10240']]);
|
||||
$file = $request->file('file');
|
||||
$hash = hash_file('sha256', $file->getRealPath());
|
||||
$existing = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->where('sha256', $hash)->whereNull('deleted_at')->first();
|
||||
if ($existing) {
|
||||
return response()->json(['data' => $this->metadata($id, $existing)], 200)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
$documentId = (string) Str::uuid();
|
||||
$key = $file->storeAs('simulation-offers/'.$id, $documentId, 'local');
|
||||
abort_unless(is_string($key), 500);
|
||||
try {
|
||||
DB::table('documents')->insert(['id' => $documentId, 'source_system' => 'simulation-fiscal-offer', 'external_id' => $id,
|
||||
'file_name' => basename($file->getClientOriginalName()), 'media_type' => $file->getMimeType(), 'storage_key' => $key,
|
||||
'sha256' => $hash, 'size_bytes' => $file->getSize(), 'uploaded_by' => $request->user()->id,
|
||||
'status' => 'quarantined', 'data_classification' => 'confidential']);
|
||||
} catch (\Throwable $e) {
|
||||
Storage::disk('local')->delete($key);
|
||||
throw $e;
|
||||
}
|
||||
ScanDocumentForMalware::dispatch($documentId)->afterCommit();
|
||||
return response()->json(['data' => $this->metadata($id, DB::table('documents')->where('id', $documentId)->first())], 201)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function index(Request $request, string $id, SimulationFormDrafts $drafts)
|
||||
{
|
||||
abort_unless(Str::isUuid($id), 404);
|
||||
$drafts->find($id, $request);
|
||||
$documents = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->get();
|
||||
return response()->json(['data' => $documents->map(fn ($doc) => $this->metadata($id, $doc))])->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function download(Request $request, string $id, string $document, SimulationFormDrafts $drafts)
|
||||
{
|
||||
abort_unless(Str::isUuid($id) && Str::isUuid($document), 404);
|
||||
$drafts->find($id, $request);
|
||||
$doc = DB::table('documents')->where('id', $document)->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->first();
|
||||
abort_unless($doc, 404);
|
||||
abort_unless($doc->status === 'active' && $doc->malware_scan_status === 'clean' && hash_equals(trim($doc->sha256), (string) $doc->malware_scanned_sha256), 409, 'Anexo conservado; descarga disponível após análise antimalware concluída.');
|
||||
$root = realpath(Storage::disk('local')->path('simulation-offers/'.$id));
|
||||
$path = realpath(Storage::disk('local')->path($doc->storage_key));
|
||||
abort_unless($root && $path && str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR)) && is_file($path), 404);
|
||||
abort_unless(hash_equals(trim($doc->sha256), hash_file('sha256', $path)), 409, 'Conteúdo do anexo diverge do hash registado.');
|
||||
return response()->download($path, basename($doc->file_name), ['Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff']);
|
||||
}
|
||||
|
||||
private function metadata(string $id, object $doc): array
|
||||
{
|
||||
return ['id' => $doc->id, 'draftId' => $id, 'name' => $doc->file_name, 'sha256' => trim($doc->sha256),
|
||||
'scan' => $doc->malware_scan_status, 'url' => '/api/v1/simulation-drafts/'.$id.'/offer-documents/'.$doc->id];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\CorridorAgribusinessOpportunities;
|
||||
use App\Services\OpportunityManagement;
|
||||
use App\Services\OpportunityPromotion;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
||||
final class CorridorAgribusinessController extends Controller
|
||||
{
|
||||
private function manifest(CorridorAgribusinessOpportunities $service): string
|
||||
{
|
||||
return hash('sha256', json_encode($service->entries(), JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
public function show(Request $request, CorridorAgribusinessOpportunities $service)
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
abort_unless($access->internalAssignment($request->user(), 'investment_project') || $access->has($request->user(), 'publisher'), 403);
|
||||
$records = Opportunity::withTrashed()->whereIn('management_creation_key', array_column($service->entries(), 'key'))->get()->keyBy('management_creation_key');
|
||||
foreach ($records as $record) abort_unless($access->canView($request->user(), $record), 403);
|
||||
return Inertia::render('management/corridor-agribusiness', ['manifestHash' => $this->manifest($service),
|
||||
'canRegister' => (bool) $access->internalAssignment($request->user(), 'investment_project'),
|
||||
'entries' => array_map(function ($entry) use ($records) {
|
||||
$r = $records->get($entry['key']);
|
||||
return [...$entry, 'id' => $r?->id, 'status' => $r?->status, 'href' => $r ? '/oportunidades/'.$r->slug : null];
|
||||
}, $service->entries())]);
|
||||
}
|
||||
|
||||
public function register(Request $request, CorridorAgribusinessOpportunities $service)
|
||||
{
|
||||
$data = $request->validate(['manifest_hash' => 'required|string|size:64', 'confirm' => 'required|accepted']);
|
||||
abort_unless(hash_equals($this->manifest($service), $data['manifest_hash']), 409, 'A fonte mudou; recupere a prévia.');
|
||||
$access = app(PortalProfileAccess::class);
|
||||
$grant = $access->internalAssignment($request->user(), 'investment_project');
|
||||
abort_unless($grant && $access->mfa($request->user()), 403, 'Use a designação existente e MFA recente.');
|
||||
return response()->json(['records' => $service->register($request->user(), $grant->organization_id)]);
|
||||
}
|
||||
|
||||
public function preview(Request $request, CorridorAgribusinessOpportunities $service)
|
||||
{
|
||||
$previews = [];
|
||||
foreach ($service->entries() as $entry) {
|
||||
$record = Opportunity::where('management_creation_key', $entry['key'])->firstOrFail();
|
||||
$input = data_get($record->workflow_context, 'promotional_summary.input');
|
||||
abort_unless(is_array($input), 409, 'Complete a selecção pública na divulgação individual.');
|
||||
$previews[] = ['id' => $record->id, ...app(OpportunityPromotion::class)->preview($request->user(), $record,
|
||||
[...$input, 'source_date' => $input['source_date'] ?: null, 'action' => 'disclose', 'etag' => app(OpportunityManagement::class)->etag($record)])];
|
||||
}
|
||||
return response()->json(['previews' => $previews]);
|
||||
}
|
||||
|
||||
public function disclose(Request $request, CorridorAgribusinessOpportunities $service)
|
||||
{
|
||||
$data = $request->validate(['confirm' => 'required|accepted', 'reason' => 'required|string|min:20|max:1000',
|
||||
'previews' => 'required|array|size:34', 'previews.*.id' => 'required|integer|distinct',
|
||||
'previews.*.token' => 'required|string|max:300000', 'previews.*.operation_key' => 'required|uuid|distinct']);
|
||||
$expected = Opportunity::whereIn('management_creation_key', array_column($service->entries(), 'key'))->pluck('id')->sort()->values()->all();
|
||||
$supplied = array_column($data['previews'], 'id'); sort($supplied);
|
||||
abort_unless($expected === $supplied && count($expected) === 34, 409, 'A prévia deve identificar exactamente as 34 fichas deste lote.');
|
||||
return response()->json(DB::transaction(function () use ($request, $data) {
|
||||
$result = [];
|
||||
foreach ($data['previews'] as $preview) $result[] = app(OpportunityPromotion::class)->confirm($request->user(), $preview['id'],
|
||||
[...$preview, 'confirm' => true, 'reason' => $data['reason']]);
|
||||
return ['records' => $result];
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -81,31 +81,18 @@ class EditorialArticleController extends Controller
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource(request()->user(), $id);
|
||||
$r = DB::table('portal_resources')->find($id);
|
||||
abort_unless($r, 404);
|
||||
$content = $r->editorial_content ? json_decode($r->editorial_content, true) : null;
|
||||
if ($content === null) {
|
||||
$document = DB::table('documents')->find($r->document_id);
|
||||
if ($document && $document->status === 'active' && $document->malware_scan_status === 'clean'
|
||||
&& in_array($document->media_type, ['text/plain', 'application/json'], true)) {
|
||||
$binary = app(VerifiedDocumentBinary::class)->respond($document);
|
||||
$bytes = $binary instanceof \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
? file_get_contents($binary->getFile()->getPathname()) : $binary->getContent();
|
||||
$original = json_decode($bytes, true);
|
||||
if (is_array($original) && ($original['format'] ?? '') === 'investmoz-legacy-article-v1') {
|
||||
$html = preg_replace('~</(?:p|div|li|h[1-6])>|<br\s*/?>~i', "\n\n", $original['content_html'] ?? '');
|
||||
$content = ['body' => trim(html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8')), 'image_resource_id' => null];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Inertia::render('portal/article-editor', ['resource' => ['id' => $r->id, 'title' => $r->title, 'metadata_version' => $r->metadata_version],
|
||||
'content' => $content, 'images' => $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->get(['r.id', 'r.title']),
|
||||
'revisions' => DB::table('portal_resource_revisions')->where('resource_id', $id)->orderByDesc('revision_no')->get(['revision_no', 'change_type', 'created_at'])]);
|
||||
$reading = app(\App\Services\EditorialReading::class);
|
||||
$original = $reading->original($r);
|
||||
$content = $reading->content($r, $original);
|
||||
return Inertia::render('portal/article-editor', ['resource' => ['id' => $r->id, 'title' => $r->title, 'metadata_version' => $r->metadata_version, 'published' => (bool) $r->published],
|
||||
'content' => $content, 'source' => $original ? array_intersect_key($original, array_flip(['source_url', 'published_at', 'modified_at', 'legacy_id', 'image_source_url'])) : null, 'images' => $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->get(['r.id', 'r.title']),
|
||||
'revisions' => DB::table('portal_resource_revisions')->where('resource_id', $id)->orderByDesc('revision_no')->get(['revision_no', 'change_type', 'created_at'])])->toResponse(request())->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function save(Request $request, int $id)
|
||||
{
|
||||
$this->installed();
|
||||
$data = $request->validate(['body' => ['required', 'string', 'max:100000'], 'image_resource_id' => ['nullable', 'integer'], 'metadata_version' => ['required', 'integer', 'min:1']]);
|
||||
$data = $this->validateContent($request);
|
||||
DB::transaction(function () use ($request, $id, $data) {
|
||||
$r = DB::table('portal_resources')->where('id', $id)->lockForUpdate()->first();
|
||||
abort_unless($r, 404);
|
||||
@@ -118,7 +105,7 @@ class EditorialArticleController extends Controller
|
||||
if ($body === '') {
|
||||
throw ValidationException::withMessages(['body' => 'Escreva o corpo do artigo.']);
|
||||
}
|
||||
$content = ['format' => 1, 'body' => $body, 'image_resource_id' => $data['image_resource_id'] ?? null];
|
||||
$content = $this->edition($r, $data);
|
||||
$revision = (int) $r->metadata_version + 1;
|
||||
DB::table('portal_resources')->where('id', $id)->update(['editorial_content' => json_encode($content, JSON_THROW_ON_ERROR), 'published' => false, 'approved_by' => null, 'metadata_version' => $revision, 'last_reviewed_by' => $request->user()->id, 'last_reviewed_at' => now(), 'updated_at' => now()]);
|
||||
$resource = DB::table('portal_resources')->find($id);
|
||||
@@ -130,16 +117,46 @@ class EditorialArticleController extends Controller
|
||||
return redirect('/management/portal-services/resources/'.$id.'/article')->with('success', 'Artigo guardado como rascunho. Publique expressamente no acervo quando estiver pronto.');
|
||||
}
|
||||
|
||||
public function show(int $id)
|
||||
private function validateContent(Request $request): array
|
||||
{
|
||||
return $request->validate(['body' => ['required', 'string', 'max:100000'], 'image_resource_id' => ['nullable', 'integer'], 'image_mode' => ['nullable', 'in:original,selected,none'], 'metadata_version' => ['required', 'integer', 'min:1']]);
|
||||
}
|
||||
|
||||
private function edition(object $resource, array $data): array
|
||||
{
|
||||
$reading = app(\App\Services\EditorialReading::class);
|
||||
$original = $reading->original($resource);
|
||||
$mode = $data['image_mode'] ?? (!empty($data['image_resource_id']) ? 'selected' : 'none');
|
||||
if ($mode === 'selected') abort_unless(!empty($data['image_resource_id']) && $reading->publicResources()->where('r.id', $data['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->exists(), 422, 'Escolha uma imagem pública disponível na biblioteca.');
|
||||
return ['format' => 1, 'body' => trim($data['body']), 'image_mode' => $mode, 'image_resource_id' => $mode === 'selected' ? (int)$data['image_resource_id'] : null,
|
||||
'preserve_original_html' => $original && trim($data['body']) === $reading->plain($original['content_html'])];
|
||||
}
|
||||
|
||||
public function preview(Request $request, int $id)
|
||||
{
|
||||
$this->installed();
|
||||
$r = $this->publicResources()->where('r.id', $id)->first(['r.id', 'r.title', 'r.description', 'r.editorial_content', 'r.document_id', 'd.issuer']);
|
||||
abort_unless($r && $r->editorial_content, 404);
|
||||
$content = json_decode($r->editorial_content, true);
|
||||
// Recheck the original bytes using the same binary gate as the library.
|
||||
$binary = app(VerifiedDocumentBinary::class)->respond($this->publicResources()->where('r.id', $id)->first(['r.*', 'd.*']));
|
||||
$image = ! empty($content['image_resource_id']) ? $this->publicResources()->where('r.id', $content['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->value('r.id') : null;
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
$resource = DB::table('portal_resources')->find($id); abort_unless($resource, 404);
|
||||
return Inertia::render('portal/article', ['article' => app(\App\Services\EditorialReading::class)->reading($resource, null, true)])
|
||||
->toResponse($request)->header('Cache-Control', 'private, no-store')->header('X-Robots-Tag', 'noindex, nofollow');
|
||||
}
|
||||
|
||||
return Inertia::render('portal/article', ['article' => ['id' => $id, 'title' => $r->title, 'description' => $r->description, 'author' => $r->issuer, 'body' => $content['body'], 'image_url' => $image ? '/documentos/'.$image.'/view' : null, 'original_url' => '/documentos/'.$id]]);
|
||||
public function previewContent(Request $request, int $id)
|
||||
{
|
||||
$this->installed();
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
$resource = DB::table('portal_resources')->find($id); abort_unless($resource, 404);
|
||||
$data = $this->validateContent($request);
|
||||
abort_unless((int)$resource->metadata_version === (int)$data['metadata_version'], 409, 'A revisão mudou; recupere antes de rever a prévia.');
|
||||
return response()->json(['article' => app(\App\Services\EditorialReading::class)->reading($resource, $this->edition($resource, $data), true)])
|
||||
->header('Cache-Control', 'private, no-store')->header('X-Robots-Tag', 'noindex, nofollow');
|
||||
}
|
||||
|
||||
public function show(Request $request, int $id)
|
||||
{
|
||||
$this->installed();
|
||||
$resource = app(\App\Services\EditorialReading::class)->publicResources()->where('r.id', $id)->first(['r.*']);
|
||||
abort_unless($resource, 404);
|
||||
return Inertia::render('portal/article', ['article' => app(\App\Services\EditorialReading::class)->reading($resource)])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\InvestmentComplements;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class InvestmentComplementController extends Controller
|
||||
{
|
||||
public function index(Request $request, InvestmentComplements $service)
|
||||
{
|
||||
$data = $request->validate(['origin' => [\Illuminate\Validation\Rule::requiredIf(!$request->has('contexts')), 'nullable', 'string', 'max:100'], 'territory_id' => 'nullable|uuid',
|
||||
'contexts' => 'sometimes|array|max:100', 'contexts.*' => 'array:origin,territory_id', 'contexts.*.origin' => 'required|string|max:100', 'contexts.*.territory_id' => 'nullable|uuid']);
|
||||
$items = [];
|
||||
$issues = [];
|
||||
foreach ($data['contexts'] ?? [['origin' => $data['origin'], 'territory_id' => $data['territory_id'] ?? null]] as $context) {
|
||||
try {
|
||||
$items = [...$items, ...$service->options($context['origin'], $context['territory_id'] ?? null)];
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException|\Symfony\Component\HttpKernel\Exception\HttpException $e) {
|
||||
$status = $e instanceof \Symfony\Component\HttpKernel\Exception\HttpException ? $e->getStatusCode() : 404;
|
||||
if (!isset($data['contexts']) || $status !== 404) throw $e;
|
||||
$issues[] = ['origin' => $context['origin'], 'message' => 'Uma referência escolhida deixou de estar disponível para consulta pública. A relação guardada conserva-se no dossier; os outros complementos continuam disponíveis.'];
|
||||
}
|
||||
}
|
||||
return response()->json(isset($data['contexts']) ? ['items' => $items, 'issues' => $issues] : $items)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
}
|
||||
|
||||
return Inertia::render('investment/catalogue', ['filters' => $filters, 'catalogueUrl' => $request->routeIs('investment.education') ? '/educacao' : '/oportunidades', 'references' => $catalogue->references(false, $filters),
|
||||
'corridorGroups' => ($filters['view'] ?? '') === 'corridors' ? app(\App\Services\OpportunityCorridorExplorer::class)->groups($filters) : [],
|
||||
'territorySelection' => app(InvestmentGeography::class)->selection($filters),
|
||||
'guides' => app(\App\Services\SectorInvestment::class)->guides(\App\Models\Sector::where('slug', $filters['sector'] ?? '')->where('is_active', true)->whereNotNull('published_at')->first()),
|
||||
'results' => $catalogue->query($filters)->paginate(12)->withQueryString(),
|
||||
@@ -88,7 +89,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
$data = $request->validate(['zone_id' => ['nullable', 'uuid'], 'from_dossier' => ['nullable', 'uuid']]);
|
||||
$dossierOrigin = isset($data['from_dossier']) ? InvestmentPortfolioController::visible()->whereKey($data['from_dossier'])->firstOrFail() : null;
|
||||
if ($dossierOrigin) {
|
||||
$linked = false;
|
||||
$linked = in_array($record->id, app(\App\Services\CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($dossierOrigin), true);
|
||||
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) {
|
||||
if ($dossierOrigin->$foreign && DB::table('opportunity_'.$relation)->where('opportunity_id', $record->id)->where($foreign, $dossierOrigin->$foreign)->exists()) {
|
||||
$linked = true;
|
||||
@@ -106,7 +107,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
$returnFilters = $request->query();
|
||||
unset($returnFilters['zone_id'], $returnFilters['from_dossier']);
|
||||
|
||||
return Inertia::render('investment/detail', ['record' => $record, 'context' => $catalogue->context($record->id), 'zone_origin' => $origin,
|
||||
return Inertia::render('investment/detail', ['complements' => app(\App\Services\InvestmentComplements::class)->options('opportunity:'.$record->id), 'record' => $record, 'context' => $catalogue->context($record->id), 'zone_origin' => $origin,
|
||||
'guides' => app(\App\Services\SectorInvestment::class)->guides($record->sector),
|
||||
'dossierOrigin' => $dossierOrigin ? ['id' => $dossierOrigin->id, 'title' => $dossierOrigin->title] : null,
|
||||
'returnUrl' => $returnFilters === [] ? '/oportunidades' : '/oportunidades?'.http_build_query($returnFilters)]);
|
||||
|
||||
@@ -61,8 +61,15 @@ class InvestmentPortfolioController extends Controller
|
||||
{
|
||||
$management = $request->routeIs('management.portfolio*');
|
||||
$filters = $request->validate(['q' => ['nullable', 'string', 'max:120'], 'kind' => ['nullable', 'in:corridor,cluster,project'],
|
||||
'sector' => ['nullable', 'string', 'max:500'],
|
||||
'province_id' => ['nullable', 'uuid'], 'district_id' => ['nullable', 'uuid'], 'admin_post_id' => ['nullable', 'uuid'], 'locality_id' => ['nullable', 'uuid']]);
|
||||
$query = $management ? InvestmentDossier::query() : self::visible();
|
||||
$sectorOptions = ($management ? InvestmentDossier::query() : self::visible())
|
||||
->whereNotNull('content->sector')->get()->pluck('content.sector')
|
||||
->filter(fn ($sector) => is_string($sector) && trim($sector) !== '')->unique()->sort()->values();
|
||||
if (! empty($filters['sector'])) {
|
||||
$query->where('content->sector', $filters['sector']);
|
||||
}
|
||||
if (! empty($filters['kind'])) {
|
||||
$query->whereNotNull($filters['kind'].'_id');
|
||||
}
|
||||
@@ -76,7 +83,7 @@ class InvestmentPortfolioController extends Controller
|
||||
}
|
||||
|
||||
return Inertia::render('portfolio/index', ['records' => $query->orderBy('title')->paginate(30)->withQueryString(),
|
||||
'management' => $management, 'filters' => $filters, 'territorySelection' => $territorySelection]);
|
||||
'management' => $management, 'filters' => $filters, 'sectors' => $sectorOptions, 'territorySelection' => $territorySelection]);
|
||||
}
|
||||
|
||||
public function show(Request $request, string $id): Response
|
||||
@@ -105,6 +112,7 @@ class InvestmentPortfolioController extends Controller
|
||||
|
||||
$opportunities = Opportunity::published()->where(function ($query) use ($record) {
|
||||
$query->whereRaw('false');
|
||||
$query->orWhereIn('id', app(\App\Services\CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record));
|
||||
foreach (['clusters' => 'cluster_id', 'corridors' => 'corridor_id', 'projects' => 'project_id'] as $relation => $foreign) {
|
||||
if ($record->$foreign) {
|
||||
$query->orWhereIn('id', DB::table('opportunity_'.$relation)->where($foreign, $record->$foreign)->select('opportunity_id'));
|
||||
@@ -130,12 +138,19 @@ class InvestmentPortfolioController extends Controller
|
||||
->orderByDesc('updated_at')->limit(10)->get(['id', 'version', 'updated_at'])
|
||||
: collect();
|
||||
|
||||
return Inertia::render('portfolio/show', ['record' => $record, 'related' => $related, 'management' => $management, 'zones' => $zones,
|
||||
return Inertia::render('portfolio/show', ['complements' => $management ? [] : app(\App\Services\InvestmentComplements::class)->options('portfolio:'.$record->id), 'record' => $record, 'related' => $related, 'management' => $management, 'zones' => $zones,
|
||||
'anchorProject' => $anchorProject, 'existingSubmissions' => $existingSubmissions,
|
||||
'geography' => app(InvestmentRelations::class)->dossier($record, $management),
|
||||
'origin' => $origin ? ['id' => $origin->id, 'title' => $origin->title] : null, 'opportunities' => $opportunities, 'chains' => $chains]);
|
||||
}
|
||||
|
||||
public function resumeInterest(Request $request, string $id): RedirectResponse
|
||||
{
|
||||
self::visible()->findOrFail($id);
|
||||
$data = $request->validate(['catalogue_query' => ['nullable', 'string', 'max:4000']]);
|
||||
return redirect('/carteira/'.$id.'?'.http_build_query([...$data, 'prepare_interest' => '1']));
|
||||
}
|
||||
|
||||
public function save(Request $request, InvestmentDossier $dossier): RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['version_no' => ['required', 'integer', 'min:1'], 'title' => ['required', 'string', 'max:255'], 'source' => ['required', 'string', 'max:2000'],
|
||||
|
||||
@@ -192,11 +192,13 @@ class InvestmentSubmissionController extends Controller
|
||||
{
|
||||
$required = $submitting ? 'required' : 'nullable';
|
||||
$data = $request->validate(['version' => ['required', 'integer', 'min:1'],
|
||||
'payload' => ['required', 'array:context_reuse,full_name,email,phone,country,residence_country_code,incorporation_country_code,organization_country_snapshot,declared_origin_country_code,organisation,representative_role,experience,investment_capacity,profile_id,sector,objective,participation_id,partnership_id,estimated_amount,currency,horizon,location,cluster_ids,corridor_ids,project_ids,contact_consent,other_details'],
|
||||
'payload' => ['required', 'array:context_opportunity,context_opportunity_snapshot,context_reuse,full_name,email,phone,country,residence_country_code,incorporation_country_code,organization_country_snapshot,declared_origin_country_code,organisation,representative_role,experience,investment_capacity,profile_id,sector,objective,participation_id,partnership_id,estimated_amount,currency,horizon,location,cluster_ids,corridor_ids,project_ids,contact_consent,other_details'],
|
||||
'payload.organization_country_snapshot' => ['sometimes', 'array', 'max:10'],
|
||||
'payload.residence_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
|
||||
'payload.incorporation_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
|
||||
'payload.declared_origin_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
|
||||
'payload.context_opportunity' => ['nullable', 'string', 'max:100'],
|
||||
'payload.context_opportunity_snapshot' => ['nullable', 'string', 'max:200000'],
|
||||
'payload.context_reuse' => ['nullable', 'string', 'max:10000'],
|
||||
'payload.other_details' => ['nullable', 'string', 'max:5000'],
|
||||
'payload.full_name' => [$required, 'string', ...($submitting ? ['min:3'] : []), 'max:150'], 'payload.email' => [$required, $submitting ? 'email:rfc' : 'string', 'max:150'],
|
||||
@@ -247,6 +249,9 @@ class InvestmentSubmissionController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$bound = DB::table('desk_cases')->where('submission_id', $request->route('id'))->where('user_id', $request->user()->id)->whereNotNull('journey_id')->first();
|
||||
if ($bound) $data['payload'] = app(\App\Services\InvestmentConsumerScope::class)->bind($request->user(), $bound->id, $data['payload'], $submitting);
|
||||
else unset($data['payload']['context_opportunity'], $data['payload']['context_opportunity_snapshot']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -266,7 +271,7 @@ class InvestmentSubmissionController extends Controller
|
||||
$version = DB::transaction(function () use ($request, $id, $data) {
|
||||
$row = $this->owned($request, $id, true, true);
|
||||
abort_unless($row->status === 'draft' && $row->version === (int) $data['version'], 409, 'Rascunho alterado. Recarregue antes de guardar.');
|
||||
$row->update(['payload' => $this->preserveCountrySource($row, $data['payload']), 'version' => $row->version + 1]);
|
||||
$row->update([...(isset($data['payload']['context_opportunity']) ? ['opportunity_id'=>(int)substr($data['payload']['context_opportunity'],12)] : []), 'payload' => $this->preserveCountrySource($row, $data['payload']), 'version' => $row->version + 1]);
|
||||
InvestmentSpecificNeed::where('submission_id', $id)->delete();
|
||||
foreach ($data['needs'] ?? [] as $need) {
|
||||
InvestmentSpecificNeed::create([...$need, 'submission_id' => $id]);
|
||||
@@ -310,7 +315,8 @@ class InvestmentSubmissionController extends Controller
|
||||
return InterestRequest::findOrFail($row->interest_request_id)->reference;
|
||||
}
|
||||
abort_unless($row->version === (int) $data['version'], 409, 'Rascunho alterado. Recarregue.');
|
||||
$opportunity = $row->opportunity_id ? Opportunity::published()->findOrFail($row->opportunity_id) : null;
|
||||
$scopeKey = $data['payload']['context_opportunity'] ?? null;
|
||||
$opportunity = $scopeKey ? Opportunity::published()->findOrFail(substr($scopeKey,12)) : ($row->opportunity_id ? Opportunity::published()->findOrFail($row->opportunity_id) : null);
|
||||
if ($row->dossier_id) {
|
||||
InvestmentPortfolioController::visible()->findOrFail($row->dossier_id);
|
||||
}
|
||||
@@ -331,12 +337,14 @@ class InvestmentSubmissionController extends Controller
|
||||
'sector_interest' => $p['sector'], 'message' => $p['objective'], 'privacy_consent' => true, 'consented_at' => now(),
|
||||
'status' => 'received', 'source_page' => '/investidor/manifestacoes/'.$id,
|
||||
'metadata' => ['submission_id' => $id, 'portal_user_id' => $request->user()->id, 'dossier_id' => $row->dossier_id,
|
||||
'context_opportunity' => $p['context_opportunity'] ?? null,
|
||||
'context_opportunity_snapshot' => $p['context_opportunity_snapshot'] ?? null,
|
||||
'declared_origin_country_code' => $p['declared_origin_country_code'] ?? null,
|
||||
'residence_country_code' => $p['residence_country_code'] ?? null,
|
||||
'incorporation_country_code' => $p['incorporation_country_code'] ?? null,
|
||||
...(isset($p['organization_country_snapshot']) ? ['organization_country_snapshot' => $p['organization_country_snapshot']] : []),
|
||||
...($zoneOrigin ? ['zone_origin' => (array) $zoneOrigin] : [])]]));
|
||||
$row->update(['payload' => $p, 'version' => $row->version + 1, 'interest_request_id' => $interest->id, 'status' => 'submitted']);
|
||||
$row->update(['opportunity_id'=>$opportunity?->id, 'payload' => $p, 'version' => $row->version + 1, 'interest_request_id' => $interest->id, 'status' => 'submitted']);
|
||||
InvestmentSpecificNeed::where('submission_id', $id)->delete();
|
||||
foreach ($data['needs'] ?? [] as $need) {
|
||||
InvestmentSpecificNeed::create([...$need, 'submission_id' => $id]);
|
||||
|
||||
@@ -19,12 +19,21 @@ class InvestmentTerritoryController extends Controller
|
||||
$data = $request->validate(['territory_id' => ['required', 'uuid']]);
|
||||
$profile = $request->is('minhas-oportunidades/*');
|
||||
$management = $request->is('management/*') || $profile;
|
||||
$actor = $request->user();
|
||||
$access = app(PortalProfileAccess::class);
|
||||
// The general consumers use this same endpoint while authenticated.
|
||||
// Reuse the read access already granted by the private cadastral routes.
|
||||
$profileAccess = $actor && $actor->hasVerifiedEmail()
|
||||
&& ($access->grants($actor) !== [] || $access->superuser($actor));
|
||||
$internal = $profileAccess || ($actor?->hasVerifiedEmail() && $actor->can('manage-portal'));
|
||||
if ($management) {
|
||||
$access = app(PortalProfileAccess::class);
|
||||
abort_unless($profile ? ($access->grants($request->user()) !== [] || $access->superuser($request->user())) : $request->user()?->can('manage-portal'), 403);
|
||||
abort_unless($profile ? $profileAccess : $internal, 403);
|
||||
}
|
||||
|
||||
return response()->json(app(TerritoryGeometry::class)->context($data['territory_id'], $management));
|
||||
return response()->json([
|
||||
...app(TerritoryGeometry::class)->context($data['territory_id'], $internal),
|
||||
'geometry_scope' => $internal ? 'authorized_internal' : 'public',
|
||||
])->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
private const TABLES = ['opportunities' => 'opportunities', 'projects' => 'projects', 'clusters' => 'economic_clusters', 'corridors' => 'development_corridors', 'zones' => 'investment_zones'];
|
||||
|
||||
@@ -11,6 +11,36 @@ use Inertia\Inertia;
|
||||
|
||||
final class InvestorContextController extends Controller
|
||||
{
|
||||
private function entryComplements(Request $request): array
|
||||
{
|
||||
$data = $request->validate(['complement_origin' => 'nullable|string|max:100', 'complement_code' => 'required_with:complement_origin|string|max:40',
|
||||
'complement_target' => 'nullable|string|max:100', 'complement_territory' => 'nullable|uuid']);
|
||||
if (empty($data['complement_origin'])) return [];
|
||||
return app(\App\Services\InvestmentComplements::class)->resolve([['origin' => $data['complement_origin'], 'code' => $data['complement_code'],
|
||||
'target' => $data['complement_target'] ?? null, 'territory_id' => $data['complement_territory'] ?? null]]);
|
||||
}
|
||||
|
||||
private function entryChoices(Request $r, InvestorContext $context): array
|
||||
{
|
||||
$data = $r->validate(['source' => 'nullable|string|max:100', 'from_dossier' => 'nullable|uuid', 'opportunities' => 'sometimes|array|max:100', 'opportunities.*' => 'integer|distinct|min:1',
|
||||
'province_id' => 'nullable|uuid', 'district_id' => 'nullable|uuid', 'admin_post_id' => 'nullable|uuid', 'locality_id' => 'nullable|uuid']);
|
||||
$source = !empty($data['source']) ? $context->reference($data['source']) : null;
|
||||
$ids = $data['opportunities'] ?? (($source['kind'] ?? '') === 'opportunity' ? [$source['id']] : []);
|
||||
abort_if(!empty($data['from_dossier']) && !$ids, 422, 'Escolha a oportunidade associada à ficha da carteira.');
|
||||
$path = app(InvestmentGeography::class)->selection($data);
|
||||
$input = array_map(fn($id) => ['source' => 'opportunity:'.$id, ...($path ? ['territory' => 'territory:'.$path[array_key_last($path)]->id] : [])], $ids);
|
||||
if (!empty($data['from_dossier'])) $input[0]['portfolio_origins'] = [$data['from_dossier']];
|
||||
foreach ($this->entryComplements($r) as $complement) {
|
||||
$keys = array_filter([$complement['origin']['key'], $complement['target']['key'] ?? null], fn($key) => str_starts_with($key, 'opportunity:'));
|
||||
foreach ($keys as $key) {
|
||||
if (in_array($key, array_column($input, 'source'), true)) continue;
|
||||
$input[] = ['source' => $key, ...($complement['territory_id'] ? ['territory' => 'territory:'.$complement['territory_id']] : []),
|
||||
...(str_starts_with($complement['origin']['key'], 'portfolio:') ? ['portfolio_origins' => [substr($complement['origin']['key'], 10)]] : [])];
|
||||
}
|
||||
}
|
||||
return app(\App\Services\InvestmentOpportunityChoices::class)->resolve($input, [], true);
|
||||
}
|
||||
|
||||
public function current(Request $r, InvestorContext $context)
|
||||
{
|
||||
return response()->json($context->current($r->user()));
|
||||
@@ -23,7 +53,8 @@ final class InvestorContextController extends Controller
|
||||
|
||||
public function show(Request $r, string $id, InvestorContext $context)
|
||||
{
|
||||
return response()->json($context->view($r->user(), $id));
|
||||
$data = $r->validate(['opportunity' => 'nullable|string|max:100']);
|
||||
return response()->json($context->view($r->user(), $id, $data['opportunity'] ?? null));
|
||||
}
|
||||
|
||||
public function options(Request $r, InvestorContext $context)
|
||||
@@ -33,12 +64,56 @@ final class InvestorContextController extends Controller
|
||||
return response()->json($context->options($d['q'] ?? ''));
|
||||
}
|
||||
|
||||
public function corridorOptions(Request $r)
|
||||
{
|
||||
$data = $r->validate(['corridor' => ['nullable', \Illuminate\Validation\Rule::in(array_keys(config('investment_corridors')))], 'territory' => 'nullable|uuid']);
|
||||
$service = app(\App\Services\InvestmentCorridorContext::class);
|
||||
return response()->json(['corridors' => collect(config('investment_corridors'))->map(fn($entry,$key)=>['key'=>$key,'name'=>$entry['name'],'provinces'=>$service->provinces($key)])->values(),
|
||||
'parks' => $service->parks($data['corridor'] ?? null, $data['territory'] ?? null)]);
|
||||
}
|
||||
|
||||
public function saveDraft(Request $r, string $id, InvestorContext $context)
|
||||
{
|
||||
return response()->json($context->saveDraft($r->user(), $id, $r->all()));
|
||||
}
|
||||
|
||||
public function edit(Request $r, string $id, InvestorContext $context)
|
||||
{
|
||||
$data = $context->view($r->user(), $id);
|
||||
abort_unless($data['can_write'], 403);
|
||||
|
||||
return Inertia::render('investor-path/context', ['initial' => $data, 'options' => $context->options(), 'source' => $r->query('source') ? $context->reference($r->validate(['source' => 'required|string|max:100'])['source']) : null, 'territorySelection' => isset($data['context']['selection']['territory']) ? app(InvestmentGeography::class)->path($data['context']['selection']['territory']['id']) : []]);
|
||||
$entries = $this->entryChoices($r, $context);
|
||||
$choices = $entries ?: null;
|
||||
$source = $r->query('source') ? $context->reference($r->validate(['source' => 'required|string|max:100'])['source']) : null;
|
||||
if ($choices === null && ($source['kind'] ?? '') === 'opportunity') $choices = app(\App\Services\InvestmentOpportunityChoices::class)->resolve([['source' => $source['key']]]);
|
||||
$explicitChoices=$choices;
|
||||
$selectionKey=$r->validate(['selection_key'=>'nullable|uuid'])['selection_key']??null;
|
||||
$entryKey=fn($c)=>$c['opportunity']['key'].($selectionKey?'@'.$selectionKey:'');
|
||||
$entrySources=array_map($entryKey,$explicitChoices??[]);
|
||||
if ($choices !== null) $choices = app(\App\Services\InvestmentOpportunityChoices::class)->mergeEntries($data['context']['opportunity_choices'] ?? [], $choices);
|
||||
$draft = $context->draft($r->user(), $id);
|
||||
if ($draft && isset($draft['opportunity_choices'])) {
|
||||
$recovered = app(\App\Services\InvestmentOpportunityChoices::class)->resolve($draft['opportunity_choices'], $data['context']['opportunity_choices'] ?? [], true);
|
||||
$new=array_filter($explicitChoices??[],fn($c)=>!in_array($entryKey($c),$draft['entry_sources']??[],true));
|
||||
$choices = app(\App\Services\InvestmentOpportunityChoices::class)->mergeEntries($recovered, array_values($new));
|
||||
}
|
||||
$differences = [];
|
||||
foreach ($explicitChoices ?? [] as $entry) {
|
||||
$kept = collect($choices ?? [])->firstWhere('opportunity.key', $entry['opportunity']['key']);
|
||||
if ($kept && ($kept['territory']['key'] ?? null) !== ($entry['territory']['key'] ?? null)) $differences[] = ['title'=>$entry['opportunity']['title'],'saved'=>$kept['territory']['title']??'Por definir','requested'=>$entry['territory']['title']??'Por definir'];
|
||||
}
|
||||
$complements = isset($draft['complements']) ? app(\App\Services\InvestmentComplements::class)->resolve($draft['complements'], $data['context']['complements'] ?? []) : ($data['context']['complements'] ?? []);
|
||||
$entryTerritory = null;
|
||||
if (($source['kind'] ?? null) === 'portfolio') {
|
||||
$path = app(InvestmentGeography::class)->selection($r->validate(['province_id' => 'nullable|uuid', 'district_id' => 'nullable|uuid', 'admin_post_id' => 'nullable|uuid', 'locality_id' => 'nullable|uuid']));
|
||||
if ($path) $entryTerritory = $context->reference('territory:'.$path[array_key_last($path)]->id);
|
||||
}
|
||||
foreach ($this->entryComplements($r) as $entry) {
|
||||
$tag = 'complement:'.hash('sha256', $entry['key'].'@'.($selectionKey ?? ''));
|
||||
$entrySources[] = $tag;
|
||||
if (!in_array($tag, $draft['entry_sources'] ?? [], true)) $complements = collect([...$complements, $entry])->unique('key')->values()->all();
|
||||
}
|
||||
return Inertia::render('investor-path/context', ['entryTerritory' => $entryTerritory, 'entryComplements' => $complements, 'entryLocationDifferences'=>$differences,'entrySources'=>$entrySources,'draft' => $draft, 'requestedChoices' => $choices, 'initial' => $data, 'options' => $context->options(), 'source' => $source, 'territorySelection' => isset($data['context']['selection']['territory']) ? app(InvestmentGeography::class)->path($data['context']['selection']['territory']['id']) : ($entryTerritory ? app(InvestmentGeography::class)->path($entryTerritory['id']) : [])]);
|
||||
}
|
||||
|
||||
public function preview(Request $r, string $id, InvestorContext $context)
|
||||
@@ -55,8 +130,16 @@ final class InvestorContextController extends Controller
|
||||
{
|
||||
$d = $r->validate(['source' => 'nullable|string|max:100', 'step' => ['nullable', 'regex:/^S(0[1-9]|1[0-9])$/']]);
|
||||
$ref = isset($d['source']) ? $context->reference($d['source']) : null;
|
||||
$submissionId = $r->validate(['submission' => 'nullable|uuid'])['submission'] ?? null;
|
||||
$entrySubmission = null;
|
||||
if ($submissionId) {
|
||||
$submission = \App\Models\InvestmentSubmission::where('id', $submissionId)->where('user_id', $r->user()->id)->where('status', 'draft')->firstOrFail();
|
||||
abort_unless(($ref['kind'] ?? null) === 'portfolio' && $submission->dossier_id === $ref['id'], 422, 'Conserve a ficha de origem da manifestação.');
|
||||
$entrySubmission = ['id' => $submission->id, 'version' => $submission->version];
|
||||
}
|
||||
|
||||
return Inertia::render('investor-path/context-start', ['contexts' => $path->contexts($r->user()), 'source' => $ref, 'step' => $d['step'] ?? null]);
|
||||
$choices = $this->entryChoices($r, $context);
|
||||
return Inertia::render('investor-path/context-start', ['entrySubmission' => $entrySubmission, 'entryComplements' => $this->entryComplements($r), 'entryQuery' => $r->only(['complement_origin', 'complement_code', 'complement_target', 'complement_territory', 'from_dossier', 'province_id', 'district_id', 'admin_post_id', 'locality_id']), 'opportunityChoices' => $choices, 'contexts' => $path->contexts($r->user()), 'source' => $ref, 'step' => $d['step'] ?? null]);
|
||||
}
|
||||
|
||||
public function installation(Request $r, InvestorContextInstallation $install)
|
||||
|
||||
@@ -88,7 +88,8 @@ class InvestorPathController extends Controller
|
||||
|
||||
public function show(Request $request, string $id, InvestorPath $path): JsonResponse|Response
|
||||
{
|
||||
$data = $path->view($request->user(), $id);
|
||||
$choice = $request->validate(['opportunity' => 'nullable|string|max:100'])['opportunity'] ?? null;
|
||||
$data = $path->view($request->user(), $id, $choice);
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('investor-path/show', $data);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ class OpportunityManagementController extends Controller
|
||||
$records = $query->orderByDesc('id')->paginate(20)->withQueryString();
|
||||
$records->getCollection()->each(function (Opportunity $r) use ($access, $service, $request) {
|
||||
$r->setAttribute('can_edit', $access->canEdit($request->user(), $r));
|
||||
$r->setAttribute('can_promote', app(\App\Services\OpportunityPromotion::class)->allowed($request->user(), $r));
|
||||
$r->setAttribute('working_status', $access->state($r));
|
||||
$r->setAttribute('publication', $r->publicationPresentation());
|
||||
$r->setAttribute('draft_title', $service->values($r)['canonical']['official_title'] ?? $r->title);
|
||||
@@ -130,6 +131,7 @@ class OpportunityManagementController extends Controller
|
||||
'profileReauthenticationRequired' => $record && $record->owner_user_id !== null
|
||||
? app(OpportunityProfileWorkflow::class)->reauthenticationRequired($request->user(), $record) : false,
|
||||
'publicationState' => $record?->publicationPresentation(),
|
||||
'promotionHref' => $record && app(\App\Services\OpportunityPromotion::class)->allowed($request->user(), $record) ? '/minhas-oportunidades/'.$record->id.'/divulgacao' : null,
|
||||
'canonicalResponse' => $record?->owner_user_id !== null ? app(OpportunityCanonicalResponse::class)->read($request->user(), $record) : null,
|
||||
'canEditDraft' => $record ? $access->canEdit($request->user(), $record) : false,
|
||||
'workflowParticipants' => $record && $record->owner_user_id !== null ? app(OpportunityProfileWorkflow::class)->participants($request->user(), $record) : ['validators' => [], 'publishers' => []],
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\OpportunityPromotion;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
final class OpportunityPromotionController extends Controller
|
||||
{
|
||||
public function show(Request $request, int $id, OpportunityPromotion $service): \Inertia\Response
|
||||
{
|
||||
return Inertia::render('management/opportunity-promotion', $service->editor($request->user(), Opportunity::findOrFail($id)));
|
||||
}
|
||||
|
||||
public function preview(Request $request, int $id, OpportunityPromotion $service): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
return response()->json($service->preview($request->user(), Opportunity::findOrFail($id), $request->all()));
|
||||
}
|
||||
|
||||
public function confirm(Request $request, int $id, OpportunityPromotion $service): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
return response()->json($service->confirm($request->user(), $id, $request->all()));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use App\Services\CountryCatalogue;
|
||||
use App\Services\IdempotencyConflict;
|
||||
use App\Services\InterestSubmission;
|
||||
use App\Services\LegacyContent;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use App\Services\VirtualDesk;
|
||||
@@ -274,7 +275,7 @@ class PortalServicesController extends Controller
|
||||
'resource' => $resource,
|
||||
'backUrl' => $backUrl,
|
||||
'downloadUrl' => route('services.download', ['id' => $id]),
|
||||
'articleUrl' => Schema::hasColumn('portal_resources','editorial_content') && DB::table('portal_resources')->where('id',$id)->whereNotNull('editorial_content')->exists() ? '/artigos/'.$id : null,
|
||||
'articleUrl' => $this->articleUrl($id),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -297,6 +298,7 @@ class PortalServicesController extends Controller
|
||||
|
||||
public function manage(Request $request, MalwareScanner $scanner): Response
|
||||
{
|
||||
$managementFilters = $request->validate(['q' => 'nullable|string|max:150', 'state' => 'nullable|in:draft,published,withdrawn', 'intent' => 'nullable|in:document,legislation']);
|
||||
$access = app(\App\Services\DocumentManagementAccess::class);
|
||||
if (! $access->full($request->user())) {
|
||||
$documents = $access->documents($request->user())->where('status', '!=', 'deleted');
|
||||
@@ -304,7 +306,7 @@ class PortalServicesController extends Controller
|
||||
$health['signatures_at'] = $health['signatures_at']?->format(DATE_ATOM);
|
||||
|
||||
$editorial = $access->editorial($request->user());
|
||||
$resources = $editorial ? $access->editorialResources($request->user())->orderByDesc('r.updated_at')
|
||||
$resources = $editorial ? $this->filterManagedResources($access->editorialResources($request->user()), $managementFilters)->orderByDesc('r.updated_at')
|
||||
->get(['r.*', 'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.status as file_status', 'd.data_classification', 'd.version_no as file_version', 'd.rights_status as file_rights_status', 'd.malware_scan_status', 'd.malware_failure_reason']) : collect();
|
||||
$links = DB::table('portal_resource_links')->whereIn('resource_id', $resources->pluck('id'))->orderBy('id')->get()->groupBy('resource_id');
|
||||
$revisions = DB::table('portal_resource_revisions')->whereIn('resource_id', $resources->pluck('id'))->orderByDesc('revision_no')->get(['resource_id', 'revision_no', 'change_type', 'actor_user_id', 'created_at'])->groupBy('resource_id');
|
||||
@@ -313,6 +315,7 @@ class PortalServicesController extends Controller
|
||||
$resource->revisions = $revisions->get($resource->id, collect())->values();
|
||||
});
|
||||
return Inertia::render('portal/manage', [
|
||||
'managementFilters' => $managementFilters, 'associationOptions' => $this->contentAssociations($request),
|
||||
'documentAccessOnly' => ! $editorial, 'editorialScopeOnly' => $editorial, 'resources' => $resources, 'partnerships' => [], 'territories' => [],
|
||||
'requirementCatalogue' => [], 'requirementAssociationsEnabled' => false,
|
||||
'disclosureAuthorization' => null, 'incompleteCount' => $resources->filter(fn ($r) => ! $r->description || ! $r->issuer || ! $r->source_url || $r->source_confirmation === 'unconfirmed' || $r->legal_status === 'unknown' || $r->rights_status === 'unconfirmed')->count(), 'possibleDuplicates' => $resources->whereNotNull('duplicate_of_resource_id')->count(),
|
||||
@@ -331,7 +334,7 @@ class PortalServicesController extends Controller
|
||||
->where('external_id', 'document-repository-disclosure-authorization-2026-10-06-v1')
|
||||
->where('action', 'public_disclosure_authorized')->first(['external_id', 'occurred_at', 'after_data']);
|
||||
$disclosureAuthorizationData = $disclosureAuthorization ? json_decode($disclosureAuthorization->after_data, true) : null;
|
||||
$resources = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->orderByDesc('r.updated_at')->limit(200)
|
||||
$resources = $this->filterManagedResources(DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at'), $managementFilters)->orderByDesc('r.updated_at')->limit(200)
|
||||
->get(['r.*', 'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.status as file_status', 'd.data_classification', 'd.version_no as file_version', 'd.rights_status as file_rights_status', 'd.malware_scan_status', 'd.malware_failure_reason']);
|
||||
$links = DB::table('portal_resource_links')->whereIn('resource_id', $resources->pluck('id'))->orderBy('id')->get()->groupBy('resource_id');
|
||||
$resources->each(fn ($resource) => $resource->links = $links->get($resource->id, collect())->values());
|
||||
@@ -342,6 +345,7 @@ class PortalServicesController extends Controller
|
||||
$malwareHealth['signatures_at'] = $malwareHealth['signatures_at']?->format(DATE_ATOM);
|
||||
|
||||
return Inertia::render('portal/manage', ['resources' => $resources,
|
||||
'managementFilters' => $managementFilters, 'associationOptions' => $this->contentAssociations($request),
|
||||
'requirementCatalogue' => $this->requirementEvidenceEnabled()
|
||||
? collect(is_array($catalogue = config('document-requirements.catalogue', [])) ? $catalogue : [])->map(fn ($item, $key) => ['key' => $key, ...$item])->values()
|
||||
: collect(),
|
||||
@@ -367,6 +371,39 @@ class PortalServicesController extends Controller
|
||||
'possibleDuplicates' => DB::table('portal_resources')->whereNotNull('duplicate_of_resource_id')->count()]);
|
||||
}
|
||||
|
||||
private function filterManagedResources(Builder $query, array $filters): Builder
|
||||
{
|
||||
if (! empty($filters['q'])) {
|
||||
$term = '%'.addcslashes($filters['q'], '%_\\').'%';
|
||||
$query->where(fn ($q) => $q->where('r.title', 'ilike', $term)->orWhere('r.document_number', 'ilike', $term)->orWhere('d.issuer', 'ilike', $term)->orWhere('d.file_name', 'ilike', $term));
|
||||
}
|
||||
if (! empty($filters['state'])) {
|
||||
$query->where('r.published', $filters['state'] === 'published');
|
||||
if ($filters['state'] !== 'published') {
|
||||
$method = $filters['state'] === 'withdrawn' ? 'whereExists' : 'whereNotExists';
|
||||
$query->$method(fn ($q) => $q->selectRaw('1')->from('portal_resource_revisions as history')->whereColumn('history.resource_id', 'r.id')->whereIn('history.change_type', ['unpublished', 'withdrawn']));
|
||||
}
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function contentAssociations(Request $request): array
|
||||
{
|
||||
$access = app(\App\Services\DocumentManagementAccess::class);
|
||||
$resources = $access->full($request->user()) ? DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at')
|
||||
: ($access->editorial($request->user()) ? $access->editorialResources($request->user()) : null);
|
||||
$documents = $resources ? $resources->orderBy('r.title')->get(['r.id as value', 'r.title as label', 'r.document_type']) : collect();
|
||||
return [
|
||||
'sector' => \App\Models\Sector::choices()->orderBy('name')->get(['id as value', 'name as label']),
|
||||
'territory' => app(\App\Services\InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id as value', 'name as label']),
|
||||
'opportunity' => app(PortalProfileAccess::class)->scope(\App\Models\Opportunity::query(), $request->user())->orderBy('title')->get(['id as value', 'title as label']),
|
||||
'document' => $documents,
|
||||
'procedure' => $documents->where('document_type', 'procedure')->values(),
|
||||
'stage' => collect(config('virtual-desk.steps', []))->map(fn ($step, $key) => ['value' => $key, 'label' => $step['title']])->values(),
|
||||
'activity' => app(\App\Services\EconomicReferenceCatalog::class)->activities()->map(fn ($row) => ['value' => $row->id, 'label' => $row->display_label])->values(),
|
||||
];
|
||||
}
|
||||
|
||||
public function upload(Request $request): RedirectResponse
|
||||
{
|
||||
$this->validateManagement($request, ['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,text/csv,image/jpeg,image/png,image/webp,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.text'.(app(LegacyContent::class)->available() ? ',application/json' : ''), 'max:10240'],
|
||||
@@ -399,7 +436,7 @@ class PortalServicesController extends Controller
|
||||
|
||||
ScanDocumentForMalware::dispatch($id)->afterCommit();
|
||||
|
||||
return to_route('services.manage')->with('success', 'Documento recebido em armazenamento privado e quarentena. Não aparece na biblioteca pública até validação e publicação explícitas.');
|
||||
return to_route('services.manage', array_filter(['intent' => in_array($request->input('intent'), ['document', 'legislation'], true) ? $request->input('intent') : null]))->with('uploaded_document_id', $id)->with('success', 'Documento recebido em armazenamento privado e quarentena. Não aparece na biblioteca pública até validação e publicação explícitas.');
|
||||
}
|
||||
|
||||
public function resource(Request $request): RedirectResponse
|
||||
@@ -663,7 +700,7 @@ class PortalServicesController extends Controller
|
||||
'data_classification' => ['nullable', 'in:internal,public'],
|
||||
'source_reference' => ['required', 'string', 'min:10', 'max:2000'],
|
||||
'legislation' => ['required', 'boolean'], 'essential' => ['required', 'boolean'],
|
||||
'associations' => ['nullable', 'array', 'max:40'], 'associations.*.target_type' => ['required', 'in:theme,module,activity,stage,procedure,entity,territory,document,investment_type,legal_source'.($this->requirementEvidenceEnabled() ? ',requirement' : '')],
|
||||
'associations' => ['nullable', 'array', 'max:40'], 'associations.*.target_type' => ['required', 'in:theme,module,activity,stage,procedure,entity,territory,document,investment_type,legal_source,sector,opportunity'.($this->requirementEvidenceEnabled() ? ',requirement' : '')],
|
||||
'associations.*.target_key' => ['required', 'string', 'max:180'],
|
||||
'associations.*.review_status' => ['nullable', 'in:pending,confirmed'],
|
||||
'associations.*.relationship' => ['required', 'in:applies_to,supports,version_of,translation_of,amends,repeals,implements,cites,related_to'],
|
||||
@@ -829,4 +866,17 @@ SQL, [$id]);
|
||||
|
||||
return $ids ?: [$id];
|
||||
}
|
||||
private function articleUrl(int $id): ?string
|
||||
{
|
||||
if (!Schema::hasColumn('portal_resources', 'editorial_content')) return null;
|
||||
$resource = DB::table('portal_resources')->find($id);
|
||||
if (!$resource) return null;
|
||||
if ($resource->editorial_content !== null) return '/artigos/'.$id;
|
||||
try {
|
||||
return app(\App\Services\EditorialReading::class)->original($resource) ? '/artigos/'.$resource->id : null;
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Procurement;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ProcurementController extends Controller
|
||||
{
|
||||
private const STATE_SQL = "CASE WHEN v.payload->>'procedural_state' IN ('cancelled','results') THEN v.payload->>'procedural_state' WHEN ((v.payload->>'deadline')::timestamp AT TIME ZONE (v.payload->>'timezone'))<=CURRENT_TIMESTAMP THEN 'deadline_ended' WHEN v.payload->>'extended'='true' THEN 'extended' ELSE 'open' END";
|
||||
|
||||
private function query(Procurement $service)
|
||||
{
|
||||
abort_unless($service->available(), 404);
|
||||
|
||||
return DB::table('portal_tenders as t')->join('portal_tender_versions as v', fn ($q) => $q->on('v.id', '=', 't.published_version_id')->on('v.tender_id', '=', 't.id'))->whereNotNull('v.published_at');
|
||||
}
|
||||
|
||||
public function index(Request $request, Procurement $service)
|
||||
{
|
||||
$filters = $request->validate(['q' => 'nullable|string|max:150', 'entity' => 'nullable|uuid', 'sector' => 'nullable|integer', 'territory' => 'nullable|uuid', 'modality' => 'nullable|string|max:200', 'financier' => 'nullable|string|max:200', 'state' => 'nullable|in:open,extended,deadline_ended,cancelled,results', 'deadline_before' => 'nullable|date_format:Y-m-d', 'regime' => 'nullable|in:state,external,private,other']);
|
||||
$filters = array_filter($filters, fn ($value) => $value !== null && $value !== '');
|
||||
$query = $this->query($service);
|
||||
if (! empty($filters['q'])) {
|
||||
$query->where(fn ($q) => $q->whereRaw("v.payload->>'title' ILIKE ?", ['%'.$filters['q'].'%'])->orWhereRaw("v.payload->>'object' ILIKE ?", ['%'.$filters['q'].'%'])->orWhereRaw("v.payload->>'reference' ILIKE ?", ['%'.$filters['q'].'%']));
|
||||
}
|
||||
foreach (['entity' => 'organization_id', 'sector' => 'sector_id', 'modality' => 'modality', 'financier' => 'financier', 'regime' => 'regime'] as $filter => $field) {
|
||||
if (! empty($filters[$filter])) {
|
||||
$query->whereRaw('v.payload->>? = ?', [$field, (string) $filters[$filter]]);
|
||||
}
|
||||
}
|
||||
if (! empty($filters['territory'])) {
|
||||
$query->whereRaw("v.payload->>'territory_id' IN (WITH RECURSIVE scope(id,depth) AS (SELECT id,0 FROM geo_locations WHERE id=? AND deleted_at IS NULL UNION ALL SELECT c.id,s.depth+1 FROM geo_locations c JOIN scope s ON c.parent_id=s.id WHERE c.deleted_at IS NULL AND s.depth<4) SELECT id::text FROM scope)", [$filters['territory']]);
|
||||
}
|
||||
if (! empty($filters['state'])) {
|
||||
$query->whereRaw(self::STATE_SQL.' = ?', [$filters['state']]);
|
||||
}
|
||||
if (! empty($filters['deadline_before'])) {
|
||||
$query->whereRaw("left(v.payload->>'deadline',10)<=?", [$filters['deadline_before']]);
|
||||
}
|
||||
$rows = $query->select('t.id', 'v.id as vid')->orderByDesc('v.published_at')->paginate(20)->withQueryString();
|
||||
$rows->through(fn ($r) => ['id' => $r->id, ...$this->summary($service, $r->vid)]);
|
||||
$facets = $this->query($service)->get(['v.payload'])->map(fn ($r) => json_decode($r->payload, true));
|
||||
$options = [];
|
||||
foreach (['entity' => ['organization_id', 'organization_name'], 'sector' => ['sector_id', 'sector_name'], 'territory' => ['territory_id', 'territory_name'], 'modality' => ['modality', 'modality'], 'financier' => ['financier', 'financier']] as $name => [$value,$label]) {
|
||||
$options[$name] = $facets->filter(fn ($p) => ! empty($p[$value]))->map(fn ($p) => ['id' => (string) $p[$value], 'name' => $p[$label]])->unique('id')->values();
|
||||
}
|
||||
|
||||
return Inertia::render('procurement/index', ['items' => $rows, 'filters' => $filters, 'options' => $options, 'regimes' => config('procurement.regimes'), 'management' => $service->access($request->user())])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
private function summary(Procurement $service, string $vid): array
|
||||
{
|
||||
$v = $service->publishedVersion($vid);
|
||||
|
||||
return ['publication' => $v, 'state' => $service->state($v)];
|
||||
}
|
||||
|
||||
public function show(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
return $this->detail($request, $id, $service, false);
|
||||
}
|
||||
|
||||
public function print(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
return $this->detail($request, $id, $service, true);
|
||||
}
|
||||
|
||||
private function detail(Request $request, string $id, Procurement $service, bool $print)
|
||||
{
|
||||
$row = $this->query($service)->where('t.id', $id)->firstOrFail(['t.id', 'v.id as vid']);
|
||||
$filters = $request->only(['q', 'entity', 'sector', 'territory', 'modality', 'financier', 'state', 'deadline_before', 'regime', 'page']);
|
||||
|
||||
return Inertia::render('procurement/show', ['id' => $id, ...$this->summary($service, $row->vid), 'printMode' => $print, 'backUrl' => '/concursos'.($filters ? '?'.http_build_query($filters) : ''),
|
||||
'history' => DB::table('portal_tender_versions')->where('tender_id', $id)->whereNotNull('published_at')->orderBy('version')->pluck('id')->map(fn ($vid) => $service->publishedVersion($vid)),
|
||||
'watching' => $request->user() && DB::table('portal_tender_watches')->where('tender_id', $id)->where('user_id', $request->user()->id)->exists()])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function manage(Request $request, Procurement $service)
|
||||
{
|
||||
$service->guard($request->user());
|
||||
|
||||
return Inertia::render('procurement/manage', ['items' => DB::table('portal_tenders')->orderByDesc('updated_at')->get(['id', 'draft', 'version', 'published_version_id'])->map(fn ($r) => ['id' => $r->id, 'title' => json_decode($r->draft, true)['title'] ?? 'Rascunho sem título', 'version' => $r->version, 'published' => (bool) $r->published_version_id])])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function edit(Request $request, Procurement $service, ?string $id = null)
|
||||
{
|
||||
$service->guard($request->user());
|
||||
$r = $id ? $service->record($id) : null;
|
||||
|
||||
return Inertia::render('procurement/edit', ['record' => $r ? ['id' => $id, 'version' => $r->version, 'draft' => json_decode($r->draft, true), 'published' => (bool) $r->published_version_id] : null,
|
||||
'references' => $service->references($request->user()), 'history' => $id ? DB::table('portal_tender_versions')->where('tender_id', $id)->orderByDesc('version')->get(['version', 'act', 'reason', 'actor_id', 'published_at', 'created_at']) : []])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function save(Request $request, Procurement $service, ?string $id = null)
|
||||
{
|
||||
$id = $service->save($request->user(), $id, $request->all());
|
||||
|
||||
return redirect('/management/concursos/'.$id)->with('success', 'Rascunho guardado. A publicação anterior permanece intacta.');
|
||||
}
|
||||
|
||||
public function preview(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
return response()->json($service->preview($request->user(), $id, (string) $request->input('act')))->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function publish(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
$service->publish($request->user(), $id, $request->all());
|
||||
|
||||
return redirect('/management/concursos/'.$id)->with('success', 'Acto publicado, com versão e auditoria. Nenhuma proposta recebida pelo portal.');
|
||||
}
|
||||
|
||||
public function watch(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
$row = $this->query($service)->where('t.id', $id)->firstOrFail(['v.id']);
|
||||
DB::table('portal_tender_watches')->updateOrInsert(['tender_id' => $id, 'user_id' => $request->user()->id], ['seen_version_id' => $row->id, 'created_at' => now(), 'updated_at' => now()]);
|
||||
|
||||
return back()->with('success', 'Concurso guardado para acompanhamento. Isto não apresenta uma proposta.');
|
||||
}
|
||||
|
||||
public function unwatch(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
abort_unless($service->available(), 404);
|
||||
DB::table('portal_tender_watches')->where('tender_id', $id)->where('user_id', $request->user()->id)->delete();
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function watched(Request $request, Procurement $service)
|
||||
{
|
||||
$rows = $this->query($service)->join('portal_tender_watches as w', 'w.tender_id', '=', 't.id')->where('w.user_id', $request->user()->id)->get(['t.id', 'v.id as vid', 'w.seen_version_id'])->map(fn ($r) => ['id' => $r->id, 'changed' => $r->seen_version_id !== $r->vid, ...$this->summary($service, $r->vid)]);
|
||||
|
||||
return Inertia::render('procurement/manage', ['watched' => true, 'items' => $rows])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,13 @@ class PublicGeographyController extends Controller
|
||||
{
|
||||
public function options(Request $request, InvestmentGeography $geography): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['type' => ['required', 'in:province,district,admin_post,locality'], 'id' => ['nullable', 'uuid'], 'parent_id' => ['nullable', 'uuid'], 'q' => ['nullable', 'string', 'max:100'], 'page' => ['nullable', 'integer', 'min:1']]);
|
||||
$data = $request->validate(['corridor' => ['nullable', \Illuminate\Validation\Rule::in(array_keys(config('investment_corridors')))], 'type' => ['required', 'in:province,district,admin_post,locality'], 'id' => ['nullable', 'uuid'], 'parent_id' => ['nullable', 'uuid'], 'q' => ['nullable', 'string', 'max:100'], 'page' => ['nullable', 'integer', 'min:1']]);
|
||||
$query = $geography->publicReferences()->where('location_type', $data['type']);
|
||||
if (!empty($data['corridor'])) {
|
||||
$corridors = app(\App\Services\InvestmentCorridorContext::class);
|
||||
if ($data['type'] === 'province') $query->whereIn('id', array_column($corridors->provinces($data['corridor']), 'id'));
|
||||
elseif (!$corridors->compatible($data['corridor'], $data['parent_id'] ?? null)) abort(422, 'O território não pertence ao corredor escolhido.');
|
||||
}
|
||||
if (! empty($data['id'])) {
|
||||
$query->where('id', $data['id']);
|
||||
}
|
||||
@@ -29,6 +34,6 @@ class PublicGeographyController extends Controller
|
||||
->orWhereRaw('source_code ILIKE ?', [$term]));
|
||||
}
|
||||
|
||||
return response()->json($query->orderBy('name')->orderBy('id')->paginate(100, ['id', 'name', 'parent_id', 'location_type', 'source_code']));
|
||||
return response()->json($query->orderBy('name')->orderBy('id')->paginate(100, ['id', 'name', 'parent_id', 'location_type', 'source_code'])->withQueryString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ class VirtualDeskController extends Controller
|
||||
|
||||
public function createRequest(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['service_version_id' => 'required|uuid', 'client_key' => 'required|uuid']);
|
||||
$row = $desk->createRequest($request->user(), $id, $data['service_version_id'], $data['client_key']);
|
||||
$data = $request->validate(['service_version_id' => 'required|uuid', 'client_key' => 'required|uuid', 'opportunity' => 'nullable|string|max:100']);
|
||||
$row = $desk->createRequest($request->user(), $id, $data['service_version_id'], $data['client_key'], $data['opportunity'] ?? null);
|
||||
|
||||
return $request->expectsJson() ? response()->json(['id' => $row->id, 'version' => $row->version], 201) : redirect('/balcao-virtual/pedidos/'.$row->id);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class EnsureScopedEditorial
|
||||
return $next($request);
|
||||
}
|
||||
abort_unless($access->editorial($actor), 403);
|
||||
$allowed = in_array($request->route()->getName(), ['editorial.edit', 'editorial.save', 'services.resource', 'services.resource.update'], true)
|
||||
$allowed = in_array($request->route()->getName(), ['editorial.edit', 'editorial.save', 'editorial.preview', 'editorial.preview-content', 'services.resource', 'services.resource.update'], true)
|
||||
|| ($request->route()->getName() === 'services.moderate' && $request->route('type') === 'resources');
|
||||
abort_unless($allowed, 403);
|
||||
if ($request->route('id') !== null) {
|
||||
|
||||
@@ -14,12 +14,14 @@ final class EnsureScopedPortalAdministration
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$actor = $request->user();
|
||||
abort_unless($actor, 403);
|
||||
abort_unless($actor !== null, 403);
|
||||
$scope = app(ScopedPortalAdministration::class);
|
||||
$path = $request->path();
|
||||
$interests = $request->is('management/interests', 'management/interests/*', 'api/v1/investment/admin/interests/*');
|
||||
// Fiscal maintenance reuses authorised administration; unrelated gates remain unchanged.
|
||||
$catalogue = $request->is('management/economic-references/lot-01', 'management/economic-references/lot-01/*',
|
||||
$geometryRead = $request->isMethod('GET') && $path === 'management/territory-geometries';
|
||||
$catalogue = $geometryRead || $request->is('management/territory-geometry-import', 'management/territory-geometry-import/*',
|
||||
'management/economic-references/lot-01', 'management/economic-references/lot-01/*',
|
||||
'management/education', 'management/education/*', 'management/investment/references', 'management/investment/references/*', 'api/v1/investment/admin/references', 'api/v1/investment/admin/references/*',
|
||||
'management/investment/zones', 'management/investment/zones/*', 'api/v2/investment/admin/zones', 'api/v2/investment/admin/zones/*',
|
||||
'management/balcao/atribuicoes', 'management/balcao/atribuicoes/*', 'management/balcao/servicos',
|
||||
|
||||
@@ -53,7 +53,7 @@ final class EnsureSimulationRecoveryCookie
|
||||
}
|
||||
}
|
||||
|
||||
return $response->withCookie(cookie(
|
||||
$response->headers->setCookie(cookie(
|
||||
self::NAME,
|
||||
$forget ? '' : $token,
|
||||
$minutes,
|
||||
@@ -64,6 +64,7 @@ final class EnsureSimulationRecoveryCookie
|
||||
false,
|
||||
'lax',
|
||||
));
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function isExplicitDraftActivity(Request $request, Response $response): bool
|
||||
|
||||
@@ -51,8 +51,10 @@ class HandleInertiaRequests extends Middleware
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'procurementEnabled' => app(\App\Services\Procurement::class)->available(),
|
||||
'homologationReserved' => (bool) config('portal.homologation_only', false),
|
||||
'profileAccess' => app(PortalProfileAccess::class)->navigation($request->user()),
|
||||
'contentManagementHref' => app(\App\Services\ContentManagement::class)->access($request->user()) !== [] ? '/gestao-conteudos' : null,
|
||||
'portalOperator' => config('portal.operator_name'),
|
||||
'portalInstitutionalContactEmail' => filter_var(config('portal.institutional_contact_email'), FILTER_VALIDATE_EMAIL) ?: null,
|
||||
'legacyContentEnabled' => app(LegacyContent::class)->available(),
|
||||
@@ -68,7 +70,7 @@ class HandleInertiaRequests extends Middleware
|
||||
'create' => ['label' => 'Nova oportunidade', 'href' => '/management/opportunities/create'],
|
||||
'installationHref' => '/management/opportunity-management-install',
|
||||
] : null,
|
||||
'flash' => ['success' => fn () => $request->session()->get('success'), 'warning' => fn () => $request->session()->get('warning')],
|
||||
'flash' => ['success' => fn () => $request->session()->get('success'), 'warning' => fn () => $request->session()->get('warning'), 'uploaded_document_id' => fn () => $request->session()->get('uploaded_document_id')],
|
||||
'scopedAdministration' => $request->user() && app(\App\Services\ScopedPortalAdministration::class)->administrative($request->user()) ? [
|
||||
['label'=>'Zonas e parques','href'=>'/management/investment/zones'],
|
||||
['label'=>'Referências económicas','href'=>'/management/economic-references/lot-01'],
|
||||
|
||||
@@ -29,7 +29,9 @@ class ProtectProfileOpportunity
|
||||
if ($record && ($record->owner_user_id !== null || str_starts_with($request->path(), 'minhas-oportunidades'))) {
|
||||
abort_if($record->owner_user_id === null && str_starts_with($request->path(), 'minhas-oportunidades'), 403, 'Registo legado não convertido para a política v1.');
|
||||
$access = app(PortalProfileAccess::class);
|
||||
$access->authorize($request->user(), $request->isMethod('GET') || str_ends_with($request->path(), '/workflow') || str_ends_with($request->path(), '/preview') || $request->is('minhas-oportunidades/*/classification/review') ? 'view' : 'edit', $record);
|
||||
// The promotional controller checks its own scoped disclosure capability,
|
||||
// like workflow decisions; publication does not grant draft editing.
|
||||
$access->authorize($request->user(), $request->isMethod('GET') || str_ends_with($request->path(), '/workflow') || str_ends_with($request->path(), '/preview') || $request->is('minhas-oportunidades/*/classification/review', 'minhas-oportunidades/*/divulgacao/previa', 'minhas-oportunidades/*/divulgacao/confirmar') ? 'view' : 'edit', $record);
|
||||
abort_if($request->is('management/opportunity-origin/apply'), 403, 'O cadastro privado v1 não usa a classificação editorial dos exemplos legados.');
|
||||
abort_if($request->isMethod('POST') && str_ends_with($request->path(), '/lifecycle') && ! str_contains($request->path(), '/resources/'), 403, 'Use o workflow com decisões segregadas; o perfil não concede exclusão.');
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ class StoreLandAssistanceCaseRequest extends FormRequest
|
||||
{
|
||||
$payload = $this->validated('payload');
|
||||
|
||||
return is_array($payload) ? $payload : [];
|
||||
if (!is_array($payload)) return [];
|
||||
if (!empty($payload['investor_path_case_id'])) return app(\App\Services\InvestmentConsumerScope::class)->bind($this->user(), $payload['investor_path_case_id'], $payload, $this->routeIs('land-assistance.support'));
|
||||
return \Illuminate\Support\Arr::except($payload, ['context_opportunity', 'context_opportunity_snapshot']);
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
@@ -33,7 +35,9 @@ class StoreLandAssistanceCaseRequest extends FormRequest
|
||||
return [
|
||||
'version' => [$this->isMethod('post') && ! $this->routeIs('land-assistance.support') ? 'nullable' : 'required', 'integer', 'min:1'],
|
||||
'context_revision' => ['nullable', 'integer', 'min:0'], 'context_reviewed' => ['nullable', 'boolean'],
|
||||
'payload' => ['required', 'array:context_reuse,support_kind,request_description,project_reference,investor_path_case_id,applicant_category,full_name,contact_person,email,phone,preferred_contact,country,declared_origin_country_code,nationality,representative_name,representative_role,representation_basis,nuit,constitution_or_residency_note,area_status,parcels,purpose,activity,known_occupants,occupant_description,known_improvements,improvements_description,prior_right_status,prior_right_description,conflict_status,conflict_description,official_process,attachments_to_follow,exploration_plan,consultation,additional_information,truth_declaration,contact_consent'],
|
||||
'payload' => ['required', 'array:context_opportunity,context_opportunity_snapshot,context_reuse,support_kind,request_description,project_reference,investor_path_case_id,applicant_category,full_name,contact_person,email,phone,preferred_contact,country,declared_origin_country_code,nationality,representative_name,representative_role,representation_basis,nuit,constitution_or_residency_note,area_status,parcels,purpose,activity,known_occupants,occupant_description,known_improvements,improvements_description,prior_right_status,prior_right_description,conflict_status,conflict_description,official_process,attachments_to_follow,exploration_plan,consultation,additional_information,truth_declaration,contact_consent'],
|
||||
'payload.context_opportunity' => ['nullable', 'string', 'max:100'],
|
||||
'payload.context_opportunity_snapshot' => ['nullable', 'string', 'max:200000'],
|
||||
'payload.context_reuse' => ['nullable', 'string', 'max:10000'],
|
||||
'payload.support_kind' => ['nullable', 'in:investment,home,family_exploitation,recognition,regularization,delimitation,change_of_use,transfer,existing_process,conflict_guidance,other'],
|
||||
'payload.request_description' => ['nullable', 'string', 'max:4000'],
|
||||
|
||||
@@ -37,7 +37,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
|
||||
'contact_province', 'contact_district', 'preferred_channel',
|
||||
];
|
||||
$economic = [
|
||||
'context_applicability_snapshot', 'investment_regime', 'context_reuse', 'compatible_fields_snapshot', 'published_iva_base', 'published_iva_currency', 'published_iva_date', 'published_iva_scope', 'monthly_iva_settings', 'monthly_iva_snapshot', 'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'cashflow_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
|
||||
'context_opportunity', 'context_applicability_snapshot', 'investment_regime', 'context_reuse', 'compatible_fields_snapshot', 'published_iva_base', 'published_iva_currency', 'published_iva_date', 'published_iva_scope', 'monthly_iva_settings', 'monthly_iva_snapshot', 'modelo_a_snapshot', 'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'cashflow_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
|
||||
'competitor_assumption', 'sales_channel_assumption', 'marketing_approach', 'location_rationale', 'project_risk_responses_snapshot', 'project_questionnaire_version',
|
||||
'customer_validation_status', 'cost_estimate_status', 'location_assessment_status', 'funding_status', 'receivables_timing_status',
|
||||
'project_type_other', 'sector_id', 'activity_id', 'products_services', 'maturity_stage', 'expected_start_date',
|
||||
@@ -72,7 +72,8 @@ class StoreSimulationFormDraftRequest extends FormRequest
|
||||
'economic_snapshot.business_plan_snapshot' => ['nullable', 'string', 'max:60000'],
|
||||
'economic_snapshot.financial_tools_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.cashflow_snapshot' => ['nullable', 'string', 'max:20000'],
|
||||
'economic_snapshot.context_applicability_snapshot' => ['nullable', 'string', 'max:20000'],
|
||||
'economic_snapshot.context_applicability_snapshot' => ['nullable', 'string', 'max:1000000'],
|
||||
'economic_snapshot.context_opportunity' => ['nullable', 'string', 'max:100'],
|
||||
'economic_snapshot.investment_regime' => ['nullable', 'in:general,industrial_park,zee,zfi'],
|
||||
'economic_snapshot.context_reuse' => ['nullable', 'string', 'max:10000'],
|
||||
'economic_snapshot.compatible_fields_snapshot' => ['nullable', 'string', 'max:10000'],
|
||||
@@ -82,6 +83,9 @@ class StoreSimulationFormDraftRequest extends FormRequest
|
||||
'economic_snapshot.published_iva_scope' => ['nullable', 'string', 'in:yes,no'],
|
||||
'economic_snapshot.monthly_iva_settings' => ['nullable', 'string', 'max:2000'],
|
||||
'economic_snapshot.monthly_iva_snapshot' => ['nullable', 'string', 'max:60000'],
|
||||
// Preserve accepted books and explicit history through the L3 refund increment.
|
||||
// This is a bounded transport choice, not a business limit on fiscal records.
|
||||
'economic_snapshot.modelo_a_snapshot' => ['nullable', 'string', 'json', 'max:6000000'],
|
||||
'economic_snapshot.project_team_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_execution_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_risk_responses_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
|
||||
@@ -31,6 +31,13 @@ class Opportunity extends Model
|
||||
|
||||
protected $appends = ['maturity_catalogue_version'];
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$data = parent::toArray();
|
||||
if (isset($data['description'])) $data['description'] = \App\Services\OpportunityPublicText::description($this, $data['description']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getMaturityCatalogueVersionAttribute(): ?string
|
||||
{
|
||||
// Only published metadata is public; never expose the current private draft.
|
||||
@@ -68,7 +75,8 @@ class Opportunity extends Model
|
||||
$scope = $public ? 'public' : ($visible && $this->owner_user_id !== null ? 'restricted' : 'unpublished');
|
||||
|
||||
return ['scope' => $scope, 'public_catalog' => $public, 'label' => match ($scope) {
|
||||
'public' => 'Publicada no catálogo público',
|
||||
'public' => data_get($this->workflow_context, 'publication.purpose') === 'promotional_preparation'
|
||||
? 'Resumo divulgado — Em preparação; não autoriza execução' : 'Publicada no catálogo público',
|
||||
'restricted' => 'Disponibilizada com acesso restrito — perfis e organização autorizados',
|
||||
default => 'Sem versão disponibilizada; aprovação interna é uma etapa distinta',
|
||||
}];
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/** Public read projection authorized by Tempo; source rows and originals stay private. */
|
||||
class ApprovedAdministrativeBoundaries
|
||||
{
|
||||
public const HASH = '06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc';
|
||||
|
||||
/** @var array<string,array<string,string>>|null */
|
||||
private ?array $items = null;
|
||||
|
||||
/** @return array<string,array<string,string>> */
|
||||
public function snapshots(): array
|
||||
{
|
||||
if ($this->items !== null) {
|
||||
return $this->items;
|
||||
}
|
||||
$contents = file_get_contents(base_path('docs/investmoz/unification/approved-administrative-boundaries-20261024.json'));
|
||||
if ($contents === false || ! hash_equals(self::HASH, hash('sha256', str_replace("\r\n", "\n", $contents)))) {
|
||||
throw new RuntimeException('Approved administrative boundary manifest changed.');
|
||||
}
|
||||
$manifest = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (count($manifest['items']) !== 408) {
|
||||
throw new RuntimeException('Exact 408-boundary scope required.');
|
||||
}
|
||||
$this->items = array_column($manifest['items'], null, 'id');
|
||||
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function allows(string $id, string $territoryId, string $geometryHash, string $propertiesHash): bool
|
||||
{
|
||||
$item = $this->snapshots()[$id] ?? null;
|
||||
|
||||
return $item !== null && $item['territory_id'] === $territoryId
|
||||
&& hash_equals($item['geometry_sha256'], $geometryHash)
|
||||
&& hash_equals($item['properties_sha256'], $propertiesHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
/** Exact replay of the 28 accepted links into the transported project identities. */
|
||||
final class ApprovedAnchorTerritories
|
||||
{
|
||||
public const HASH = 'd2893d0a638d630435aca1890f79a8094dc58eb65bbdd65a470bb685fa075190';
|
||||
|
||||
/** @return array{manifest_sha256:string,projects:list<array<string,mixed>>,links:list<array<string,mixed>>} */
|
||||
public function plan(): array
|
||||
{
|
||||
$file = base_path('docs/investmoz/unification/approved-anchor-territories-20261023.json');
|
||||
$contents = is_file($file) ? file_get_contents($file) : false;
|
||||
if ($contents !== false) {
|
||||
$contents = str_replace("\r\n", "\n", $contents);
|
||||
}
|
||||
if ($contents === false || ! hash_equals(self::HASH, hash('sha256', $contents))) {
|
||||
throw new RuntimeException('Reviewed territorial manifest changed.');
|
||||
}
|
||||
$manifest = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (count($manifest['items']) !== 28) {
|
||||
throw new RuntimeException('Exact 28-link scope required.');
|
||||
}
|
||||
$projects = [];
|
||||
$links = [];
|
||||
foreach ($manifest['items'] as $item) {
|
||||
$project = DB::table('projects')->where('id', $item['project_id'])->whereNull('deleted_at')->first();
|
||||
$dossier = DB::table('investment_dossiers')->where('id', $item['dossier_id'])->whereNull('deleted_at')->first();
|
||||
if (! $project || $project->code !== $item['code'] || ! $dossier || $dossier->project_id !== $project->id
|
||||
|| ! hash_equals($item['content_sha256'], hash('sha256', $dossier->content))
|
||||
|| ! hash_equals($item['source_sha256'], hash('sha256', $dossier->source))) {
|
||||
throw new RuntimeException('Project identity or documentary source changed: '.$item['code']);
|
||||
}
|
||||
$original = $item['original_relation'];
|
||||
$geo = DB::table('geo_locations')->where('id', $original['geo_location_id'])->whereNull('deleted_at')->first();
|
||||
foreach (['location_type', 'source_code', 'parent_id', 'data_classification'] as $field) {
|
||||
if (! $geo || $item['territory'][$field] !== $geo->$field) {
|
||||
throw new RuntimeException('Territory identity changed: '.$original['geo_location_id']);
|
||||
}
|
||||
}
|
||||
$metadata = $projects[$project->id]['after_metadata'] ?? json_decode($project->metadata, true, 512, JSON_THROW_ON_ERROR);
|
||||
$key = $geo->id.':'.$original['role'];
|
||||
$evidence = $item['evidence'];
|
||||
// Preserve the original dossier id in evidence and explicitly map the destination.
|
||||
$evidence['transported_dossier_id'] = $dossier->id;
|
||||
if (isset($metadata['documented_location_sources'][$key]) && $metadata['documented_location_sources'][$key] != $evidence) {
|
||||
throw new RuntimeException('Existing territorial evidence differs; no overwrite: '.$item['code']);
|
||||
}
|
||||
$projects[$project->id] ??= ['id' => $project->id, 'code' => $project->code, 'before_version' => (int) $project->version_no, 'before_metadata' => json_decode($project->metadata, true, 512, JSON_THROW_ON_ERROR)];
|
||||
$metadata['documented_location_sources'][$key] = $evidence;
|
||||
$projects[$project->id]['after_metadata'] = $metadata;
|
||||
$existing = DB::table('project_locations')->where('project_id', $project->id)->where('geo_location_id', $geo->id)->where('role', $original['role'])->first();
|
||||
if ($existing && ($existing->deleted_at || $existing->data_classification !== 'public')) {
|
||||
throw new RuntimeException('Archived or restricted territorial relation must not be revived.');
|
||||
}
|
||||
$identity = DB::table('project_locations')->where('id', $original['id'])->first();
|
||||
if ($identity && (! $existing || $identity->id !== $existing->id)) {
|
||||
throw new RuntimeException('Historical relation identity is already occupied.');
|
||||
}
|
||||
$links[] = ['code' => $item['code'], 'already_present' => (bool) $existing, 'row' => [...$original, 'project_id' => $project->id]];
|
||||
}
|
||||
|
||||
return ['manifest_sha256' => self::HASH, 'projects' => array_values($projects), 'links' => $links];
|
||||
}
|
||||
|
||||
/** Caller supplies an authenticated, backed-up transaction; no source publication changes.
|
||||
* @param array{manifest_sha256:string,projects:list<array<string,mixed>>,links:list<array<string,mixed>>} $plan
|
||||
* @return array<string,int>
|
||||
*/
|
||||
public function apply(array $plan): array
|
||||
{
|
||||
if (DB::transactionLevel() < 1 || $this->plan() !== $plan) {
|
||||
throw new RuntimeException('Reviewed transaction and unchanged preview required.');
|
||||
}
|
||||
$inserted = 0;
|
||||
$updated = 0;
|
||||
foreach ($plan['projects'] as $project) {
|
||||
if ($project['before_metadata'] == $project['after_metadata']) {
|
||||
continue;
|
||||
}
|
||||
$count = DB::table('projects')->where('id', $project['id'])->where('version_no', $project['before_version'])->update([
|
||||
'metadata' => json_encode($project['after_metadata'], JSON_THROW_ON_ERROR), 'version_no' => $project['before_version'] + 1, 'updated_at' => now(),
|
||||
]);
|
||||
if ($count !== 1) {
|
||||
throw new RuntimeException('Concurrent project change.');
|
||||
}
|
||||
DB::table('audit_events')->insert(['source_system' => 'approved-anchor-territories-recovery-v1', 'external_id' => self::HASH.':'.$project['id'], 'entity_type' => 'projects', 'entity_id' => $project['id'], 'action' => 'accepted_territories_restored',
|
||||
'data_classification' => 'public', 'before_data' => json_encode(['version_no' => $project['before_version'], 'metadata' => $project['before_metadata']], JSON_THROW_ON_ERROR),
|
||||
'after_data' => json_encode(['version_no' => $project['before_version'] + 1, 'manifest_sha256' => self::HASH, 'metadata' => $project['after_metadata'], 'new_approval' => false], JSON_THROW_ON_ERROR)]);
|
||||
$updated++;
|
||||
}
|
||||
foreach ($plan['links'] as $link) {
|
||||
if ($link['already_present']) {
|
||||
continue;
|
||||
}
|
||||
DB::table('project_locations')->insert($link['row']);
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
return ['relations_restored' => $inserted, 'projects_updated' => $updated, 'new_dossiers' => 0, 'publication_changes' => 0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** Navigation over existing authorisations; never grants a role or publishes content. */
|
||||
final class ContentManagement
|
||||
{
|
||||
public function access(?User $actor): array
|
||||
{
|
||||
if (! $actor || ! $actor->hasVerifiedEmail()) return [];
|
||||
$profiles = app(PortalProfileAccess::class);
|
||||
$navigation = $profiles->navigation($actor);
|
||||
$legacy = $profiles->legacyWriteAllowed($actor);
|
||||
$documents = app(DocumentManagementAccess::class);
|
||||
$editor = $documents->full($actor) || $documents->editorial($actor);
|
||||
$upload = $editor || $documents->limited($actor);
|
||||
$result = [];
|
||||
if (app(Procurement::class)->available() && app(Procurement::class)->access($actor)) $result['procurement'] = ['index' => '/management/concursos', 'create' => '/management/concursos/novo'];
|
||||
if ($legacy || ($navigation['mine'] ?? null) || ($navigation['review'] ?? null)) {
|
||||
$result['opportunities'] = ['index' => $legacy ? '/management/opportunities' : '/minhas-oportunidades',
|
||||
'create' => $legacy ? '/management/opportunities/create' : ($navigation['create'] ?? null), 'profileWorkflow' => ! $legacy];
|
||||
}
|
||||
if ($upload) $result['documents'] = ['index' => '/management/portal-services', 'edit' => $editor];
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function view(User $actor): array
|
||||
{
|
||||
$access = $this->access($actor);
|
||||
abort_unless($access !== [], 403);
|
||||
$counts = [];
|
||||
if (isset($access['opportunities'])) {
|
||||
$counts['opportunities'] = app(PortalProfileAccess::class)->scope(Opportunity::query(), $actor)
|
||||
->selectRaw("COALESCE(workflow_context->>'working_state',status) as state, count(*) as total")
|
||||
->groupByRaw("COALESCE(workflow_context->>'working_state',status)")->pluck('total', 'state');
|
||||
}
|
||||
if (isset($access['documents'])) {
|
||||
$documents = app(DocumentManagementAccess::class);
|
||||
$query = $documents->full($actor) ? DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at')
|
||||
: ($access['documents']['edit'] ? $documents->editorialResources($actor) : null);
|
||||
$counts['documents'] = $query ? $query->select('r.published', DB::raw('count(*) as total'))->groupBy('r.published')->get()
|
||||
->mapWithKeys(fn ($row) => [$row->published ? 'published' : 'unpublished' => (int) $row->total]) : [];
|
||||
}
|
||||
return ['access' => $access, 'counts' => $counts];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
/** Supplement the six named agribusiness sections without changing corridor masters or source documents. */
|
||||
final class CorridorAgribusiness
|
||||
{
|
||||
public function enrich(string $html, array $links = []): string
|
||||
{
|
||||
$data = json_decode(file_get_contents(__DIR__.'/../../database/data/corridor-investment-opportunities.json'), true, 512, JSON_THROW_ON_ERROR);
|
||||
$doc = new DOMDocument;
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
try {
|
||||
$doc->loadHTML('<?xml encoding="UTF-8"><html><body>'.$html.'</body></html>', LIBXML_NONET);
|
||||
$body = $doc->getElementsByTagName('body')->item(0);
|
||||
if (! $body) {
|
||||
return $html;
|
||||
}
|
||||
$changed = false;
|
||||
foreach ($data['groups'] as $index => $group) {
|
||||
$marker = 'agronegocio-oportunidades-'.($index + 1);
|
||||
if ($doc->getElementById($marker)) {
|
||||
continue;
|
||||
}
|
||||
foreach (iterator_to_array($body->childNodes) as $heading) {
|
||||
if (! $heading instanceof DOMElement) {
|
||||
continue;
|
||||
}
|
||||
$name = preg_replace('/^[a-f]\)\s*/u', '', trim($heading->textContent));
|
||||
if ($name !== $group['name']) {
|
||||
continue;
|
||||
}
|
||||
$chains = $heading->nextSibling;
|
||||
while ($chains && ! $chains instanceof DOMElement) {
|
||||
$chains = $chains->nextSibling;
|
||||
}
|
||||
// Insert only into the exact named section and its existing priority-chain paragraph.
|
||||
if (! $chains || ! str_starts_with(trim($chains->textContent), 'Cadeias de valor prioritárias:')) {
|
||||
continue;
|
||||
}
|
||||
$fragment = $doc->createDocumentFragment();
|
||||
$label = $doc->createElement('p');
|
||||
$strong = $doc->createElement('strong');
|
||||
$strong->appendChild($doc->createTextNode('Oportunidades de investimento em agronegócio:'));
|
||||
$label->appendChild($strong);
|
||||
$fragment->appendChild($label);
|
||||
$list = $doc->createElement('ul');
|
||||
$list->setAttribute('id', $marker);
|
||||
foreach ($group['opportunities'] as $itemIndex => $opportunity) {
|
||||
$item = $doc->createElement('li');
|
||||
$chain = $doc->createElement('strong');
|
||||
$chain->appendChild($doc->createTextNode($opportunity['chain'].': '));
|
||||
$item->appendChild($chain);
|
||||
$item->appendChild($doc->createTextNode($opportunity['description']));
|
||||
if (isset($links[$index][$itemIndex])) {
|
||||
$item->appendChild($doc->createTextNode(' '));
|
||||
$link = $doc->createElement('a');
|
||||
$link->setAttribute('href', $links[$index][$itemIndex]);
|
||||
$link->appendChild($doc->createTextNode('Conhecer a oportunidade e iniciar o roteiro'));
|
||||
$item->appendChild($link);
|
||||
}
|
||||
$list->appendChild($item);
|
||||
}
|
||||
$fragment->appendChild($list);
|
||||
$source = $doc->createElement('p');
|
||||
$source->appendChild($doc->createTextNode('Fonte das oportunidades: '.$data['source']));
|
||||
$fragment->appendChild($source);
|
||||
$body->insertBefore($fragment, $chains->nextSibling);
|
||||
$changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $changed ? implode('', array_map(fn ($node) => $doc->saveHTML($node), iterator_to_array($body->childNodes))) : $html;
|
||||
} finally {
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
/** Source-defined opportunities; never infer a master corridor, site or promoter. */
|
||||
final class CorridorAgribusinessOpportunities
|
||||
{
|
||||
public const SOURCE_HASH = '1473b06c48aa234e771660e3f2a51b9d2e4e1943bd52d878e0ae854ee6657055';
|
||||
|
||||
/** Documentary cross-reference only; keep the two source designations and master records intact. */
|
||||
public function portfolioOpportunityIds(\App\Models\InvestmentDossier $dossier): array
|
||||
{
|
||||
if (!$dossier->corridor_id || data_get($dossier->content, 'code') !== 'COR-02') return [];
|
||||
// COR-02 is Beira e Centro in investment-portfolio.json. The seven entries are explicitly
|
||||
// grouped under Corredor da Beira in the accepted source, not selected by province/name search.
|
||||
$keys = collect($this->entries())->where('corridor', 'Corredor da Beira')->pluck('key');
|
||||
return Opportunity::published()->whereIn('management_creation_key', $keys)
|
||||
->where('workflow_context->corridor_agribusiness->source_sha256', self::SOURCE_HASH)->pluck('id')->all();
|
||||
}
|
||||
public const LIMITATIONS = 'Promotor, implantação exacta, área, montante, financiamento, viabilidade e licenças não indicados nesta fonte. O corredor é um enquadramento documental; não identifica uma parcela nem confirma disponibilidade.';
|
||||
|
||||
public function entries(): array
|
||||
{
|
||||
$data = json_decode(file_get_contents(database_path('data/corridor-investment-opportunities.json')), true, 512, JSON_THROW_ON_ERROR);
|
||||
if ($data['source_sha256'] !== self::SOURCE_HASH) throw new \RuntimeException('Fonte divergente; reconciliar antes de cadastrar.');
|
||||
$sector = Sector::where('slug', 'agronegocio')->where('is_active', true)->whereNull('deleted_at')->firstOrFail();
|
||||
$entries = [];
|
||||
foreach ($data['groups'] as $g => $group) {
|
||||
foreach ($group['opportunities'] as $i => $item) {
|
||||
$key = (string) Uuid::uuid5(Uuid::NAMESPACE_URL, 'investmoz:agronegocio:'.self::SOURCE_HASH.':'.$g.':'.$i);
|
||||
$title = Str::limit($item['chain'].' — '.$group['name'], 150, '…');
|
||||
$description = $item['description']."\n\nEnquadramento documental: ".$group['name'].".\n\nFonte: ".$data['source'].'; SHA-256 '.self::SOURCE_HASH.".\n\n".self::LIMITATIONS;
|
||||
$entries[] = ['key' => $key, 'group' => $g, 'index' => $i, 'corridor' => $group['name'], 'chain' => $item['chain'],
|
||||
'source' => $data['source'], 'source_sha256' => self::SOURCE_HASH, 'title' => $title,
|
||||
'summary' => $item['description'], 'description' => $description, 'sector_id' => $sector->id];
|
||||
}
|
||||
}
|
||||
if (count($entries) !== 34) throw new \RuntimeException('O lote deve conter as 34 entradas da fonte.');
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function register(User $actor, string $organization): array
|
||||
{
|
||||
// The caller uses the ordinary profile creation route and its approved scope.
|
||||
abort_unless(str_starts_with(request()->path(), 'minhas-oportunidades'), 422);
|
||||
return DB::transaction(function () use ($actor, $organization) {
|
||||
DB::select('SELECT pg_advisory_xact_lock(231026, 34)');
|
||||
$result = [];
|
||||
foreach ($this->entries() as $entry) {
|
||||
$record = Opportunity::withTrashed()->where('management_creation_key', $entry['key'])->first();
|
||||
if ($record) {
|
||||
abort_unless(app(PortalProfileAccess::class)->canView($actor, $record), 403);
|
||||
abort_unless(data_get($record->workflow_context, 'corridor_agribusiness.source_sha256') === self::SOURCE_HASH, 409);
|
||||
// Recovery never overwrites editorial changes, withdrawal or archive.
|
||||
$result[] = ['id' => $record->id, 'created' => false, 'key' => $entry['key']];
|
||||
continue;
|
||||
}
|
||||
$record = app(OpportunityManagement::class)->save($actor, ['creation_key' => $entry['key'], 'organization_id' => $organization,
|
||||
'opportunity_category' => 'investment_project', 'sector_id' => $entry['sector_id'],
|
||||
'title' => $entry['title'], 'summary' => $entry['summary'], 'description' => $entry['description'],
|
||||
'canonical' => ['opportunity_category' => 'investment_project', 'official_title' => $entry['title'],
|
||||
'executive_summary' => $entry['summary'], 'demand_statement' => $entry['description'], 'sector_id' => $entry['sector_id']],
|
||||
'reason' => 'Cadastro privado das 34 oportunidades de agronegócio solicitado pela Tempo em 23/10/2026; não constitui publicação nem aprovação de execução.']);
|
||||
$context = $record->workflow_context ?? [];
|
||||
$context['corridor_agribusiness'] = $entry;
|
||||
$context['promotional_summary'] = ['input' => ['title' => $entry['title'], 'summary' => $entry['summary'],
|
||||
'description' => $entry['description'], 'source_reference' => $entry['source'].'; SHA-256 '.self::SOURCE_HASH,
|
||||
'limitations' => self::LIMITATIONS, 'edition' => '', 'source_date' => '', 'territory_ids' => [], 'sector_id' => $entry['sector_id']]];
|
||||
$record->workflow_context = $context;
|
||||
$record->maturity_label = 'Em preparação';
|
||||
$record->save();
|
||||
DB::table('audit_events')->insert(['source_system' => 'corridor-agribusiness-registration', 'entity_type' => 'opportunity',
|
||||
'action' => 'private_source_registered', 'actor_user_id' => $actor->id, 'data_classification' => 'restricted',
|
||||
'after_data' => json_encode(['record_id' => $record->id, 'source' => $entry, 'no_publication' => true, 'no_master_relation_inferred' => true], JSON_THROW_ON_ERROR)]);
|
||||
$result[] = ['id' => $record->id, 'created' => true, 'key' => $entry['key']];
|
||||
}
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
public function articleLinks(): array
|
||||
{
|
||||
$records = Opportunity::published()->whereIn('management_creation_key', array_column($this->entries(), 'key'))->get()->keyBy('management_creation_key');
|
||||
$links = [];
|
||||
foreach ($this->entries() as $entry) {
|
||||
if ($record = $records->get($entry['key'])) $links[$entry['group']][$entry['index']] = '/oportunidades/'.$record->slug;
|
||||
}
|
||||
return $links;
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,9 @@ class DocumentManagementAccess
|
||||
/** Restore the approved editorial operation without a global management grant. */
|
||||
public function editorial(User $actor): bool
|
||||
{
|
||||
return config('portal.homologation_only', false)
|
||||
&& in_array(strtolower(trim($actor->email)), config('homologation_editorial.accounts', []), true)
|
||||
// Tempo's named editorial designation does not depend on whether the
|
||||
// site's technical reviewer reservation is enabled.
|
||||
return in_array(strtolower(trim($actor->email)), config('homologation_editorial.accounts', []), true)
|
||||
&& app(PortalProfileAccess::class)->superuser($actor)
|
||||
&& ! app(PortalProfileAccess::class)->has($actor, 'auditor')
|
||||
&& app(PortalProfileAccess::class)->mfa($actor);
|
||||
@@ -49,6 +50,11 @@ class DocumentManagementAccess
|
||||
->where('d.external_id', $item['external_id']);
|
||||
});
|
||||
}
|
||||
foreach (json_decode(file_get_contents(config_path('legacy_editorial_resources.json')), true, 512, JSON_THROW_ON_ERROR) as $item) {
|
||||
// These exact original bytes contain the legacy identity/source.
|
||||
// Destination upload IDs differ; the approved file scope does not.
|
||||
$query->orWhere('d.sha256', $item['sha256']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use GuzzleHttp\Psr7\UriResolver;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
/** Reads verified originals without writing a derived edition or changing publication. */
|
||||
final class EditorialReading
|
||||
{
|
||||
public function publicResources()
|
||||
{
|
||||
return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
|
||||
->where('r.published', true)->where('r.rights_status', 'authorized')->where('d.rights_status', 'authorized')
|
||||
->where('d.status', 'active')->where('d.data_classification', 'public')->whereNull('d.deleted_at')
|
||||
->where('d.malware_scan_status', 'clean')->whereColumn('d.sha256', 'd.malware_scanned_sha256');
|
||||
}
|
||||
|
||||
public function original(object $resource): ?array
|
||||
{
|
||||
$document = DB::table('documents')->find($resource->document_id);
|
||||
if (! $document || $document->status !== 'active' || $document->malware_scan_status !== 'clean' || ! in_array($document->media_type, ['text/plain', 'application/json'], true)) {
|
||||
return null;
|
||||
}
|
||||
$binary = app(VerifiedDocumentBinary::class)->respond($document);
|
||||
$bytes = $binary instanceof BinaryFileResponse ? file_get_contents($binary->getFile()->getPathname()) : $binary->getContent();
|
||||
if (! is_string($bytes) || strlen($bytes) > 10485760) {
|
||||
return null;
|
||||
}
|
||||
$original = json_decode($bytes, true);
|
||||
|
||||
return is_array($original) && ($original['format'] ?? '') === 'investmoz-legacy-article-v1' && is_string($original['content_html'] ?? null) ? $original : null;
|
||||
}
|
||||
|
||||
public function plain(string $html): string
|
||||
{
|
||||
return trim(html_entity_decode(strip_tags(preg_replace('~</(?:p|div|li|h[1-6]|tr)>|<br\s*/?>~i', "\n\n", $html)), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
}
|
||||
|
||||
public function content(object $resource, ?array $original): array
|
||||
{
|
||||
return $resource->editorial_content ? json_decode($resource->editorial_content, true, 512, JSON_THROW_ON_ERROR)
|
||||
: ['body' => $this->plain($original['content_html'] ?? ''), 'image_resource_id' => null, 'image_mode' => 'original', 'preserve_original_html' => true];
|
||||
}
|
||||
|
||||
private function sourceUrl(mixed $url, ?string $base = null): ?string
|
||||
{
|
||||
if (! is_string($url) || trim($url) === '') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$absolute = $base ? (string) UriResolver::resolve(new Uri($base), new Uri($url)) : $url;
|
||||
|
||||
return filter_var($absolute, FILTER_VALIDATE_URL) && in_array(strtolower((string) parse_url($absolute, PHP_URL_SCHEME)), ['http', 'https'], true) ? $absolute : null;
|
||||
} catch (\InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function reading(object $resource, ?array $override = null, bool $private = false): array
|
||||
{
|
||||
$original = $this->original($resource);
|
||||
$content = $override ?? $this->content($resource, $original);
|
||||
abort_unless(trim($content['body'] ?? '') !== '', 404, 'Este documento não contém um artigo legível.');
|
||||
$document = DB::table('documents')->find($resource->document_id);
|
||||
// The original is verified above; ordinary text adaptations retain the binary gate too.
|
||||
app(VerifiedDocumentBinary::class)->respond($document);
|
||||
$mode = $content['image_mode'] ?? (! empty($content['image_resource_id']) ? 'selected' : 'none');
|
||||
$originalImageUrl = $this->sourceUrl($original['image_source_url'] ?? null, $original['source_url'] ?? null);
|
||||
$expectedImageSha = $original['image_sha256'] ?? null;
|
||||
if (! $expectedImageSha && $originalImageUrl === 'https://investemoz.tempo.co.mz/images/tempo-background.png') {
|
||||
foreach (json_decode(file_get_contents(config_path('legacy_editorial_resources.json')), true, 512, JSON_THROW_ON_ERROR) as $accepted) {
|
||||
if ($accepted['sha256'] === $document->sha256) {
|
||||
$expectedImageSha = $accepted['original_image_sha256'] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$images = $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp']);
|
||||
$image = null;
|
||||
if ($mode === 'selected' && ! empty($content['image_resource_id'])) {
|
||||
$image = (clone $images)->where('r.id', $content['image_resource_id'])->first(['r.id', 'r.title']);
|
||||
}
|
||||
if ($mode === 'original' && $original) {
|
||||
$image = (clone $images)->where(function ($q) use ($expectedImageSha, $originalImageUrl) {
|
||||
$q->whereRaw('1=0');
|
||||
if ($expectedImageSha) {
|
||||
$q->orWhere('d.sha256', $expectedImageSha);
|
||||
} elseif ($originalImageUrl) {
|
||||
$q->orWhere('r.source_url', $originalImageUrl)->orWhere('d.original_source', $originalImageUrl);
|
||||
}
|
||||
})->first(['r.id', 'r.title']);
|
||||
}
|
||||
if ($image) {
|
||||
// A public flag alone never licenses missing or changed image bytes.
|
||||
$binary = (clone $images)->where('r.id', $image->id)->first(['d.*']);
|
||||
try {
|
||||
app(VerifiedDocumentBinary::class)->respond($binary);
|
||||
} catch (HttpException) {
|
||||
$image = null;
|
||||
}
|
||||
}
|
||||
$originalFormat = $original && ($content['preserve_original_html'] ?? false) && $content['body'] === $this->plain($original['content_html']);
|
||||
$html = $originalFormat ? app(LegacyContent::class)->sanitize($original['content_html'], true) : null;
|
||||
$imageReferences = [];
|
||||
if ($original) {
|
||||
preg_match_all('~<img\b[^>]*\bsrc\s*=\s*([\'"])(.*?)\1~is', $original['content_html'], $matches);
|
||||
foreach ($matches[2] as $source) {
|
||||
$url = $this->sourceUrl(html_entity_decode($source, ENT_QUOTES | ENT_HTML5, 'UTF-8'), $original['source_url'] ?? null);
|
||||
if ($url) {
|
||||
$imageReferences[] = $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['id' => $resource->id, 'title' => $resource->title, 'description' => $resource->description,
|
||||
'author' => $document->issuer, 'body' => $content['body'], 'html' => $html,
|
||||
'image_url' => $image ? '/documentos/'.$image->id.'/view' : null,
|
||||
'image_notice' => $mode === 'selected' && ! $image ? 'A imagem escolhida está indisponível para divulgação; a selecção mantém-se na edição.' : ($mode === 'original' && ! $image && ! empty($original['image_source_url']) ? 'Imagem de origem referenciada, mas sem exemplar público verificado na biblioteca. A referência original mantém-se preservada.' : null),
|
||||
'source_url' => $this->sourceUrl($original['source_url'] ?? $resource->source_url),
|
||||
'published_at' => $original['published_at'] ?? null, 'modified_at' => $original['modified_at'] ?? null,
|
||||
'legacy_id' => $original['legacy_id'] ?? null, 'image_source_url' => $originalImageUrl, 'image_expected_sha256' => $expectedImageSha, 'image_references' => array_values(array_unique($imageReferences)),
|
||||
'metadata_version' => $resource->metadata_version, 'private_preview' => $private,
|
||||
'original_url' => $private ? '/management/documents/'.$resource->document_id.'/download' : '/documentos/'.$resource->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class InvestmentCatalogue
|
||||
'cluster_id' => ['nullable', 'uuid'], 'corridor_id' => ['nullable', 'uuid'], 'project_id' => ['nullable', 'uuid'],
|
||||
'geo_location_id' => ['nullable', 'uuid'], 'education' => ['nullable', 'boolean'],
|
||||
'zee_id' => ['nullable', 'uuid'], 'zfi_id' => ['nullable', 'uuid'], 'park_id' => ['nullable', 'uuid'],
|
||||
'dimension' => ['nullable', 'in:cluster,corridor,zee,zfi'], 'view' => ['nullable', 'in:grid,list,map,combined'],
|
||||
'dimension' => ['nullable', 'in:cluster,corridor,zee,zfi'], 'view' => ['nullable', 'in:grid,list,map,combined,corridors'],
|
||||
'maturity_level' => ['nullable', 'integer', 'between:1,8'], 'facets' => ['nullable', 'array', 'max:20'], 'facets.*' => ['integer', 'min:1'],
|
||||
'min_amount' => ['nullable', 'numeric', 'min:0', 'max:999999999999'],
|
||||
'max_amount' => ['nullable', 'numeric', 'min:0', 'max:999999999999', ...($request->filled('min_amount') ? ['gte:min_amount'] : [])],
|
||||
@@ -231,16 +231,25 @@ class InvestmentCatalogue
|
||||
} elseif (in_array($relation, ['clusters', 'corridors'], true)) {
|
||||
$query->whereIn('id', $this->publicStructureIds($relation, $foreign));
|
||||
} else {
|
||||
$query->whereIn('id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign));
|
||||
$query->whereIn('id', app(OpportunityClassification::class)->targets('project')->select('id'));
|
||||
}
|
||||
}
|
||||
if (! $management && $path !== [] && $relation !== 'locations') {
|
||||
$dossiers = InvestmentPortfolioController::visible();
|
||||
app(InvestmentGeography::class)->constrainDossiersToTerritories($dossiers,
|
||||
app(InvestmentGeography::class)->descendants($path[array_key_last($path)]->id));
|
||||
$query->whereIn('id', $dossiers->whereNotNull($foreign)->select($foreign));
|
||||
$territories = app(InvestmentGeography::class)->descendants($path[array_key_last($path)]->id);
|
||||
$query->where(fn ($area) => $area->whereIn('primary_territory_id', $territories)
|
||||
->orWhereIn('id', $dossiers->whereNotNull($foreign)->select($foreign)));
|
||||
}
|
||||
$result[$relation] = $query->orderBy('name')->get(['id', 'name']);
|
||||
if (! $management && $relation !== 'locations') {
|
||||
$dossiers = InvestmentPortfolioController::visible()->whereIn($foreign, $result[$relation]->pluck('id'))
|
||||
->orderBy('id')->get(['id', $foreign])->unique($foreign)->keyBy($foreign);
|
||||
$result[$relation]->each(function ($record) use ($dossiers): void {
|
||||
$record->url = isset($dossiers[$record->id]) ? '/carteira/'.$dossiers[$record->id]->id : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$zones = app(InvestmentZones::class)->visible();
|
||||
@@ -250,24 +259,15 @@ class InvestmentCatalogue
|
||||
['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'geo_location_id']));
|
||||
}
|
||||
$result['zones'] = $zones->orderBy('name')->get(['id', 'name', 'category']);
|
||||
if (! $management && ($path !== [] || ! empty($territoryFilters['sector']))) {
|
||||
$scope = \Illuminate\Support\Arr::only($territoryFilters, ['sector','province_id','district_id','admin_post_id','locality_id','include_national']);
|
||||
$available = ['chain' => [], 'activity' => []];
|
||||
$this->query($scope)->reorder()->chunkById(200, function ($records) use (&$available) {
|
||||
foreach ($records as $record) {
|
||||
foreach (app(OpportunityClassification::class)->publicRows($record) as $row) {
|
||||
if (isset($available[$row['target_type']])) $available[$row['target_type']][] = $row['target_id'];
|
||||
}
|
||||
foreach (['subsector_id','economic_activity_id'] as $key) {
|
||||
$id = $record->workflow_context['publication']['taxonomy'][$key] ?? null;
|
||||
if ($id) $available['activity'][] = $id;
|
||||
}
|
||||
}
|
||||
}, 'opportunities.id', 'id');
|
||||
foreach ($available as $kind => $ids) {
|
||||
$result['classification']['targets'][$kind] = collect($result['classification']['targets'][$kind])->whereIn('id', $ids)->values();
|
||||
}
|
||||
if (! $management && $path !== []) {
|
||||
// A chain's documented scope comes from its public cluster, never
|
||||
// from the opportunities surviving the current search.
|
||||
$ids = app(OpportunityClassification::class)->targets('chain')
|
||||
->whereIn('cluster_id', $result['clusters']->pluck('id'))->pluck('id');
|
||||
$result['classification']['targets']['chain'] = collect($result['classification']['targets']['chain'])->whereIn('id', $ids)->values();
|
||||
}
|
||||
// Activities are a public classification catalogue, not evidence of
|
||||
// investment availability in a territory or sector.
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -292,6 +292,10 @@ class InvestmentCatalogue
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!empty($selected['corridor_id'])) {
|
||||
$dossier = InvestmentPortfolioController::visible()->where('corridor_id', $selected['corridor_id'])->first();
|
||||
if ($dossier) $ids['corridor_id'] = array_values(array_unique([...($ids['corridor_id'] ?? []), ...app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($dossier)]));
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
@@ -308,6 +312,18 @@ class InvestmentCatalogue
|
||||
/** @return array<string,mixed> */
|
||||
public function context(int $id, bool $management = false): array
|
||||
{
|
||||
$opportunity = Opportunity::findOrFail($id);
|
||||
if (! $management && data_get($opportunity->workflow_context, 'publication.purpose') === OpportunityPromotion::PURPOSE) {
|
||||
// Promotional disclosure includes only its explicit public coverage.
|
||||
// Private profiles, source evidence and other dossier relations are not auto-published.
|
||||
$public = ['profile' => null, 'facets' => collect(), 'zones' => collect(), 'classification' => [], 'source_evidence' => [],
|
||||
'primary_territory_path' => app(InvestmentGeography::class)->path($opportunity->primary_territory_id, true),
|
||||
'opportunity_locations' => app(InvestmentRelations::class)->opportunityLocations($id)];
|
||||
foreach (self::RELATIONS as $relation => $_) $public[$relation] = collect();
|
||||
$public['locations'] = DB::table('opportunity_locations as l')->join('geo_locations as g', 'g.id', '=', 'l.geo_location_id')
|
||||
->where('l.opportunity_id', $id)->where('g.data_classification', 'public')->whereNull('g.deleted_at')->get(['g.id', 'g.name']);
|
||||
return $public;
|
||||
}
|
||||
$result = ['profile' => DB::table('opportunity_profiles')->where('opportunity_id', $id)->first(),
|
||||
'facets' => DB::table('opportunity_facets as f')->join('investment_reference_options as o', 'o.id', '=', 'f.option_id')
|
||||
->where('f.opportunity_id', $id)->get(['o.id', 'o.group', 'o.label'])];
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\InvestmentPortfolioController;
|
||||
use App\Models\Opportunity;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/** Resolve documented complements without creating opportunities or master relations. */
|
||||
final class InvestmentComplements
|
||||
{
|
||||
private const TERMS = [
|
||||
'irrigation' => ['Regadio', 'regadio|irrigação|irrigacao'],
|
||||
'storage' => ['Silos e armazenagem', 'silos?|armazenagem|armazenamento'],
|
||||
'cold' => ['Cadeia de frio', 'frio|refrigeração'],
|
||||
'processing' => ['Processamento', 'processamento|transformação|moagem'],
|
||||
'spinning' => ['Fiação', 'fiação|fiacao'],
|
||||
'logistics' => ['Logística', 'logística|logistica|transporte'],
|
||||
'certification' => ['Certificação', 'certificação|certificacao'],
|
||||
'training' => ['Formação', 'formação|formacao'],
|
||||
'finance' => ['Financiamento', 'financiamento|serviços financeiros'],
|
||||
];
|
||||
|
||||
public function options(string $origin, ?string $territory = null): array
|
||||
{
|
||||
[$originKind, $originId] = array_pad(explode(':', $origin, 2), 2, '');
|
||||
abort_unless(($originKind === 'portfolio' && Str::isUuid($originId)) || ($originKind === 'opportunity' && ctype_digit($originId)), 422, 'Origem complementar inválida.');
|
||||
$principal = null;
|
||||
$relationEvidence = [];
|
||||
if (str_starts_with($origin, 'portfolio:')) {
|
||||
$record = InvestmentPortfolioController::visible()->addSelect('version_no')->findOrFail(substr($origin, 10));
|
||||
$text = trim(($record->content['focus'] ?? '')."\n".($record->content['gaps'] ?? '')."\n".($record->content['description'] ?? ''));
|
||||
$reference = ['key' => $origin, 'title' => $record->title, 'href' => '/carteira/'.$record->id, 'version' => $record->version_no];
|
||||
$ids = app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record);
|
||||
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) {
|
||||
if ($record->$foreign) $ids = [...$ids, ...DB::table('opportunity_'.$relation)->where($foreign, $record->$foreign)->pluck('opportunity_id')->all()];
|
||||
}
|
||||
$basis = $record->source;
|
||||
foreach (array_unique($ids) as $id) $relationEvidence[$id] = ['type' => 'portfolio', 'origin' => $origin, 'source' => $basis];
|
||||
$serviceOrigins = [['dossier', $record->id]];
|
||||
foreach (['project_id' => 'project', 'cluster_id' => 'cluster'] as $foreign => $type) {
|
||||
if ($record->$foreign) $serviceOrigins[] = [$type, $record->$foreign];
|
||||
}
|
||||
} else {
|
||||
$reference = app(InvestorContext::class)->reference($origin);
|
||||
abort_unless($reference['kind'] === 'opportunity', 422);
|
||||
$principal = Opportunity::published()->findOrFail($reference['id']);
|
||||
$text = $principal->summary;
|
||||
$basis = 'Resumo cadastrado da oportunidade '.$principal->reference;
|
||||
$projects = DB::table('opportunity_projects')->where('opportunity_id', $principal->id)->pluck('project_id');
|
||||
$links = DB::table('complementary_project_links')->whereIn('anchor_project_id', $projects)
|
||||
->whereNull('deleted_at')->where('data_classification', 'public')->get(['id', 'anchor_project_id', 'complementary_project_id', 'relation_type', 'version_no']);
|
||||
$targetProjects = DB::table('opportunity_projects')->whereIn('project_id', $links->pluck('complementary_project_id'))->get();
|
||||
$ids = [$principal->id, ...$targetProjects->pluck('opportunity_id')->all()];
|
||||
$relationEvidence[$principal->id] = ['type' => 'same_fiche', 'source' => $basis];
|
||||
foreach ($targetProjects as $targetProject) $relationEvidence[$targetProject->opportunity_id] = ['type' => 'complementary_project',
|
||||
'links' => $links->where('complementary_project_id', $targetProject->project_id)->values()->map(fn($link) => (array)$link)->all()];
|
||||
foreach (Opportunity::published()->whereIn('id', $ids)->where('id', '!=', $principal->id)->get() as $linked) $text .= "\n".$linked->summary;
|
||||
if (count($ids) > 1) $basis .= '; relações complementares públicas entre projectos';
|
||||
$serviceOrigins = [['investment', (string) $principal->id]];
|
||||
$classification = app(OpportunityClassification::class);
|
||||
$principalChains = collect($classification->publicRows($principal))->where('target_type', 'chain')->filter(fn($r) => !empty($r['source_reference']));
|
||||
$chains = $principalChains->pluck('target_id')->all();
|
||||
if ($chains) {
|
||||
Opportunity::published()->whereNot('id', $principal->id)->chunkById(100, function ($records) use (&$ids, &$relationEvidence, $chains, $principalChains, $classification) {
|
||||
foreach ($records as $candidate) {
|
||||
$related = collect($classification->publicRows($candidate))->where('target_type', 'chain')->filter(fn($r) => !empty($r['source_reference']));
|
||||
$shared = array_intersect($chains, $related->pluck('target_id')->all());
|
||||
if ($shared) {
|
||||
$ids[] = $candidate->id;
|
||||
$relationEvidence[$candidate->id] ??= ['type' => 'value_chain', 'principal' => $principalChains->whereIn('target_id', $shared)->values()->all(), 'target' => $related->whereIn('target_id', $shared)->values()->all()];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
$territoryReference = $territory ? app(InvestorContext::class)->reference('territory:'.$territory) : null;
|
||||
$candidates = Opportunity::published()->whereIn('id', array_unique($ids))->get();
|
||||
$items = [];
|
||||
foreach (self::TERMS as $code => [$label, $pattern]) {
|
||||
if (!preg_match('/\b(?:'.$pattern.')\b/iu', $text, $match)) continue;
|
||||
$offers = [];
|
||||
foreach ($candidates as $candidate) {
|
||||
// A common corridor alone does not prove the component. Keep the supporting excerpt.
|
||||
if (!preg_match('/\b(?:'.$pattern.')\b/iu', $candidate->title."\n".$candidate->summary, $evidence)) continue;
|
||||
// Mentioning a need for funding/training in a project's summary is not an offer.
|
||||
if (in_array($code, ['certification', 'training', 'finance'], true) && !preg_match('/\b(?:'.$pattern.')\b/iu', $candidate->title)) continue;
|
||||
if (in_array($code, ['certification', 'training', 'finance'], true) && preg_match('/\b(?:necessidade|pedido|procura)\b/iu', $candidate->title)) continue;
|
||||
if ($territory && $candidate->primary_territory_id && !$this->compatible($territory, $candidate->primary_territory_id)) continue;
|
||||
$offers[] = ['key' => 'opportunity:'.$candidate->id, 'title' => $candidate->title, 'href' => '/oportunidades/'.$candidate->slug,
|
||||
'relationship' => $relationEvidence[$candidate->id] ?? null,
|
||||
'version' => $candidate->management_version, 'evidence' => $evidence[0], 'location' => ($principal && $principal->id === $candidate->id ? 'Componente referido na própria ficha. ' : '').($candidate->primary_territory_id ? app(InvestorContext::class)->reference('territory:'.$candidate->primary_territory_id)['title'] : 'Implantação por definir')];
|
||||
}
|
||||
// Services are concrete published offerings explicitly associated with this source.
|
||||
$services = [];
|
||||
if (in_array($code, ['certification', 'training', 'finance'], true) && Schema::hasTable('partnership_origins') && Schema::hasTable('partnership_interests')) {
|
||||
if ($serviceOrigins) {
|
||||
$services = DB::table('portal_partnerships as p')->join('partnership_origins as o', 'o.partnership_id', '=', 'p.id')
|
||||
->where(function ($q) use ($serviceOrigins) {
|
||||
foreach ($serviceOrigins as [$type, $id]) $q->orWhere(fn($q) => $q->where('o.type', $type)->where('o.target_id', $id));
|
||||
})->where('p.status', 'published')->where('p.kind', 'offering')
|
||||
->where('p.partnership_type', $code === 'finance' ? 'finance' : 'training')
|
||||
->when($code !== 'finance', fn($q) => $q->where(fn($q) => $q->where('p.title', 'ilike', '%'.$match[0].'%')->orWhereRaw("p.payload->>'summary' ILIKE ?", ['%'.$match[0].'%'])))->distinct()->get(['p.id', 'p.title'])
|
||||
->filter(function ($p) use ($territory) {
|
||||
if (!$territory) return true;
|
||||
$locations = DB::table('partnership_locations')->where('partnership_id', $p->id)->pluck('geo_location_id');
|
||||
return $locations->isEmpty() || $locations->contains(fn($id) => $this->compatible($territory, $id));
|
||||
})->map(fn ($p) => ['key' => 'partnership:'.$p->id, 'title' => $p->title, 'href' => '/parcerias/'.$p->id, 'location' => 'Consultar âmbito na oferta'])->values()->all();
|
||||
}
|
||||
}
|
||||
$excerpt = collect(explode("\n", $text))->first(fn($line) => preg_match('/\b(?:'.$pattern.')\b/iu', $line));
|
||||
$items[] = ['code' => $code, 'title' => $label, 'origin' => $reference, 'territory_id' => $territory, 'territory' => $territoryReference,
|
||||
'source' => $basis, 'excerpt' => $excerpt, 'source_sha256' => hash('sha256', $text), 'opportunities' => $offers, 'services' => $services];
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function compatible(string $a, string $b): bool
|
||||
{
|
||||
if ($a === $b) return true;
|
||||
$geo = app(InvestmentGeography::class);
|
||||
return in_array($b, $geo->descendants($a), true) || in_array($a, $geo->descendants($b), true);
|
||||
}
|
||||
|
||||
public function resolve(array $input, array $previous = []): array
|
||||
{
|
||||
$resolved = [];
|
||||
foreach ($input as $choice) {
|
||||
$target = $choice['target'] ?? null;
|
||||
$old = collect($previous)->first(fn($r) => $r['origin']['key'] === $choice['origin'] && $r['code'] === $choice['code'] && ($r['target']['key'] ?? null) === $target && ($r['territory_id'] ?? null) === ($choice['territory_id'] ?? null));
|
||||
try {
|
||||
$item = collect($this->options($choice['origin'], $choice['territory_id'] ?? null))->firstWhere('code', $choice['code']);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
if (!$old) throw $e;
|
||||
$item = null;
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
|
||||
if (!$old || $e->getStatusCode() !== 404) throw $e;
|
||||
$item = null;
|
||||
}
|
||||
$offer = $target && $item ? collect([...$item['opportunities'], ...$item['services']])->firstWhere('key', $target) : null;
|
||||
if ($old && (!$item || ($target && !$offer) || (!$target && ($item['opportunities'] || $item['services'])))) {
|
||||
$resolved[$old['key']] = [...$old, 'availability' => 'review_required'];
|
||||
continue;
|
||||
}
|
||||
abort_unless($item, 422, 'Complemento sem referência documentada neste investimento.');
|
||||
abort_if($target && !$offer, 422, 'A oferta complementar deixou de estar disponível neste contexto.');
|
||||
abort_if(!$target && ($item['opportunities'] || $item['services']), 422, 'Já existe uma oferta relacionada; seleccione a ficha correspondente.');
|
||||
$key = hash('sha256', $choice['origin'].'|'.$choice['code'].'|'.($target ?? 'idea').'|'.($choice['territory_id'] ?? ''));
|
||||
$resolved[$key] = ['key' => $key, 'origin' => $item['origin'], 'code' => $item['code'], 'title' => $item['title'],
|
||||
'kind' => $target ? (str_starts_with($target, 'opportunity:') ? 'opportunity' : 'service') : 'need',
|
||||
'target' => $offer, 'territory_id' => $item['territory_id'], 'territory' => $item['territory'], 'source' => $item['source'], 'excerpt' => $item['excerpt'], 'source_sha256' => $item['source_sha256']];
|
||||
}
|
||||
return array_values($resolved);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/** An identified opportunity within the authorised dossier, never the account's last choice. */
|
||||
final class InvestmentConsumerScope
|
||||
{
|
||||
public function key(array $context, ?string $key, bool $required = false): ?string
|
||||
{
|
||||
$choices = $context['opportunity_choices'] ?? [];
|
||||
$key = $key ?: (count($choices) === 1 ? $choices[0]['opportunity']['key'] : null);
|
||||
abort_if($key && !collect($choices)->contains('opportunity.key', $key), 422, 'Escolha uma oportunidade deste dossier.');
|
||||
abort_if($required && count($choices) > 1 && !$key, 422, 'Escolha a oportunidade em tratamento.');
|
||||
return $key;
|
||||
}
|
||||
|
||||
public function facts(array $payload, array $context, ?string $key): array
|
||||
{
|
||||
$key = $this->key($context, $key);
|
||||
$path = $payload['investor_path'] ?? [];
|
||||
$defaults = ['land_needed' => 'unknown', 'external_funding' => 'unknown', 'viability' => 'unknown'];
|
||||
// A dossier-wide dispensation cannot decide a distinct opportunity's needs.
|
||||
if ($key && count($context['opportunity_choices'] ?? []) > 1) return [...$defaults, ...($path['opportunity_facts'][$key] ?? [])];
|
||||
if ($key && isset($path['opportunity_facts'][$key])) return [...$defaults, ...$path['opportunity_facts'][$key]];
|
||||
if (count($context['opportunity_choices'] ?? []) > 1) return $defaults;
|
||||
return [...$defaults, ...($path['facts'] ?? [])];
|
||||
}
|
||||
|
||||
public function context(array $context, ?string $key): array
|
||||
{
|
||||
$choices = $context['opportunity_choices'] ?? [];
|
||||
if (!$choices) return $context;
|
||||
$key = $key ?: (count($choices) === 1 ? $choices[0]['opportunity']['key'] : null);
|
||||
$choice = $key ? collect($choices)->firstWhere('opportunity.key', $key) : null;
|
||||
abort_if($key && !$choice, 422, 'Escolha uma oportunidade deste dossier.');
|
||||
$specific = ['project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'special_zone_id', 'investment_regime'];
|
||||
$context['fields'] = Arr::except($context['fields'], $specific);
|
||||
$context['provenance'] = Arr::except($context['provenance'] ?? [], $specific);
|
||||
$context['selection'] = Arr::except($context['selection'], ['opportunity', 'territory', 'park']);
|
||||
if (!$choice) {
|
||||
$context['opportunity_choices'] = [];
|
||||
$context['requirements_assessment']['requirements'] = [];
|
||||
return $context;
|
||||
}
|
||||
$context['selection']['opportunity'] = $choice['opportunity'];
|
||||
$context['fields'] = [...$context['fields'], ...Arr::only($choice['opportunity']['fields'], $specific)];
|
||||
foreach (Arr::only($choice['opportunity']['fields'], $specific) as $field => $value) $context['provenance'][$field] = Arr::only($choice['opportunity'], ['key','kind','id','title','version','sha256']);
|
||||
unset($context['selection']['sector']);
|
||||
if (!empty($context['fields']['sector_id'])) $context['selection']['sector'] = app(InvestorContext::class)->reference('sector:'.$context['fields']['sector_id']);
|
||||
if ($choice['territory']) {
|
||||
$context['selection']['territory'] = $choice['territory'];
|
||||
$context['fields'] = [...Arr::except($context['fields'], array_keys(InvestmentGeography::FIELDS)), ...$choice['territory']['fields']];
|
||||
foreach ($choice['territory']['fields'] as $field => $value) $context['provenance'][$field] = Arr::only($choice['territory'], ['key','kind','id','title','version','sha256']);
|
||||
} else {
|
||||
$context['fields'] = Arr::except($context['fields'], ['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location']);
|
||||
}
|
||||
$park = $choice['park'] ?? null;
|
||||
if ($park) $context['selection']['park'] = $park;
|
||||
$context['fields']['special_zone_id'] = $park['id'] ?? '';
|
||||
$context['fields']['investment_regime'] = $park['fields']['investment_regime'] ?? 'general';
|
||||
$context['opportunity_choices'] = [$choice];
|
||||
$context['requirements_assessment']['requirements'] = array_values(array_filter($context['requirements_assessment']['requirements'] ?? [], fn($r) => $r['opportunity_key'] === $key));
|
||||
$context['consumer_opportunity'] = $choice;
|
||||
$context['complements'] = array_values(array_filter($context['complements'] ?? [], fn($c) => ($c['origin']['key'] ?? null) === $key || ($c['target']['key'] ?? null) === $key || in_array(substr($c['origin']['key'] ?? '',10),array_column($choice['portfolio_origins'] ?? [],'id'),true)));
|
||||
$context['dependency_hash'] = app(InvestorContext::class)->dependencyHash(['opportunity_choices'=>[$choice], 'fields'=>Arr::except($context['fields'],['project_title','project_summary']), 'complements'=>$context['complements']]);
|
||||
return $context;
|
||||
}
|
||||
|
||||
public function bind(User $actor, string $id, array $payload, bool $required = false): array
|
||||
{
|
||||
$case = app(VirtualDesk::class)->case($actor, $id, true);
|
||||
$context = app(InvestorContext::class)->state($case);
|
||||
$scoped = $this->context($context, $payload['context_opportunity'] ?? null);
|
||||
$choice = $scoped['consumer_opportunity'] ?? null;
|
||||
abort_if($required && count($context['opportunity_choices'] ?? []) > 1 && !$choice, 422, 'Escolha a oportunidade em tratamento antes de submeter.');
|
||||
if (!$choice) return Arr::except($payload, ['context_opportunity', 'context_opportunity_snapshot']);
|
||||
// Canonical server snapshot; client text cannot establish a different origin or territory.
|
||||
$payload['context_opportunity'] = $choice['opportunity']['key'];
|
||||
$payload['context_opportunity_snapshot'] = json_encode(['context_id' => $id, 'revision' => $context['revision'], 'choice' => $choice, 'requirements' => $scoped['requirements_assessment'] ?? null], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
/** Preparation within the existing path; no licence, DUAT or fiscal benefit is granted. */
|
||||
final class InvestmentContextRequirements
|
||||
{
|
||||
public const VERSION = 'context-requirements-20261023';
|
||||
|
||||
public function assess(array $context): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach ($context['opportunity_choices'] ?? [] as $choice) {
|
||||
$activities = $choice['activities'] ?? [];
|
||||
$key = $choice['opportunity']['key'];
|
||||
$common = ['opportunity_key' => $key, 'opportunity_title' => $choice['opportunity']['title']];
|
||||
$add = function ($code, $title, $state, $step, $authority, $documents, $basis, $url) use (&$rows, $common) {
|
||||
$rows[] = [...$common, 'code' => $code, 'title' => $title, 'state' => $state, 'step' => $step,
|
||||
'authority' => $authority, 'documents' => $documents, 'basis' => $basis, 'source_url' => $url,
|
||||
'decision' => 'Preparação; enquadramento e acto concreto conservam confirmação na etapa correspondente.'];
|
||||
};
|
||||
$add('geography', 'Localização e geolocalização', $choice['territory'] ? 'selected' : 'missing', 'S09', 'Cadastro territorial e serviços de cadastro competentes',
|
||||
['Referência administrativa confirmada', 'Planta/esboço e coordenadas quando disponíveis; não inventar polígonos'],
|
||||
'Referência territorial escolhida no dossier; não comprova disponibilidade nem direito de uso.', '/investimentos');
|
||||
$add('land', 'Acesso à terra e DUAT', 'conditional', 'S10', 'Serviços de cadastro e órgão competente para o pedido territorial concreto',
|
||||
['Fundamento do uso: DUAT, arrendamento ou situação declarada', 'Planta/esboço do local; documentação do pedido correspondente'],
|
||||
'Regulamento de Investimento, artigos 9(1)(c)–(d) e 14(1)(c)–(d). A preparação distingue DUAT e arrendamento; escolher território não atribui terra.', PrivateInvestmentPreparation::SOURCE.'#page=42');
|
||||
$add('environment', 'Enquadramento ambiental', 'conditional', 'S08', 'Autoridade de Avaliação do Impacto Ambiental; competência central/provincial conforme categorização',
|
||||
['Descrição da actividade, dimensão e localização', 'Instrumento ambiental definido pela categorização competente; acto e condições quando emitidos'],
|
||||
'Decreto 54/2015: a natureza, dimensão e localização determinam o enquadramento; a categoria não é deduzida do corredor ou do sector.', 'https://sibmoz.gov.mz/legal-framework/');
|
||||
$add('sector', 'Licenciamento sectorial', $activities ? 'declared' : 'missing', 'S08', 'BAU, IP e autoridade sectorial competente conforme a actividade',
|
||||
['Actividades efectivas e modalidade', 'Requerimento e anexos da modalidade aplicável; acto quando emitido'],
|
||||
'Identificar as actividades efectivas antes de seleccionar o procedimento. O nome Agronegócio não escolhe uma licença única.', 'https://www.bau.gov.mz/nossos-servicos/');
|
||||
foreach (['processing' => ['Licenciamento industrial', 'Decreto 22/2014'], 'trade' => ['Licenciamento comercial', 'Decreto 34/2013']] as $activity => [$title, $basis]) {
|
||||
if (in_array($activity, $activities, true)) $add($activity, $title, 'conditional', 'S08', 'BAU, IP / entidade licenciadora da modalidade',
|
||||
['Caracterização do estabelecimento e actividade', 'Documentos e regime aplicáveis, incluindo simplificado ou mera comunicação quando preenchidas as suas condições'],
|
||||
$basis.'; regimes 39/2017 e 27/2023 no catálogo oficial BAU. Dimensão e modalidade ainda devem ser especificadas; não se selecciona automaticamente o regime.', 'https://www.bau.gov.mz/legislacao/');
|
||||
}
|
||||
foreach (['water' => 'Uso de água', 'forestry' => 'Actividade florestal', 'animal' => 'Actividade pecuária', 'transport' => 'Transporte e logística'] as $activity => $title) {
|
||||
if (in_array($activity, $activities, true)) $add($activity, $title, 'undetermined', 'S08', 'Autoridade sectorial competente a identificar para a modalidade declarada',
|
||||
['Modalidade e dimensão da actividade', 'Fonte e documentação sectorial específica a associar ao dossier'],
|
||||
'Actividade declarada; a referência do lote não contém o procedimento específico. Não se considera dispensada ou autorizada.', 'https://www.apiex.gov.mz/wp-content/uploads/2024/01/Mozambique-Business-Licensing-Guide.pdf');
|
||||
}
|
||||
}
|
||||
return ['version' => self::VERSION, 'requirements' => $rows,
|
||||
'fiscal_notice' => 'Localização, corredor, cluster ou projecto âncora não activam benefícios fiscais. O simulador mantém as condições, fonte, período e âmbito próprios; condições ausentes permanecem indeterminadas.'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class InvestmentCorridorContext
|
||||
{
|
||||
public function key(Opportunity $record): ?string
|
||||
{
|
||||
$name = data_get($record->workflow_context, 'corridor_agribusiness.corridor');
|
||||
foreach (config('investment_corridors') as $key => $entry) if ($entry['name'] === $name) return $key;
|
||||
return null;
|
||||
}
|
||||
|
||||
public function provinces(string $key): array
|
||||
{
|
||||
$entry = config('investment_corridors.'.$key);
|
||||
abort_unless(is_array($entry), 422, 'Seleccione um corredor disponível.');
|
||||
$records = app(InvestmentGeography::class)->publicReferences()->where('location_type', 'province')
|
||||
->whereIn('source_code', $entry['provinces'])->get(['id', 'name', 'parent_id', 'location_type', 'source_code'])->all();
|
||||
abort_unless(count($records) === count($entry['provinces']), 503, 'Não foi possível recuperar todas as províncias deste corredor. Tente novamente.');
|
||||
return $records;
|
||||
}
|
||||
|
||||
public function compatible(string $key, ?string $territory): bool
|
||||
{
|
||||
if (!$territory) return true;
|
||||
$path = app(InvestmentGeography::class)->path($territory);
|
||||
return $path !== [] && in_array($path[0]->id, array_column($this->provinces($key), 'id'), true);
|
||||
}
|
||||
|
||||
/** Both membership in the selected area and documented public zone coverage are required. */
|
||||
public function parks(?string $key, ?string $territory): array
|
||||
{
|
||||
if (!$key || !$territory) return [];
|
||||
abort_unless($this->compatible($key, $territory), 422, 'A localização não pertence ao corredor escolhido.');
|
||||
$selectedPath = array_column(app(InvestmentGeography::class)->path($territory), 'id');
|
||||
$candidates = app(InvestmentZones::class)->visible()->whereIn('category', ['industrial_park', 'zee', 'zfi'])->orderBy('name')->get();
|
||||
$result = [];
|
||||
foreach ($candidates as $zone) {
|
||||
$ids = DB::table('investment_zone_locations')->where('zone_id', $zone->id)->where('validation_status', 'validated')
|
||||
->whereIn('scope_type', ['legal_scope', 'administrative', 'site'])
|
||||
->where(fn($q)=>$q->whereNull('valid_from')->orWhereDate('valid_from','<=',today()))
|
||||
->where(fn($q)=>$q->whereNull('valid_until')->orWhereDate('valid_until','>=',today()))->get(['geo_location_id','scope_type'])->all();
|
||||
if ($zone->primary_territory_id) $ids[] = (object)['geo_location_id'=>$zone->primary_territory_id,'scope_type'=>'site'];
|
||||
foreach ($ids as $link) {
|
||||
$id=$link->geo_location_id;
|
||||
if (!$this->compatible($key, $id)) continue;
|
||||
$path = array_column(app(InvestmentGeography::class)->path($id), 'id');
|
||||
if (in_array($territory, $path, true) || ($link->scope_type==='legal_scope' && in_array($id, $selectedPath, true))) {
|
||||
$result[] = $zone->only(['id', 'name', 'category', 'version_no']); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\InvestmentPortfolioController;
|
||||
use App\Models\Opportunity;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** Multiple identified references in the existing encrypted investment context. */
|
||||
final class InvestmentOpportunityChoices
|
||||
{
|
||||
public function portfolioOrigins(array $ids, int $opportunity): array
|
||||
{
|
||||
$origins = [];
|
||||
foreach (array_unique($ids) as $id) {
|
||||
$record = InvestmentPortfolioController::visible()->addSelect('version_no')->findOrFail($id);
|
||||
$kind = null;
|
||||
if (in_array($opportunity, app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record), true)) $kind = 'corridor';
|
||||
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) {
|
||||
if ($record->$foreign && DB::table('opportunity_'.$relation)->where('opportunity_id', $opportunity)->where($foreign, $record->$foreign)->exists()) $kind = rtrim($relation, 's');
|
||||
}
|
||||
abort_unless($kind, 422, 'A associação escolhida não pertence a esta oportunidade.');
|
||||
$origins[] = ['id' => $record->id, 'title' => $record->title, 'kind' => $kind, 'href' => '/carteira/'.$record->id,
|
||||
'version' => $record->version_no, 'sha256' => hash('sha256', json_encode([$record->title, $record->source, $record->content], JSON_THROW_ON_ERROR))];
|
||||
}
|
||||
return $origins;
|
||||
}
|
||||
|
||||
/** Add explicitly consulted associations without resetting saved geography or deliberate answers. */
|
||||
public function mergeEntries(array $saved, array $entries): array
|
||||
{
|
||||
foreach ($entries as $entry) {
|
||||
$index = array_search($entry['opportunity']['key'], array_column(array_column($saved, 'opportunity'), 'key'), true);
|
||||
if ($index === false) { $saved[] = $entry; continue; }
|
||||
$origins = collect([...($saved[$index]['portfolio_origins'] ?? []), ...($entry['portfolio_origins'] ?? [])])->unique('id')->values()->all();
|
||||
if ($origins) $saved[$index]['portfolio_origins'] = $origins;
|
||||
}
|
||||
return $saved;
|
||||
}
|
||||
|
||||
public function resolve(array $choices, array $previous = [], bool $pending = false): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($choices as $choice) {
|
||||
$ref = app(InvestorContext::class)->reference($choice['source']);
|
||||
abort_unless($ref['kind'] === 'opportunity', 422, 'Seleccione uma ficha de oportunidade.');
|
||||
$record = Opportunity::findOrFail($ref['id']);
|
||||
$old = collect($previous)->first(fn ($row) => $row['opportunity']['key'] === $ref['key']);
|
||||
$origins = $this->portfolioOrigins($choice['portfolio_origins'] ?? array_column($old['portfolio_origins'] ?? [], 'id'), (int) $record->id);
|
||||
$corridors = app(InvestmentCorridorContext::class);
|
||||
$corridor = $choice['corridor_key'] ?? $old['corridor_key'] ?? $corridors->key($record);
|
||||
$provinces = $corridor ? $corridors->provinces($corridor) : [];
|
||||
$documented = $record->primary_territory_id;
|
||||
$territory = array_key_exists('territory', $choice) ? $choice['territory'] : ($documented ? 'territory:'.$documented : null);
|
||||
if (!$territory && count($provinces) === 1) $territory = 'territory:'.$provinces[0]->id;
|
||||
if ($corridor && $territory) abort_unless($corridors->compatible($corridor, substr($territory, 10)), 422, 'A localização não pertence ao corredor escolhido.');
|
||||
$previousTerritory = $old['territory']['key'] ?? null;
|
||||
$newAlternative = $documented && $territory !== 'territory:'.$documented
|
||||
&& (! $old || ($old['documented_territory_id'] ?? null) !== $documented || $territory !== $previousTerritory);
|
||||
$corridorChanged = $old && ($old['corridor_key'] ?? $corridors->key($record)) !== $corridor;
|
||||
$incompatibleOld = $corridorChanged && $previousTerritory && !$corridors->compatible($corridor, substr($previousTerritory, 10));
|
||||
if (!$pending && ($newAlternative || ($previousTerritory && $territory !== $previousTerritory && !$incompatibleOld))) {
|
||||
abort_unless(! empty($choice['confirm_location_change']) && mb_strlen(trim($choice['location_reason'] ?? '')) >= 20,
|
||||
422, 'Uma localização documentada ou já confirmada só muda por escolha explícita, com confirmação e motivo.');
|
||||
}
|
||||
$location = $territory ? app(InvestorContext::class)->reference($territory) : null;
|
||||
abort_unless($location === null || $location['kind'] === 'territory', 422);
|
||||
$parkKey = array_key_exists('park', $choice) ? $choice['park'] : ($old['park']['key'] ?? null);
|
||||
$park = $parkKey ? app(InvestorContext::class)->reference($parkKey) : null;
|
||||
if ($park) {
|
||||
$eligible = $corridor ? array_column($corridors->parks($corridor, $location['id'] ?? null), 'id') : [];
|
||||
if (!in_array($park['id'], $eligible, true)) {
|
||||
abort_if(!empty($choice['park']), 422, 'A zona ou parque não é compatível com a localização desta oportunidade.');
|
||||
$park = null;
|
||||
}
|
||||
abort_unless(!$park || $park['kind'] === 'park', 422);
|
||||
}
|
||||
$result[] = ['opportunity' => $ref, 'territory' => $location, ...($origins ? ['portfolio_origins' => $origins] : []),
|
||||
'park' => $park,
|
||||
'corridor_key' => $corridor, 'corridor_name' => $corridor ? config('investment_corridors.'.$corridor.'.name') : null,
|
||||
'allowed_provinces' => array_map(fn($p)=>(array)$p, $provinces),
|
||||
'territory_path' => $location ? array_map(fn ($node) => \Illuminate\Support\Arr::only((array) $node, ['id', 'name', 'parent_id', 'location_type']), app(InvestmentGeography::class)->path($location['id'], true)) : [],
|
||||
'activities' => $choice['activities'] ?? ($old['activities'] ?? []),
|
||||
'documented_territory_id' => $documented, 'location_reason' => $choice['location_reason'] ?? '',
|
||||
'location_state' => $location ? ($documented && $territory === 'territory:'.$documented ? 'documented' : 'investor_selected') : 'undetermined',
|
||||
'location_changed_explicitly' => $old && $territory === $previousTerritory ? (bool) ($old['location_changed_explicitly'] ?? false) : (bool) ($choice['confirm_location_change'] ?? false),
|
||||
'coverage' => data_get($record->workflow_context, 'corridor_agribusiness.corridor'),
|
||||
'source' => data_get($record->workflow_context, 'corridor_agribusiness.source'),
|
||||
'source_sha256' => data_get($record->workflow_context, 'corridor_agribusiness.source_sha256')];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
/** Context belongs to the existing encrypted dossier. Sources remain read-only. */
|
||||
@@ -22,10 +23,71 @@ final class InvestorContext
|
||||
|
||||
public const SOURCE = 'investor-context';
|
||||
|
||||
public const FIELDS = ['project_title', 'project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'currency', 'expected_start_date', 'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'investor_origin', 'person_type', 'entity_stage', 'tax_residency_country', 'fiscal_residency_country', 'activity_id', 'project_type', 'maturity_stage', 'special_zone_id', 'investment_regime'];
|
||||
public const FIELDS = ['project_title', 'project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'currency', 'expected_start_date', 'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'investor_origin', 'person_type', 'entity_stage', 'tax_residency_country', 'fiscal_residency_country', 'activity_id', 'project_type', 'maturity_stage', 'special_zone_id', 'investment_regime', ...PrivateInvestmentPreparation::FIELDS];
|
||||
|
||||
public function __construct(private readonly VirtualDesk $desk) {}
|
||||
|
||||
public function draft(User $actor, string $id): ?array
|
||||
{
|
||||
$case = $this->desk->case($actor, $id, true);
|
||||
$draft = $this->desk->decode($case->payload)['investor_path']['context_drafts'][(string)$actor->id] ?? null;
|
||||
return $draft ? [...$draft, 'needs_review' => (int)$draft['version'] !== (int)$case->version] : null;
|
||||
}
|
||||
|
||||
public function saveDraft(User $actor, string $id, array $input): array
|
||||
{
|
||||
$case=$this->desk->case($actor,$id,true);
|
||||
abort_if($case->closed_at,409);
|
||||
$sequence = (int)($input['draft_sequence'] ?? 0);
|
||||
$entrySources=Validator::make($input,['entry_sources'=>'sometimes|array|max:200','entry_sources.*'=>'string|max:100'])->validate()['entry_sources']??[];
|
||||
unset($input['draft_sequence'],$input['entry_sources']);
|
||||
abort_unless($sequence > 0, 422);
|
||||
$data = $this->input($input);
|
||||
if (isset($data['complements'])) app(InvestmentComplements::class)->resolve($data['complements'], $this->state($case)['complements'] ?? []);
|
||||
foreach ($data['selection'] as $slot=>$key) {
|
||||
if (!$key) continue;
|
||||
$ref=$this->reference($key);
|
||||
abort_unless(in_array($ref['kind'],$slot==='opportunity'?['opportunity','business']:[$slot],true),422,'Referência incompatível com a escolha.');
|
||||
}
|
||||
foreach ($data['opportunity_choices'] ?? [] as $choice) {
|
||||
$ref = $this->reference($choice['source']);
|
||||
abort_unless($ref['kind'] === 'opportunity', 422);
|
||||
$record = Opportunity::findOrFail($ref['id']);
|
||||
app(InvestmentOpportunityChoices::class)->portfolioOrigins($choice['portfolio_origins'] ?? [], (int) $record->id);
|
||||
$key = $choice['corridor_key'] ?? app(InvestmentCorridorContext::class)->key($record);
|
||||
if (!empty($choice['territory'])) {
|
||||
$location = $this->reference($choice['territory']);
|
||||
abort_unless($location['kind'] === 'territory', 422);
|
||||
if ($key) abort_unless(app(InvestmentCorridorContext::class)->compatible($key, $location['id']), 422, 'A localização não pertence ao corredor escolhido.');
|
||||
}
|
||||
}
|
||||
if (isset($data['opportunity_choices'])) app(InvestmentOpportunityChoices::class)->resolve($data['opportunity_choices'], $this->state($case)['opportunity_choices'] ?? [], true);
|
||||
if (!empty($data['selection']['park'])) {
|
||||
$first=$data['opportunity_choices'][0] ?? null;
|
||||
$source=$first['source'] ?? $data['selection']['opportunity'] ?? null;
|
||||
if ($source && str_starts_with($source,'opportunity:')) {
|
||||
$key=$first['corridor_key'] ?? app(InvestmentCorridorContext::class)->key(Opportunity::findOrFail(substr($source,12)));
|
||||
if ($key) {
|
||||
$territory=$first['territory'] ?? $data['selection']['territory'] ?? null;
|
||||
$ids=app(InvestmentCorridorContext::class)->provinces($key);
|
||||
$territory=$territory ? substr($territory,10) : (count($ids)===1?$ids[0]->id:null);
|
||||
$park=$this->reference($data['selection']['park']);
|
||||
abort_unless($park['kind']==='park' && in_array($park['id'],array_column(app(InvestmentCorridorContext::class)->parks($key,$territory),'id'),true),422,'A zona ou parque não tem enquadramento compatível documentado nesta localização.');
|
||||
}
|
||||
}
|
||||
}
|
||||
return DB::transaction(function () use ($actor, $id, $data, $sequence, $entrySources) {
|
||||
$case = $this->desk->case($actor, $id, true, true);
|
||||
abort_if($case->closed_at || (int)$case->version !== (int)$data['version'], 409, 'O dossier mudou. Recupere os dados antes de continuar.');
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
$previous = $payload['investor_path']['context_drafts'][(string)$actor->id] ?? [];
|
||||
abort_if(($previous['draft_sequence'] ?? 0) > $sequence, 409, 'Existe um rascunho mais recente. Recupere o dossier antes de continuar.');
|
||||
$payload['investor_path']['context_drafts'][(string)$actor->id] = [...$data, 'draft_sequence' => $sequence, 'entry_sources'=>array_values(array_unique([...($previous['entry_sources']??[]),...$entrySources]))];
|
||||
DB::table('desk_cases')->where('id', $id)->update(['payload' => $this->desk->encode($payload)]);
|
||||
return ['saved' => true];
|
||||
});
|
||||
}
|
||||
|
||||
public function installed(): bool
|
||||
{
|
||||
return Schema::hasTable('investor_navigation_preferences');
|
||||
@@ -50,6 +112,7 @@ final class InvestorContext
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($context['opportunity_choices']) && !array_key_exists('park', $context['opportunity_choices'][0])) $context['opportunity_choices'][0]['park'] = $context['selection']['park'] ?? null;
|
||||
$zone = $context['selection']['park'] ?? null;
|
||||
$context['fields']['investment_regime'] = $zone['fields']['investment_regime'] ?? 'general';
|
||||
$context['fields']['special_zone_id'] = $zone['id'] ?? '';
|
||||
@@ -59,6 +122,8 @@ final class InvestorContext
|
||||
|
||||
public function dependencyHash(array $selection): string
|
||||
{
|
||||
unset($selection['portfolio'], $selection['selection']['portfolio']);
|
||||
if (isset($selection['opportunity_choices'])) $selection['opportunity_choices'] = array_map(fn($choice)=>Arr::except($choice,['portfolio_origins']),$selection['opportunity_choices']);
|
||||
return hash('sha256', json_encode(Arr::sortRecursive($selection), JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
@@ -104,6 +169,18 @@ final class InvestorContext
|
||||
if ($o->primary_territory_id) {
|
||||
$fields = [...$fields, ...$this->territoryFields($o->primary_territory_id)];
|
||||
}
|
||||
} elseif ($kind === 'portfolio') {
|
||||
abort_unless(Str::isUuid($id), 422);
|
||||
$dossier = \App\Http\Controllers\InvestmentPortfolioController::visible()->findOrFail($id);
|
||||
$title = $dossier->title;
|
||||
$href = '/carteira/'.$id;
|
||||
// Documentary coverage is not the investor's chosen implantation.
|
||||
$version = DB::table('investment_dossiers')->where('id', $id)->value('version_no');
|
||||
$fields = ['project_title' => $title, 'project_summary' => $dossier->content['description'] ?? $dossier->content['focus'] ?? ''];
|
||||
$state = 'documented_origin';
|
||||
$zoneAnalysis = ['source' => $dossier->source, 'documented_sector' => $dossier->content['sector'] ?? null,
|
||||
'documented_location' => $dossier->content['location'] ?? $dossier->content['region'] ?? null,
|
||||
'geography' => app(InvestmentRelations::class)->dossier($dossier), 'available_opportunity' => false];
|
||||
} elseif ($kind === 'business') {
|
||||
abort_unless(config('existing-business.enabled') && Str::isUuid($id), 404);
|
||||
$o = ExistingBusiness::published()->with('sector')->findOrFail($id);
|
||||
@@ -267,10 +344,31 @@ final class InvestorContext
|
||||
|
||||
private function input(array $input): array
|
||||
{
|
||||
abort_if(array_diff(array_keys($input), ['version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', '_token']), 422, 'Campo de controlo desconhecido.');
|
||||
abort_if(array_diff(array_keys($input), ['complements', 'opportunity_choices', 'version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', 'document_links', '_token']), 422, 'Campo de controlo desconhecido.');
|
||||
|
||||
return Validator::make($input, ['version' => 'required|integer|min:1', 'selection' => 'present|array:opportunity,sector,territory,park',
|
||||
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary,currency,expected_start_date,investor_origin,person_type,entity_stage,tax_residency_country,fiscal_residency_country,project_type,maturity_stage', 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
|
||||
abort_if(! app(PrivateInvestmentPreparation::class)->available() && (! empty($input['document_links']) || array_intersect(array_keys(is_array($input['declarations'] ?? null) ? $input['declarations'] : []), PrivateInvestmentPreparation::FIELDS)), 409, 'Esta preparação ainda não está disponível neste ambiente. As respostas foram preservadas.');
|
||||
|
||||
$inputRules = [
|
||||
'document_links' => 'sometimes|array:'.implode(',', PrivateInvestmentPreparation::DOCUMENTS),
|
||||
'document_links.*' => 'nullable|uuid',
|
||||
];
|
||||
foreach (app(PrivateInvestmentPreparation::class)->questions() as $question) {
|
||||
$inputRules['declarations.'.$question['key']] = isset($question['options']) ? ['nullable', Rule::in(array_keys($question['options']))] : ['nullable', 'regex:/^\d+(?:\.\d+)?$/D', 'numeric', 'min:0', 'max:999999999999999999'];
|
||||
}
|
||||
|
||||
return Validator::make($input, [...$inputRules,
|
||||
'complements' => 'sometimes|array|max:100', 'complements.*' => 'array:origin,code,target,territory_id',
|
||||
'complements.*.origin' => 'required|string|max:100', 'complements.*.code' => 'required|string|max:40',
|
||||
'complements.*.target' => 'nullable|string|max:100', 'complements.*.territory_id' => 'nullable|uuid',
|
||||
'opportunity_choices' => 'sometimes|array|max:100', 'opportunity_choices.*' => 'array:source,territory,location_reason,confirm_location_change,activities,corridor_key,portfolio_origins,park',
|
||||
'opportunity_choices.*.portfolio_origins' => 'sometimes|array|max:100', 'opportunity_choices.*.portfolio_origins.*' => 'uuid',
|
||||
'opportunity_choices.*.corridor_key' => ['sometimes', 'nullable', Rule::in(array_keys(config('investment_corridors')))],
|
||||
'opportunity_choices.*.activities' => 'sometimes|array', 'opportunity_choices.*.activities.*' => 'string|distinct|in:production,processing,trade,transport,water,forestry,animal',
|
||||
'opportunity_choices.*.park' => 'sometimes|nullable|string|max:100',
|
||||
'opportunity_choices.*.source' => 'required|string|distinct|max:100', 'opportunity_choices.*.territory' => 'nullable|string|max:100',
|
||||
'opportunity_choices.*.location_reason' => 'nullable|string|max:2000', 'opportunity_choices.*.confirm_location_change' => 'sometimes|boolean',
|
||||
'version' => 'required|integer|min:1', 'selection' => 'present|array:portfolio,opportunity,sector,territory,park',
|
||||
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary,currency,expected_start_date,investor_origin,person_type,entity_stage,tax_residency_country,fiscal_residency_country,project_type,maturity_stage,'.implode(',', PrivateInvestmentPreparation::FIELDS), 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
|
||||
'declarations.currency' => 'nullable|regex:/^[A-Z]{3}$/', 'declarations.expected_start_date' => 'nullable|date_format:Y-m-d', 'declarations.investor_origin' => 'nullable|in:national,foreign,mixed', 'declarations.person_type' => 'nullable|in:singular,collective,sole_trader,consortium', 'declarations.entity_stage' => 'nullable|in:existing,being_formed', 'declarations.tax_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.fiscal_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.project_type' => 'nullable|string|max:100', 'declarations.maturity_stage' => 'nullable|string|max:100',
|
||||
'replace_fields' => 'nullable|array', 'replace_fields.*' => 'string|in:'.implode(',', self::FIELDS)])->validate();
|
||||
}
|
||||
@@ -283,7 +381,41 @@ final class InvestorContext
|
||||
abort_unless((int) $case->version === $data['version'], 409, 'O dossier mudou; recupere a versão atual.');
|
||||
app(InvestorPath::class)->view($actor, $id);
|
||||
$old = $this->state($case);
|
||||
$complements = array_key_exists('complements', $data) ? app(InvestmentComplements::class)->resolve($data['complements'], $old['complements'] ?? []) : ($old['complements'] ?? []);
|
||||
$links = $old['preparation_documents'] ?? [];
|
||||
foreach ($data['document_links'] ?? [] as $key => $document) {
|
||||
if ($document) {
|
||||
abort_unless(DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->where('d.id', $document)->whereNull('d.deleted_at')->exists(), 422, 'Seleccione um documento já associado a este dossier; os dados anteriores foram preservados.');
|
||||
$links[$key] = $document;
|
||||
} else {
|
||||
unset($links[$key]);
|
||||
}
|
||||
}
|
||||
$choices = $old['opportunity_choices'] ?? [];
|
||||
if (! array_key_exists('opportunity_choices', $data) && array_key_exists('territory', $data['selection'])) {
|
||||
$source = $data['selection']['opportunity'] ?? ($old['selection']['opportunity']['key'] ?? null);
|
||||
if ($source && str_starts_with($source, 'opportunity:')) {
|
||||
$documented = Opportunity::findOrFail(substr($source, 12))->primary_territory_id;
|
||||
abort_if($documented && $data['selection']['territory'] !== 'territory:'.$documented, 422,
|
||||
'Confirme a alternativa territorial por ficha e indique o motivo; a localização documentada foi preservada.');
|
||||
}
|
||||
}
|
||||
if (array_key_exists('opportunity_choices', $data)) {
|
||||
$choices = app(InvestmentOpportunityChoices::class)->resolve($data['opportunity_choices'], $choices);
|
||||
$data['selection']['opportunity'] = $choices[0]['opportunity']['key'] ?? null;
|
||||
// Shared fields concern the first identified opportunity; other sites remain separate.
|
||||
$data['selection']['territory'] = $choices[0]['territory']['key'] ?? null;
|
||||
if (array_key_exists('park', $data['opportunity_choices'][0] ?? [])) $data['selection']['park'] = $choices[0]['park']['key'] ?? null;
|
||||
} elseif (array_key_exists('opportunity', $data['selection']) && $choices !== []) {
|
||||
abort(422, 'Actualize a selecção identificada de oportunidades; não substitua silenciosamente o conjunto por uma ficha.');
|
||||
}
|
||||
$selection = $old['selection'];
|
||||
foreach ($complements as $complement) {
|
||||
if (($complement['availability'] ?? '') === 'review_required') continue;
|
||||
foreach ([$complement['origin']['key'], $complement['target']['key'] ?? ''] as $key) {
|
||||
if (str_starts_with($key, 'opportunity:')) abort_unless(in_array($key, array_column(array_column($choices, 'opportunity'), 'key'), true), 422, 'Conserve a oportunidade escolhida ou retire explicitamente a sua relação complementar.');
|
||||
}
|
||||
}
|
||||
foreach ($data['selection'] as $slot => $key) {
|
||||
if ($key === null || $key === '') {
|
||||
unset($selection[$slot]);
|
||||
@@ -295,6 +427,16 @@ final class InvestorContext
|
||||
abort_unless(in_array($ref['kind'], $expected, true), 422, 'Referência incompatível com a dimensão.');
|
||||
$selection[$slot] = $ref;
|
||||
}
|
||||
$corridor = $choices[0]['corridor_key'] ?? (isset($selection['opportunity']) && $selection['opportunity']['kind'] === 'opportunity'
|
||||
? app(InvestmentCorridorContext::class)->key(Opportunity::findOrFail($selection['opportunity']['id'])) : null);
|
||||
if ($corridor && isset($selection['territory'])) abort_unless(app(InvestmentCorridorContext::class)->compatible($corridor, $selection['territory']['id']), 422, 'A localização não pertence ao corredor escolhido.');
|
||||
if ($corridor && isset($selection['park'])) {
|
||||
$eligible = array_column(app(InvestmentCorridorContext::class)->parks($corridor, $selection['territory']['id'] ?? null), 'id');
|
||||
if (!in_array($selection['park']['id'], $eligible, true)) {
|
||||
abort_if(!empty($data['selection']['park']), 422, 'A zona ou parque não tem enquadramento compatível documentado nesta localização.');
|
||||
unset($selection['park']);
|
||||
}
|
||||
}
|
||||
// A new opportunity may propose its explicitly stored sector; it never matches names.
|
||||
if (isset($data['selection']['opportunity']) && ! array_key_exists('sector', $data['selection']) && isset($selection['opportunity']['fields']['sector_id'])) {
|
||||
if (Sector::where('is_active', true)->whereKey($selection['opportunity']['fields']['sector_id'])->exists()) {
|
||||
@@ -303,7 +445,7 @@ final class InvestorContext
|
||||
}
|
||||
$proposed = [];
|
||||
$origins = [];
|
||||
foreach (['opportunity', 'sector', 'park', 'territory'] as $slot) {
|
||||
foreach (['portfolio', 'opportunity', 'sector', 'park', 'territory'] as $slot) {
|
||||
if (in_array($slot, ['park', 'territory'], true) && array_intersect(array_keys($selection[$slot]['fields'] ?? []), array_keys(InvestmentGeography::FIELDS))) {
|
||||
foreach (['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location'] as $key) {
|
||||
unset($proposed[$key],$origins[$key]);
|
||||
@@ -339,31 +481,61 @@ final class InvestorContext
|
||||
unset($fields[$key],$provenance[$key]);
|
||||
}
|
||||
}
|
||||
if ($corridor) {
|
||||
foreach (['province_id','district_id','admin_post_id','locality_id','province','district','location'] as $key) {
|
||||
if (isset($selection['territory']['fields'][$key])) {
|
||||
$fields[$key]=(string)$selection['territory']['fields'][$key];
|
||||
$provenance[$key]=Arr::except($selection['territory'],['fields']);
|
||||
} else unset($fields[$key],$provenance[$key]);
|
||||
}
|
||||
$fields['special_zone_id']=$selection['park']['id']??'';
|
||||
$fields['investment_regime']=$selection['park']['fields']['investment_regime']??'general';
|
||||
}
|
||||
$replaced = [];
|
||||
foreach (array_unique([...array_keys($old['fields']), ...array_keys($fields)]) as $key) {
|
||||
if (($old['fields'][$key] ?? null) !== ($fields[$key] ?? null)) {
|
||||
$replaced[] = ['field' => $key, 'before' => $old['fields'][$key] ?? null, 'after' => $fields[$key] ?? null];
|
||||
}
|
||||
}
|
||||
foreach (['opportunity', 'sector', 'territory', 'park'] as $slot) {
|
||||
foreach (['portfolio', 'opportunity', 'sector', 'territory', 'park'] as $slot) {
|
||||
if (($old['selection'][$slot] ?? null) !== ($selection[$slot] ?? null)) {
|
||||
$changed[] = $slot;
|
||||
}
|
||||
}
|
||||
$changedFields = array_column($replaced, 'field');
|
||||
if ($complements !== ($old['complements'] ?? [])) $changed[] = 'complements';
|
||||
$choicesChanged = $choices !== ($old['opportunity_choices'] ?? []);
|
||||
$choiceInputsChanged = array_map(fn($c)=>Arr::except($c,['portfolio_origins']),$choices) !== array_map(fn($c)=>Arr::except($c,['portfolio_origins']),$old['opportunity_choices']??[]);
|
||||
if ($choiceInputsChanged) $changed = array_values(array_unique([...$changed, 'opportunity', 'territory']));
|
||||
elseif ($choicesChanged) $changed[] = 'portfolio_origins';
|
||||
$conditionsChanged = (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary']));
|
||||
$dependencyChanged = $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || $conditionsChanged;
|
||||
$dependencyChanged = $choiceInputsChanged || in_array('complements', $changed, true) || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || $conditionsChanged;
|
||||
$privateConditionsChanged = (bool) array_intersect($changedFields, PrivateInvestmentPreparation::FIELDS);
|
||||
$calculationInputsChanged = $choiceInputsChanged || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]));
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
$results = $payload['investor_path']['results'] ?? [];
|
||||
$requests = DB::table('desk_requests')->where('case_id', $id)->orderBy('id')->get(['id', 'status', 'protocol', 'version'])->all();
|
||||
$steps = $dependencyChanged ? array_values(array_unique([...(array_intersect($changed, ['opportunity', 'sector']) ? ['S02', 'S03', 'S04', 'S08', 'S12', 'S13', 'S14', 'S15', 'S17'] : []), ...(array_intersect($changed, ['territory', 'park']) ? ['S08', 'S09', 'S10', 'S11', 'S17'] : [])])) : [];
|
||||
if ($conditionsChanged) {
|
||||
if (array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...PrivateInvestmentPreparation::FIELDS]))) {
|
||||
$steps = array_values(array_unique([...$steps, 'S02', 'S03', 'S08', 'S12', 'S13', 'S17']));
|
||||
}
|
||||
$plan = ['case_id' => $id, 'version' => (int) $case->version, 'revision' => (int) $old['revision'], 'selection' => $selection, 'fields' => $fields, 'provenance' => $provenance,
|
||||
'updated' => $replaced, 'maintained' => $kept, 'changed_dimensions' => $changed, 'dependent_results' => (int) $case->user_id === $actor->id && $dependencyChanged ? array_map(fn ($r) => ['id' => $r['id'], 'engine' => $r['engine']], $results) : [],
|
||||
'requests' => $requests, 'review_steps' => $steps, 'dependency_changed' => $dependencyChanged, 'documents_preserved' => DB::table('desk_documents')->where('case_id', $id)->count(),
|
||||
if ($privateConditionsChanged) {
|
||||
$steps = array_values(array_unique([...$steps, 'S02', 'S05', 'S06', 'S07']));
|
||||
}
|
||||
if (in_array('complements', $changed, true)) $steps = array_values(array_unique([...$steps, 'S02', 'S08']));
|
||||
$privatePreparation = null;
|
||||
if (app(PrivateInvestmentPreparation::class)->available()) {
|
||||
$docs = $this->preparationDocuments($id);
|
||||
$beforePreparation = app(PrivateInvestmentPreparation::class)->assess($old, $docs);
|
||||
$afterPreparation = app(PrivateInvestmentPreparation::class)->assess(['fields' => $fields, 'selection' => $selection, 'preparation_documents' => $links], $docs);
|
||||
$privatePreparation = ['before' => $beforePreparation, 'after' => $afterPreparation];
|
||||
}
|
||||
$plan = ['complements' => $complements, 'opportunity_choices' => $choices, 'case_id' => $id, 'version' => (int) $case->version, 'revision' => (int) $old['revision'], 'selection' => $selection, 'fields' => $fields, 'provenance' => $provenance,
|
||||
'preparation_documents' => $links, 'private_investment' => $privatePreparation,
|
||||
'updated' => $replaced, 'maintained' => $kept, 'changed_dimensions' => $changed, 'dependent_results' => (int) $case->user_id === $actor->id && $calculationInputsChanged ? array_map(fn ($r) => ['id' => $r['id'], 'engine' => $r['engine']], $results) : [],
|
||||
'requests' => $requests, 'review_steps' => $steps, 'dependency_changed' => $dependencyChanged, 'calculation_inputs_changed' => $calculationInputsChanged, 'documents_preserved' => DB::table('desk_documents')->where('case_id', $id)->count(),
|
||||
'notice' => 'Não confirma incentivos, licenças, disponibilidade de terra, reserva ou autorização. Submissões e decisões anteriores mantêm o conteúdo enviado.'];
|
||||
$plan['requirements_assessment'] = app(InvestmentContextRequirements::class)->assess(['opportunity_choices' => $choices, 'fields' => $fields]);
|
||||
$hash = hash_hmac('sha256', json_encode(Arr::sortRecursive([$actor->id, $data, $plan]), JSON_THROW_ON_ERROR), config('app.key'));
|
||||
|
||||
return [...$plan, 'preview_hash' => $hash];
|
||||
@@ -390,7 +562,7 @@ final class InvestorContext
|
||||
$before = $this->state($case);
|
||||
$plan = $this->preview($actor, $id, $input);
|
||||
abort_unless(hash_equals($plan['preview_hash'], $control['preview_hash']), 409, 'O dossier ou uma fonte mudou desde a prévia; compare novamente.');
|
||||
if (! $plan['changed_dimensions'] && ! $plan['updated'] && empty($input['declarations']) && empty($input['replace_fields'])) {
|
||||
if (! $plan['changed_dimensions'] && ! $plan['updated'] && empty($input['declarations']) && empty($input['replace_fields']) && empty($input['document_links'])) {
|
||||
$result = ['case_id' => $id, 'version' => (int) $case->version, 'revision' => $before['revision'], 'changed' => false, 'replayed' => false];
|
||||
DB::table('audit_events')->insert(['source_system' => self::SOURCE, 'entity_type' => 'desk_cases', 'entity_id' => $id, 'actor_user_id' => $actor->id, 'correlation_id' => $control['operation_key'], 'action' => 'context_unchanged', 'data_classification' => 'restricted', 'after_data' => json_encode(['case_id' => $id, 'request_hash' => $requestHash, 'result' => $result], JSON_THROW_ON_ERROR)]);
|
||||
|
||||
@@ -402,14 +574,17 @@ final class InvestorContext
|
||||
foreach ($plan['review_steps'] as $step) {
|
||||
$review[$step] = ['revision' => $revision, 'reason' => 'O contexto mudou; reveja os pressupostos e a evidência aplicável.'];
|
||||
}
|
||||
$context = ['revision' => $revision, 'selection' => $plan['selection'], 'fields' => $plan['fields'], 'provenance' => $plan['provenance'], 'review_steps' => $review,
|
||||
'dependency_hash' => $this->dependencyHash(['selection' => $plan['selection'], 'fields' => Arr::except($plan['fields'], ['project_title', 'project_summary'])]), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
|
||||
if ($plan['dependency_changed']) {
|
||||
$context = ['opportunity_choices' => $plan['opportunity_choices'], 'preparation_documents' => $plan['preparation_documents'], 'preparation_snapshot' => $plan['private_investment']['after'] ?? $before['preparation_snapshot'] ?? null, 'revision' => $revision, 'selection' => $plan['selection'], 'fields' => $plan['fields'], 'provenance' => $plan['provenance'], 'review_steps' => $review,
|
||||
'dependency_hash' => $this->dependencyHash([...($plan['opportunity_choices'] !== [] ? ['opportunity_choices' => $plan['opportunity_choices']] : []), ...($plan['complements'] ? ['complements' => $plan['complements']] : []), 'selection' => $plan['selection'], 'fields' => Arr::except($plan['fields'], ['project_title', 'project_summary'])]), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
|
||||
$context['requirements_assessment'] = $plan['requirements_assessment'];
|
||||
$context['complements'] = $plan['complements'];
|
||||
if ($plan['calculation_inputs_changed']) {
|
||||
foreach ($payload['investor_path']['results'] ?? [] as $i => $r) {
|
||||
$payload['investor_path']['results'][$i] = [...$r, 'context_state' => 'review_required', 'context_invalidated_revision' => $revision, 'context_invalidated_at' => now()->toIso8601String()];
|
||||
}
|
||||
}
|
||||
$payload['investor_path']['context'] = $context;
|
||||
unset($payload['investor_path']['context_drafts'][(string)$actor->id]);
|
||||
DB::table('desk_cases')->where('id', $id)->update(['payload' => $this->desk->encode($payload), 'title' => $context['fields']['project_title'] ?? $case->title, 'version' => $case->version + 1, 'updated_at' => now()]);
|
||||
$this->desk->event($this->desk->case($actor, $id), $actor, 'investment_context_changed', ['before' => $before, 'after' => $context, 'affected_result_ids' => array_column($plan['dependent_results'], 'id'), 'preserved_request_ids' => array_column($plan['requests'], 'id')]);
|
||||
$result = ['case_id' => $id, 'version' => (int) $case->version + 1, 'revision' => $revision, 'replayed' => false];
|
||||
@@ -419,12 +594,13 @@ final class InvestorContext
|
||||
}, 3);
|
||||
}
|
||||
|
||||
public function view(User $actor, string $id): array
|
||||
public function view(User $actor, string $id, ?string $opportunity = null): array
|
||||
{
|
||||
$case = $this->desk->case($actor, $id);
|
||||
abort_unless($this->desk->owns($actor, $case, false) || $actor->can('manage-portal'), 403);
|
||||
$ctx = $this->state($case);
|
||||
$path = app(InvestorPath::class)->view($actor, $id);
|
||||
$ctx['land_preparation'] = $this->landPreparation($case, $opportunity);
|
||||
$path = app(InvestorPath::class)->view($actor, $id, $opportunity);
|
||||
$ctx['path_applicability'] = ['definition_id' => $path['definition_id'], 'definition_version' => $path['definition_version'], 'source_sha256' => $path['source_sha256'],
|
||||
'steps' => array_values(array_map(fn ($step) => Arr::only($step, ['code', 'title', 'applicability', 'reason', 'state', 'context_review_required']), array_filter($path['steps'], fn ($step) => ! empty($step['branch']))))];
|
||||
|
||||
@@ -439,25 +615,77 @@ final class InvestorContext
|
||||
}
|
||||
}
|
||||
|
||||
return ['simulator_context_revision' => $simulatorRevision, 'case' => $path['case'], 'context' => $ctx, 'steps' => array_map(fn ($s) => Arr::only($s, ['code', 'title', 'state', 'context_review_required']), $path['steps']),
|
||||
$preparation = app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess(app(InvestmentConsumerScope::class)->context($ctx, $opportunity), $this->preparationDocuments($id)) : null;
|
||||
$ctx['preparation_assessment'] = $preparation;
|
||||
|
||||
$prefill = $this->prefill($ctx, $opportunity);
|
||||
$saved = $this->desk->decode($case->payload);
|
||||
if (!empty($saved['reuse_consent'])) {
|
||||
$contacts = array_filter(['email' => $saved['email'] ?? null, 'phone' => $saved['profile']['phone'] ?? null], fn($v) => is_string($v) && $v !== '');
|
||||
$prefill['simulator'] = [...$contacts, ...$prefill['simulator']];
|
||||
$prefill['land'] = [...$contacts, ...(!empty($saved['name']) ? ['full_name' => $saved['name']] : []), ...$prefill['land']];
|
||||
}
|
||||
return ['private_investment' => $preparation, 'preparation_documents' => $this->preparationDocuments($id), 'simulator_context_revision' => $simulatorRevision, 'case' => $path['case'], 'context' => $ctx, 'steps' => array_map(fn ($s) => Arr::only($s, ['code', 'title', 'state', 'context_review_required']), $path['steps']),
|
||||
'can_write' => $path['canWrite'], 'next' => $path['next'], 'results' => $path['results'], 'requests' => $path['requests'],
|
||||
'prefill' => $this->prefill($ctx), 'notice' => 'A seleção apoia a preparação e não confirma benefícios, licenças, direitos ou disponibilidade.'];
|
||||
'prefill' => $prefill, 'notice' => 'A seleção apoia a preparação e não confirma benefícios, licenças, direitos ou disponibilidade.'];
|
||||
}
|
||||
|
||||
public function prefill(array $ctx): array
|
||||
public function preparationDocuments(string $id): array
|
||||
{
|
||||
return DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->whereNull('d.deleted_at')->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.version_no', 'd.sha256'])->map(fn ($row) => (array) $row)->all();
|
||||
}
|
||||
|
||||
public function landPreparation(\stdClass $case, ?string $opportunity = null): array
|
||||
{
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
$ctx = $this->state($case);
|
||||
$key = app(InvestmentConsumerScope::class)->key($ctx, $opportunity);
|
||||
$path = $payload['investor_path'] ?? [];
|
||||
$need = app(InvestmentConsumerScope::class)->facts($payload, $ctx, $key)['land_needed'];
|
||||
|
||||
return ['need' => in_array($need, ['yes', 'no'], true) ? $need : 'unknown', 'reason' => $key ? ($path['opportunity_fact_reasons'][$key]['land_needed'] ?? '') : ($path['fact_reasons']['land_needed'] ?? '')];
|
||||
}
|
||||
|
||||
public function prefill(array $ctx, ?string $opportunity = null): array
|
||||
{
|
||||
$original = $ctx;
|
||||
$ctx = app(InvestmentConsumerScope::class)->context($ctx, $opportunity);
|
||||
$f = $ctx['fields'];
|
||||
$sim = Arr::except(Arr::only($f, self::FIELDS), ['sector', 'province', 'district', 'location']);
|
||||
$sim = Arr::except(Arr::only($f, self::FIELDS), ['sector', 'province', 'district', 'location', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]);
|
||||
// Entering this tool from an investment dossier selects the existing investment-analysis route.
|
||||
// The reconciliation preserves any purpose deliberately chosen in an existing simulator draft.
|
||||
$sim += ['journey_type' => 'investor', 'journey_type_label' => 'Avaliar o meu investimento', 'purpose' => 'simulate_only', 'purpose_label' => 'Apenas simular'];
|
||||
if (($ctx['land_preparation']['need'] ?? null) === 'yes') {
|
||||
$tenure = ['duat' => ['duat', 'DUAT'], 'lease' => ['leased', 'Arrendado']][$f['site_tenure'] ?? ''] ?? null;
|
||||
if ($tenure) [$sim['land_status'], $sim['land_status_label']] = $tenure;
|
||||
}
|
||||
// Human labels travel with the exact existing identifiers, not inferred locations.
|
||||
foreach (app(InvestmentGeography::class)->publicReferences()->whereIn('id', array_values(Arr::only($f, array_keys(InvestmentGeography::FIELDS))))->get(['id', 'name']) as $place) {
|
||||
foreach (array_keys(InvestmentGeography::FIELDS) as $key) {
|
||||
if (($f[$key] ?? null) === $place->id) $sim[$key.'_label'] = $place->name;
|
||||
}
|
||||
}
|
||||
if (! empty($f['sector_id']) && ($sector = Sector::where('is_active', true)->find($f['sector_id']))) $sim['sector_id_label'] = $sector->name;
|
||||
$zone = $ctx['selection']['park'] ?? null;
|
||||
$sim['context_applicability_snapshot'] = json_encode([
|
||||
'version' => 1, 'context_revision' => $ctx['revision'],
|
||||
'consumer_opportunity' => $ctx['consumer_opportunity']['opportunity']['key'] ?? null,
|
||||
'portfolio_origin' => $ctx['selection']['portfolio'] ?? null,
|
||||
'zone' => $zone ? Arr::only($zone, ['id', 'title', 'href', 'version', 'sha256', 'fields', 'analysis']) : null,
|
||||
'provenance' => $ctx['provenance'],
|
||||
'opportunity_choices' => $ctx['opportunity_choices'] ?? [],
|
||||
'complements' => $ctx['complements'] ?? [],
|
||||
'requirements_assessment' => $ctx['requirements_assessment'] ?? null,
|
||||
'path' => $ctx['path_applicability'] ?? null,
|
||||
'land_preparation' => $ctx['land_preparation'] ?? null,
|
||||
'field_provenance' => array_map(fn($origin) => Arr::only($origin, ['kind', 'title', 'version', 'context_revision']), $ctx['provenance'] ?? []),
|
||||
'preparation' => isset($ctx['preparation_assessment']) ? Arr::except($ctx['preparation_assessment'], ['questions']) : null,
|
||||
'fields' => Arr::only($f, ['investor_origin', 'tax_residency_country', 'entity_stage', 'currency', 'expected_start_date', 'investment_regime']),
|
||||
'notice' => 'Referências e declarações para preparação; sem reconhecimento automático de elegibilidade ou benefício.',
|
||||
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$scoped = app(InvestmentConsumerScope::class)->context($original, $opportunity);
|
||||
$f = $scoped['fields'];
|
||||
return ['simulator' => $sim, 'land' => array_filter(['project_reference' => $f['project_title'] ?? null, 'activity' => $f['sector'] ?? null, 'purpose' => $f['project_summary'] ?? null, 'parcels' => [Arr::only($f, array_keys(InvestmentGeography::FIELDS))]], fn ($v) => $v !== null),
|
||||
'desk' => array_filter(['title' => $f['project_title'] ?? null, 'territory_id' => $f['locality_id'] ?? $f['admin_post_id'] ?? $f['district_id'] ?? $f['province_id'] ?? null, 'location_source' => isset($f['province_id']) ? 'Referência consultada no contexto v'.($ctx['revision'] ?? 0).': '.($f['location'] ?? 'território identificado').'. Não confirma disponibilidade.' : null, 'description' => $f['project_summary'] ?? null, 'sector' => $f['sector'] ?? null, ...Arr::only($f, array_keys(InvestmentGeography::FIELDS))], fn ($v) => $v !== null),
|
||||
'submission' => array_filter(['currency' => $f['currency'] ?? null, 'sector' => $f['sector'] ?? null, 'objective' => $f['project_summary'] ?? null, 'location' => $f['location'] ?? null], fn ($v) => $v !== null)];
|
||||
|
||||
@@ -68,7 +68,7 @@ final class InvestorPath
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function view(User $actor, string $id): array
|
||||
public function view(User $actor, string $id, ?string $opportunity = null): array
|
||||
{
|
||||
abort_unless($this->available(), 404);
|
||||
$case = $this->desk->case($actor, $id);
|
||||
@@ -79,18 +79,26 @@ final class InvestorPath
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
$path = $payload['investor_path'] ?? [];
|
||||
$context = app(InvestorContext::class)->state($case);
|
||||
$facts = $path['facts'] ?? ['land_needed' => 'unknown', 'external_funding' => 'unknown', 'viability' => 'unknown'];
|
||||
$scope = app(InvestmentConsumerScope::class);
|
||||
$opportunity = $scope->key($context, $opportunity);
|
||||
$allChoices = $context['opportunity_choices'] ?? [];
|
||||
$facts = $scope->facts($payload, $context, $opportunity);
|
||||
$factReasons = $opportunity ? ($path['opportunity_fact_reasons'][$opportunity] ?? []) : ($path['fact_reasons'] ?? []);
|
||||
$context = $scope->context($context, $opportunity);
|
||||
$records = DB::table('journey_steps')->where('journey_id', $journey->id)->whereNull('deleted_at')->get()->keyBy('step_code');
|
||||
$steps = [];
|
||||
foreach ($definition['steps'] as $step) {
|
||||
$row = $records->get($step['code']);
|
||||
abort_unless($row !== null, 409, 'Etapa em falta; reparar a instalação sem reconstruir o histórico.');
|
||||
$detail = $this->desk->decode($row->path_detail);
|
||||
if ($opportunity && (count($allChoices) > 1 || isset($detail['opportunities'][$opportunity]))) {
|
||||
$detail = $detail['opportunities'][$opportunity] ?? ['state' => 'not_started'];
|
||||
}
|
||||
$value = $step['branch'] ? ($facts[$step['branch']] ?? 'unknown') : 'yes';
|
||||
$applicable = $value === 'unknown' ? 'undetermined' : ($value === 'no' ? 'not_needed_for_preparation' : 'applicable');
|
||||
$state = $value === 'no' ? 'dispensed' : $detail['state'];
|
||||
$steps[] = [...$step, 'id' => $row->id, 'version' => $row->version_no, 'state' => $state, 'saved_state' => $detail['state'],
|
||||
'applicability' => $applicable, 'reason' => $value === 'no' ? ($path['fact_reasons'][$step['branch']] ?? '') : ($detail['reason'] ?? ''),
|
||||
'applicability' => $applicable, 'reason' => $value === 'no' ? ($factReasons[$step['branch']] ?? '') : ($detail['reason'] ?? ''),
|
||||
'detail' => $detail, 'preparation_allowed' => $value !== 'no',
|
||||
'action_gaps' => []];
|
||||
}
|
||||
@@ -136,22 +144,29 @@ final class InvestorPath
|
||||
$documents = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')
|
||||
->where('l.case_id', $id)->whereNull('d.deleted_at')
|
||||
->when(! $owner && ! $manager, fn ($q) => $q->where(fn ($p) => $p->whereNull('l.request_id')->orWhereIn('l.request_id', $requests->pluck('id'))))
|
||||
->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status']);
|
||||
->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.version_no', 'd.sha256']);
|
||||
// Only the private draft owner can receive the financial snapshot/result. Dossier access alone is insufficient.
|
||||
$results = (int) $case->user_id === $actor->id ? ($path['results'] ?? []) : [];
|
||||
$next = collect($steps)->first(fn ($step) => $step['preparation_allowed'] && ! in_array($step['state'], ['completed', 'approved', 'waiting_external', 'submitted', 'reviewing'], true));
|
||||
|
||||
$landCase = (int) $case->user_id === $actor->id
|
||||
? LandAssistanceCase::where('user_id', $actor->id)->latest('updated_at')->get()
|
||||
->first(fn ($land) => ($land->payload['investor_path_case_id'] ?? null) === $id) : null;
|
||||
->first(fn ($land) => ($land->payload['investor_path_case_id'] ?? null) === $id
|
||||
&& (!$allChoices || ($opportunity && ($land->payload['context_opportunity'] ?? null) === $opportunity))) : null;
|
||||
|
||||
return ['case' => ['id' => $id, 'title' => $case->title, 'version' => $case->version, 'closed' => $case->closed_at !== null],
|
||||
$preparation = ($owner || $manager) && app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess($context, $documents->map(fn ($d) => (array) $d)->all()) : null;
|
||||
if ($preparation && $preparation['enabled']) {
|
||||
$next = ['code' => 'S06', 'title' => $preparation['next'], 'action' => 'Preparar', 'href' => '/investimentos/'.$id.'/contexto/editar#preparation-questions'];
|
||||
}
|
||||
|
||||
return ['private_investment' => $preparation, 'case' => ['id' => $id, 'title' => $case->title, 'version' => $case->version, 'closed' => $case->closed_at !== null],
|
||||
'journey_id' => $journey->id, 'definition_id' => $version->id, 'definition_version' => $version->version_no, 'source_sha256' => $definition['source_sha256'],
|
||||
'phases' => $definition['phases'], 'steps' => $steps, 'facts' => $facts, 'fact_reasons' => $path['fact_reasons'] ?? [],
|
||||
'phases' => $definition['phases'], 'steps' => $steps, 'facts' => $facts, 'fact_reasons' => $factReasons,
|
||||
'context_opportunity' => $opportunity, 'opportunity_options' => $allChoices,
|
||||
'fintech' => ($owner || $manager) && (isset($path['fintech']) || collect($path['sector_contexts'] ?? [])->contains(fn ($s) => in_array($s['slug'], ['fintech', 'tecnologias-de-informacao', 'mercado-financeiro'], true)) || collect($path['content_sources'] ?? [])->contains('return_href', '/tech-fintech')) ? app(FintechInvestment::class)->view($payload, $steps) : null,
|
||||
'canWrite' => $owner && ! $case->closed_at, 'canReview' => $manager && (int) $case->user_id !== $actor->id && ! $case->closed_at,
|
||||
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => '#'.$next['code']] : null,
|
||||
'investment_context' => ($owner || $manager) ? $context : null,
|
||||
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => $next['href'] ?? '#'.$next['code']] : null,
|
||||
'investment_context' => ($owner || $manager) ? [...$context, 'opportunity_choices' => $opportunity ? $context['opportunity_choices'] : $allChoices] : null,
|
||||
'results' => $results, 'requests' => $requests, 'documents' => $documents,
|
||||
'zone_contexts' => ($owner || $manager) ? ($path['zone_contexts'] ?? []) : [],
|
||||
'sector_contexts' => ($owner || $manager) ? ($path['sector_contexts'] ?? []) : [],
|
||||
@@ -162,25 +177,31 @@ final class InvestorPath
|
||||
->get(['id', 'title', 'status', 'version_no', 'due_at', 'assignee_user_id']),
|
||||
'services' => DB::table('desk_service_versions')->where('status', 'published')->orderByDesc('version')->get(['id', 'code', 'definition'])
|
||||
->unique('code')->values()->map(fn ($s) => ['id' => $s->id, 'title' => json_decode($s->definition, true)['title']]),
|
||||
'tools' => ['simulator' => (int) $case->user_id === $actor->id ? '/simulador-fiscal?context='.$id.($case->simulation_id ? '&draft='.$case->simulation_id : '') : null,
|
||||
'desk' => '/balcao-virtual/dossiers/'.$id, 'guidance' => '/balcao-virtual/dossiers/'.$id.'/orientacao',
|
||||
'tools' => ['simulator' => (int) $case->user_id === $actor->id ? '/simulador-fiscal?context='.$id.($case->simulation_id ? '&draft='.$case->simulation_id : '').($opportunity ? '&opportunity='.urlencode($opportunity) : '') : null,
|
||||
'desk' => '/balcao-virtual/dossiers/'.$id.($opportunity ? '?opportunity='.urlencode($opportunity) : ''), 'guidance' => '/balcao-virtual/dossiers/'.$id.'/orientacao',
|
||||
'library' => '/documentos?context='.$id, 'opportunities' => '/oportunidades?context='.$id, 'territory' => '/zonas?context='.$id, 'partners' => '/parcerias/painel?context='.$id,
|
||||
'land' => (int) $case->user_id === $actor->id ? ($landCase ? '/investidor/terra-duat/'.$landCase->id : '/investidor/terra-duat/novo?context='.$id) : null]];
|
||||
'land' => ($facts['land_needed'] ?? 'unknown') !== 'no' && (int) $case->user_id === $actor->id ? ($landCase ? '/investidor/terra-duat/'.$landCase->id : '/investidor/terra-duat/novo?context='.$id.($opportunity ? '&opportunity='.urlencode($opportunity) : '')) : null]];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function facts(User $actor, string $id, array $input): void
|
||||
{
|
||||
$data = Validator::make($input, ['version' => 'required|integer|min:1',
|
||||
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'context_opportunity' => 'nullable|string|max:100',
|
||||
'land_needed' => ['required', Rule::in(['yes', 'no', 'unknown'])], 'external_funding' => ['required', Rule::in(['yes', 'no', 'unknown'])],
|
||||
'viability' => ['required', Rule::in(['unknown', 'proceed', 'reformulate', 'archive'])],
|
||||
'reason' => 'required|string|min:3|max:3000'])->validate();
|
||||
DB::transaction(function () use ($actor, $id, $data): void {
|
||||
$case = $this->writable($actor, $id, $data['version']);
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
$choice = app(InvestmentConsumerScope::class)->key(app(InvestorContext::class)->state($case), $data['context_opportunity'] ?? null, true);
|
||||
foreach (['land_needed', 'external_funding', 'viability'] as $key) {
|
||||
$payload['investor_path']['facts'][$key] = $data[$key];
|
||||
$payload['investor_path']['fact_reasons'][$key] = $data['reason'];
|
||||
if ($choice) {
|
||||
$payload['investor_path']['opportunity_facts'][$choice][$key] = $data[$key];
|
||||
$payload['investor_path']['opportunity_fact_reasons'][$choice][$key] = $data['reason'];
|
||||
} else {
|
||||
$payload['investor_path']['facts'][$key] = $data[$key];
|
||||
$payload['investor_path']['fact_reasons'][$key] = $data['reason'];
|
||||
}
|
||||
}
|
||||
$this->savePayload($case, $payload);
|
||||
$this->desk->event($this->desk->case($actor, $id), $actor, 'path_applicability_declared', $data);
|
||||
@@ -190,7 +211,7 @@ final class InvestorPath
|
||||
/** @param array<string,mixed> $input */
|
||||
public function stage(User $actor, string $id, string $code, array $input): void
|
||||
{
|
||||
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'state' => ['required', Rule::in(array_diff(InvestorPathDefinition::STATES, ['dispensed']))],
|
||||
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'context_opportunity' => 'nullable|string|max:100', 'state' => ['required', Rule::in(array_diff(InvestorPathDefinition::STATES, ['dispensed']))],
|
||||
'reason' => 'required|string|min:3|max:3000', 'action' => ['required', Rule::in(['prepare', 'submit', 'execute', 'operate', 'review', 'record_external'])],
|
||||
'document_id' => 'nullable|uuid', 'authority' => 'nullable|string|max:200', 'reference' => 'nullable|string|max:300',
|
||||
'context_revision' => 'nullable|integer|min:0', 'context_reviewed' => 'nullable|boolean', 'decision_date' => 'nullable|date_format:Y-m-d|before_or_equal:today'])->validate();
|
||||
@@ -198,7 +219,8 @@ final class InvestorPath
|
||||
$case = $this->desk->case($actor, $id, false, true);
|
||||
abort_if($case->closed_at, 409);
|
||||
abort_unless((int) $case->version === $data['version'], 409, 'O dossier mudou; recupere a versão actual.');
|
||||
$view = $this->view($actor, $id);
|
||||
$key = app(InvestmentConsumerScope::class)->key(app(InvestorContext::class)->state($case), $data['context_opportunity'] ?? null, true);
|
||||
$view = $this->view($actor, $id, $key);
|
||||
$step = $this->findStep($view, $code);
|
||||
abort_unless($step !== null && $step['preparation_allowed'], 422, 'Etapa dispensada de preparação; altere os pressupostos com fundamento.');
|
||||
$state = $data['state'];
|
||||
@@ -233,7 +255,10 @@ final class InvestorPath
|
||||
}
|
||||
abort_if($state === 'completed' && ($step['context_review_required'] ?? false) && (! (bool) ($data['context_reviewed'] ?? false) || ($data['context_revision'] ?? -1) !== ($view['investment_context']['revision'] ?? 0)), 422, 'O contexto desta etapa exige nova conferência fundamentada; o registo anterior permanece no histórico.');
|
||||
$detail = [...$data, 'context_dependency_hash' => $view['investment_context']['dependency_hash'] ?? null, 'fintech_fingerprint' => $view['fintech']['review_fingerprints'][$code] ?? null, 'scope' => $scope, 'actor_id' => $actor->id, 'recorded_at' => now()->toIso8601String()];
|
||||
DB::table('journey_steps')->where('id', $step['id'])->update(['path_detail' => $this->desk->encode($detail),
|
||||
$stored = $this->desk->decode(DB::table('journey_steps')->where('id', $step['id'])->value('path_detail'));
|
||||
if ($key) $stored['opportunities'][$key] = $detail;
|
||||
else $stored = [...$stored, ...$detail];
|
||||
DB::table('journey_steps')->where('id', $step['id'])->update(['path_detail' => $this->desk->encode($stored),
|
||||
'status' => in_array($state, ['completed', 'approved'], true) ? 'completed' : ($state === 'not_started' ? 'pending' : 'active'),
|
||||
'version_no' => $step['version'] + 1, 'updated_at' => now()]);
|
||||
DB::table('desk_cases')->where('id', $id)->update(['version' => $case->version + 1, 'updated_at' => now()]);
|
||||
@@ -262,11 +287,12 @@ final class InvestorPath
|
||||
abort_unless(is_array($parsed) && ($parsed['version'] ?? null) === 1 && is_array($parsed['data'] ?? null), 422, 'Snapshot financeiro incompatível.');
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
$context = app(InvestorContext::class)->state($case);
|
||||
$opportunity = app(InvestmentConsumerScope::class)->key($context, $economic['context_opportunity'] ?? null, true);
|
||||
if ($context['revision'] > 0) {
|
||||
abort_unless(($data['context_revision'] ?? -1) === $context['revision'], 409, 'O contexto mudou; recupere e confira os pressupostos antes de devolver o resultado.');
|
||||
abort_unless($data['context_reviewed'] ?? false, 422, 'Confirme a revisão dos pressupostos financeiros para este contexto.');
|
||||
}
|
||||
$resultKey = hash('sha256', $draft->id.':'.$draft->version_no.':'.$snapshot.':context:'.$context['revision']);
|
||||
$resultKey = hash('sha256', $draft->id.':'.$draft->version_no.':'.$snapshot.':context:'.$context['revision'].($opportunity ? ':'.$opportunity : ''));
|
||||
foreach ($payload['investor_path']['results'] ?? [] as $result) {
|
||||
if ($result['source_key'] === $resultKey) {
|
||||
return $result;
|
||||
@@ -275,6 +301,7 @@ final class InvestorPath
|
||||
abort_unless((int) $case->version === $data['version'], 409, 'Recupere a versão actual do dossier.');
|
||||
$result = ['id' => (string) Str::uuid(), 'source_key' => $resultKey, 'draft_id' => $draft->id, 'draft_version' => $draft->version_no,
|
||||
'snapshot_sha256' => $draft->snapshot_sha256, 'financial_snapshot' => $snapshot,
|
||||
'context_opportunity' => $opportunity,
|
||||
'currency' => $economic['currency'] ?? null, 'engine' => 'financial-calculators:v1',
|
||||
'kind' => 'local_calculation_inputs', 'context_state' => 'current', 'context_revision' => $context['revision'], 'context_dependency_hash' => $context['dependency_hash'] ?? app(InvestorContext::class)->dependencyHash([]), 'recorded_at' => now()->toIso8601String()];
|
||||
$payload['investor_path']['results'][] = $result;
|
||||
|
||||
@@ -78,6 +78,9 @@ final class LegacyContent
|
||||
$image = $this->publicResources()->where('r.id', $adaptation['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->first();
|
||||
}
|
||||
}
|
||||
if ($slug === 'agronegocio') {
|
||||
$localHtml = app(CorridorAgribusiness::class)->enrich($localHtml, app(CorridorAgribusinessOpportunities::class)->articleLinks());
|
||||
}
|
||||
preg_match_all('~href="([^"]+)"~i', $localHtml, $existingLinks);
|
||||
$alreadyLinked = array_map(fn ($href) => html_entity_decode($href, ENT_QUOTES | ENT_HTML5, 'UTF-8'), $existingLinks[1]);
|
||||
$relatedArticles = [];
|
||||
@@ -279,7 +282,7 @@ final class LegacyContent
|
||||
}
|
||||
|
||||
/** Allow presentation markup only. No scripts, styles, events, embeds or remote image execution. */
|
||||
public function sanitize(string $html): string
|
||||
public function sanitize(string $html, bool $preserveStructure = false): string
|
||||
{
|
||||
$doc = new DOMDocument;
|
||||
$old = libxml_use_internal_errors(true);
|
||||
@@ -296,7 +299,7 @@ final class LegacyContent
|
||||
}
|
||||
$headingOffset = 2 - (empty($sourceLevels) ? 2 : min($sourceLevels));
|
||||
$previousHeading = 1;
|
||||
$clean = function (DOMNode $node) use (&$clean, $doc, $headingOffset, &$previousHeading): void {
|
||||
$clean = function (DOMNode $node) use (&$clean, $doc, $headingOffset, &$previousHeading, $preserveStructure): void {
|
||||
foreach (iterator_to_array($node->childNodes) as $child) {
|
||||
if (! $child instanceof DOMElement) {
|
||||
continue;
|
||||
@@ -311,7 +314,7 @@ final class LegacyContent
|
||||
$node->replaceChild($section, $child);
|
||||
$child = $section;
|
||||
}
|
||||
if (! in_array(strtolower($child->tagName), ['p', 'strong', 'b', 'em', 'i', 'ul', 'ol', 'li', 'br', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a'], true)) {
|
||||
if (! in_array(strtolower($child->tagName), array_merge(['p', 'strong', 'b', 'em', 'i', 'ul', 'ol', 'li', 'br', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a'], $preserveStructure ? ['div', 'span', 'small', 'section', 'table', 'caption', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'figure', 'figcaption'] : []), true)) {
|
||||
$node->removeChild($child);
|
||||
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
|
||||
/** Read-only grouping of the existing public catalogue. */
|
||||
final class OpportunityCorridorExplorer
|
||||
{
|
||||
public function groups(array $filters): array
|
||||
{
|
||||
$catalogue = app(InvestmentCatalogue::class);
|
||||
$groups = [];
|
||||
foreach ($catalogue->query($filters)->get() as $record) {
|
||||
$context = $catalogue->context($record->id);
|
||||
$classification = $context['classification'] ?? [];
|
||||
$corridors = collect($context['corridors'])->mapWithKeys(fn ($c) => ['corridor:'.$c->id => $c->name])->all();
|
||||
foreach ($classification as $relation) {
|
||||
if ($relation['target_type'] === 'corridor' && ! in_array($relation['purpose'], ['proximity', 'administrative_boundary'], true)) {
|
||||
$corridors['corridor:'.$relation['target_id']] = $relation['label'];
|
||||
}
|
||||
}
|
||||
// The approved source names are not aliases for similarly named master records.
|
||||
$source = data_get($record->workflow_context, 'corridor_agribusiness');
|
||||
$chain = null;
|
||||
if (is_array($source) && ($source['source_sha256'] ?? null) === CorridorAgribusinessOpportunities::SOURCE_HASH) {
|
||||
$corridors['source:'.hash('sha256', $source['corridor'])] = $source['corridor'];
|
||||
$chain = $source['chain'];
|
||||
}
|
||||
$chains = collect($classification)->where('target_type', 'chain')->pluck('label')->all();
|
||||
if ($chain) $chains[] = $chain;
|
||||
$stages = collect($classification)->pluck('production_stage')->filter()->unique()->values()->all();
|
||||
// Keep the source's actual activity description rather than inventing stage labels.
|
||||
if ($stages === [] && $chain && ! empty($source['summary'])) $stages = [$source['summary']];
|
||||
$path = collect($context['primary_territory_path'] ?? []);
|
||||
$province = $path->firstWhere('location_type', 'province')->name ?? $record->province;
|
||||
$district = $path->firstWhere('location_type', 'district')->name ?? $record->district;
|
||||
$item = ['id' => $record->id, 'title' => $record->title, 'slug' => $record->slug, 'summary' => $record->summary,
|
||||
'province' => $province, 'district' => $district, 'chains' => array_values(array_unique($chains)),
|
||||
'stages' => $stages, 'maturity_label' => $record->maturity_label];
|
||||
foreach ($corridors ?: ['other' => 'Outras localizações'] as $key => $name) {
|
||||
$groups[$key] ??= ['id' => $key, 'name' => $name, 'opportunities' => []];
|
||||
$groups[$key]['opportunities'][$record->id] = $item;
|
||||
}
|
||||
}
|
||||
foreach ($groups as &$group) {
|
||||
$group['opportunities'] = array_values($group['opportunities']);
|
||||
$group['count'] = count($group['opportunities']);
|
||||
$group['locations'] = array_values(array_unique(array_filter(array_map(fn ($r) => implode(' / ', array_filter([$r['province'], $r['district']])), $group['opportunities']))));
|
||||
}
|
||||
unset($group);
|
||||
uasort($groups, fn ($a, $b) => ($a['id'] === 'other' ? 1 : 0) <=> ($b['id'] === 'other' ? 1 : 0) ?: strnatcasecmp($a['name'], $b['name']));
|
||||
return array_values($groups);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,9 @@ class OpportunityManagement
|
||||
public function values(Opportunity $record): array
|
||||
{
|
||||
$base = $record->only(self::FIELDS);
|
||||
if (data_get($record->workflow_context, 'publication.purpose') === OpportunityPromotion::PURPOSE) {
|
||||
$base = [...$base, ...($record->workflow_context['promotional_private_base'] ?? [])];
|
||||
}
|
||||
$draft = $record->management_draft ? json_decode($record->management_draft, true, 512, JSON_THROW_ON_ERROR) : [];
|
||||
|
||||
return [...$base, ...($this->closed($record) ? $draft : array_intersect_key($draft, array_flip(self::PRIVATE_DRAFT_FIELDS)))];
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/** Tempo's promotional disclosure, independent of investment execution approval. */
|
||||
final class OpportunityPromotion
|
||||
{
|
||||
public const PURPOSE = 'promotional_preparation';
|
||||
public const NOTICE = 'Em preparação. A divulgação não confirma financiamento, disponibilidade de terreno, licenças ou autorização de execução do investimento.';
|
||||
|
||||
public function allowed(User $actor, Opportunity $record): bool
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
if ($record->trashed() || $record->owner_user_id === null || ! $access->canView($actor, $record)
|
||||
|| $access->has($actor, 'auditor') || in_array($record->status, ['archived', 'rejected'], true)) return false;
|
||||
// Never overwrite a normal approved publication or its workflow decisions.
|
||||
if ($record->published_at && data_get($record->workflow_context, 'publication.purpose') !== self::PURPOSE) return false;
|
||||
$internal = $access->internalAssignment($actor, $record->opportunity_category);
|
||||
return ($internal && $access->promoterRecordScope($actor, $record) && $access->operational($actor, 'edit', $record->id))
|
||||
|| ($access->scoped($actor, 'publisher', $record->organization_id, $record->opportunity_category)
|
||||
&& $access->operational($actor, 'publish', $record->id));
|
||||
}
|
||||
|
||||
private function authorize(User $actor, Opportunity $record): void
|
||||
{
|
||||
abort_unless($this->allowed($actor, $record), 403, 'A designação e o âmbito existentes não autorizam esta divulgação.');
|
||||
if (! app(PortalProfileAccess::class)->mfa($actor)) {
|
||||
throw new \Illuminate\Http\Exceptions\HttpResponseException(response()->json(['message' => 'Confirme a entrada normal com MFA recente para divulgar.'], 403)->header('X-Portal-Reauthentication', 'required'));
|
||||
}
|
||||
}
|
||||
|
||||
public function editor(User $actor, Opportunity $record): array
|
||||
{
|
||||
abort_unless($this->allowed($actor, $record), 403);
|
||||
$saved = data_get($record->workflow_context, 'promotional_summary', []);
|
||||
// Only previously public text is recovered automatically. No private contact/document is copied.
|
||||
$existing = data_get($record->workflow_context, 'publication.purpose') === self::PURPOSE;
|
||||
$form = $saved['input'] ?? ['title' => $existing ? $record->title : '', 'summary' => $existing ? $record->summary : '',
|
||||
'description' => $existing ? $record->description : '', 'source_reference' => '', 'edition' => '', 'source_date' => '',
|
||||
'limitations' => '', 'territory_ids' => $existing ? DB::table('opportunity_locations')->where('opportunity_id', $record->id)->pluck('geo_location_id')->all() : [],
|
||||
'sector_id' => $record->sector_id];
|
||||
return ['record' => ['id' => $record->id, 'reference' => $record->reference], 'etag' => app(OpportunityManagement::class)->etag($record),
|
||||
'initial' => $form, 'publicNow' => $existing && $record->status === 'published',
|
||||
'reauthenticationRequired' => ! app(PortalProfileAccess::class)->mfa($actor),
|
||||
'sectors' => \App\Models\Sector::catalog()->orderBy('name')->get(['id', 'name']),
|
||||
'territories' => app(InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id', 'name', 'location_type']),
|
||||
'history' => DB::table('audit_events')->where('source_system', 'tempo-preparation-publication')
|
||||
->where('after_data->record_id', $record->id)->orderByDesc('occurred_at')->get(['id', 'action', 'actor_user_id', 'occurred_at'])];
|
||||
}
|
||||
|
||||
public function preview(User $actor, Opportunity $record, array $input): array
|
||||
{
|
||||
$this->authorize($actor, $record);
|
||||
$data = Validator::make($input, ['etag' => 'required|string|size:64', 'action' => 'required|in:disclose,withdraw',
|
||||
'title' => 'required_if:action,disclose|nullable|string|max:255', 'summary' => 'required_if:action,disclose|nullable|string|max:499',
|
||||
'description' => 'nullable|string|max:6000', 'limitations' => 'nullable|string|max:2500',
|
||||
'source_reference' => 'nullable|string|max:2000', 'edition' => 'nullable|string|max:150', 'source_date' => 'nullable|date_format:Y-m-d',
|
||||
'sector_id' => ['nullable', 'integer', Rule::in(\App\Models\Sector::catalog()->pluck('id')->all())],
|
||||
'territory_ids' => 'sometimes|array|max:30', 'territory_ids.*' => 'required|uuid|distinct',
|
||||
])->validate();
|
||||
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']), 409, 'A ficha mudou. Reabra antes de preparar outra prévia.');
|
||||
if ($data['action'] === 'withdraw') abort_unless(data_get($record->workflow_context, 'publication.purpose') === self::PURPOSE, 409);
|
||||
$paths = [];
|
||||
foreach ($data['territory_ids'] ?? [] as $id) {
|
||||
$path = app(InvestmentGeography::class)->path($id, true);
|
||||
abort_unless(count($path) > 0 && collect($path)->contains('id', $id), 422, 'Seleccione apenas territórios públicos existentes.');
|
||||
$paths[] = $path;
|
||||
}
|
||||
$text = fn ($key) => trim(strip_tags($data[$key] ?? ''));
|
||||
$body = implode("\n\n", array_filter([$text('description'), self::NOTICE,
|
||||
'Limitações e lacunas: '.($text('limitations') ?: 'Informação ainda por completar.'),
|
||||
'Fonte: '.($text('source_reference') ?: 'Não indicada.'), 'Edição: '.($text('edition') ?: 'Não indicada.'),
|
||||
'Data-base: '.($data['source_date'] ?? 'Não indicada.')]));
|
||||
$body = OpportunityPublicText::description($record, $body);
|
||||
$projection = ['title' => $text('title'), 'summary' => $text('summary'), 'description' => $body,
|
||||
'sector_id' => $data['sector_id'] ?? null, 'maturity_label' => 'Em preparação', 'notice' => self::NOTICE,
|
||||
'territories' => array_map(fn ($p) => ['id' => end($p)->id, 'label' => implode(' / ', array_column($p, 'name'))], $paths)];
|
||||
if ($data['action'] === 'disclose') abort_unless($projection['title'] !== '' && $projection['summary'] !== '', 422, 'Indique título e resumo em texto simples.');
|
||||
if ($data['action'] === 'withdraw') $projection = [...$projection, ...$record->only(['title', 'summary', 'description', 'sector_id', 'maturity_label'])];
|
||||
$payload = ['actor_id' => $actor->id, 'record_id' => $record->id, 'etag' => $data['etag'], 'action' => $data['action'],
|
||||
'input' => $data, 'projection' => $projection, 'expires' => now()->addMinutes(15)->timestamp];
|
||||
return ['action' => $data['action'], 'projection' => $projection, 'token' => Crypt::encryptString(json_encode($payload, JSON_THROW_ON_ERROR)),
|
||||
'current' => $record->only(['title', 'summary', 'description', 'status']), 'writes' => false];
|
||||
}
|
||||
|
||||
public function confirm(User $actor, int $id, array $input): array
|
||||
{
|
||||
$data = Validator::make($input, ['token' => 'required|string|max:300000', 'operation_key' => 'required|uuid',
|
||||
'confirm' => 'required|accepted', 'reason' => 'required|string|min:10|max:1000'])->validate();
|
||||
try { $preview = json_decode(Crypt::decryptString($data['token']), true, flags: JSON_THROW_ON_ERROR); }
|
||||
catch (\Throwable) { abort(422, 'Prévia inválida. Prepare outra prévia.'); }
|
||||
abort_unless(($preview['actor_id'] ?? null) === $actor->id && ($preview['record_id'] ?? null) === $id, 403);
|
||||
return DB::transaction(function () use ($actor, $id, $data, $preview) {
|
||||
DB::select('SELECT pg_advisory_xact_lock(hashtext(?))', ['promotional-operation:'.$data['operation_key']]);
|
||||
$record = Opportunity::whereKey($id)->lockForUpdate()->firstOrFail();
|
||||
$this->authorize($actor, $record);
|
||||
$digest = hash('sha256', $data['token'].'|'.$data['reason']);
|
||||
$replay = DB::table('audit_events')->where('source_system', 'tempo-preparation-publication')
|
||||
->where('after_data->operation_key', $data['operation_key'])->first();
|
||||
if ($replay) {
|
||||
$prior = json_decode($replay->after_data, true);
|
||||
abort_unless($prior['record_id'] === $id && (int) $replay->actor_user_id === $actor->id && hash_equals($prior['request_hash'], $digest), 409, 'Chave usada noutra operação.');
|
||||
return ['id' => $id, 'replayed' => true, 'message' => 'Operação já registada; nenhuma alteração repetida.'];
|
||||
}
|
||||
abort_unless(($preview['expires'] ?? 0) >= now()->timestamp, 409, 'A prévia expirou. Prepare outra prévia.');
|
||||
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $preview['etag']), 409, 'Alteração concorrente: a ficha foi preservada. Reabra e reveja a nova versão.');
|
||||
$before = $record->getRawOriginal();
|
||||
$oldLocations = DB::table('opportunity_locations')->where('opportunity_id', $id)->pluck('geo_location_id')->all();
|
||||
$context = $record->workflow_context ?? [];
|
||||
if ($preview['action'] === 'withdraw') {
|
||||
$record->status = 'suspended';
|
||||
$record->published_at = null;
|
||||
$context['publication']['visibility'] = 'withdrawn';
|
||||
$action = 'promotional_summary_withdrawn';
|
||||
} else {
|
||||
$p = $preview['projection'];
|
||||
if ($p['sector_id'] !== null) abort_unless(\App\Models\Sector::catalog()->whereKey($p['sector_id'])->exists(), 409, 'O sector deixou de estar disponível no catálogo. Prepare outra prévia.');
|
||||
$paths = [];
|
||||
foreach ($preview['input']['territory_ids'] ?? [] as $territory) {
|
||||
$path = app(InvestmentGeography::class)->path($territory, true);
|
||||
abort_unless(count($path) && collect($path)->contains('id', $territory), 409, 'Um território deixou de estar disponível. Prepare outra prévia.');
|
||||
$paths[] = $path;
|
||||
}
|
||||
$currentTerritories = array_map(fn ($path) => ['id' => end($path)->id, 'label' => implode(' / ', array_column($path, 'name'))], $paths);
|
||||
abort_unless($currentTerritories === $p['territories'], 409, 'O enquadramento territorial mudou. Prepare outra prévia.');
|
||||
// Preserve the original private draft bytes; retain its former base
|
||||
// columns in the existing hidden context before projecting public text.
|
||||
$context['promotional_private_base'] ??= $record->only(OpportunityManagement::FIELDS);
|
||||
$context['working_state'] ??= app(PortalProfileAccess::class)->state($record);
|
||||
$context['promotional_summary'] = ['input' => $preview['input'], 'projection' => $p];
|
||||
$context['publication'] = ['visibility' => 'public', 'purpose' => self::PURPOSE, 'actor_id' => $actor->id,
|
||||
'at' => now()->toIso8601String(), 'execution_approved' => false, 'private_draft_preserved' => true];
|
||||
$action = data_get($before, 'status') === 'published' ? 'promotional_summary_updated' : 'promotional_summary_published';
|
||||
foreach (['title', 'summary', 'description', 'sector_id', 'maturity_label'] as $field) $record->{$field} = $p[$field];
|
||||
foreach (['promoter_name', 'promoter_type', 'financing_modality', 'expected_impact', 'maturity_level', 'estimated_investment_amount', 'estimated_investment_currency'] as $field) $record->{$field} = null;
|
||||
$main = $paths[0] ?? [];
|
||||
$record->primary_territory_id = $main ? end($main)->id : null;
|
||||
$record->province = collect($main)->firstWhere('location_type', 'province')?->name;
|
||||
$record->district = collect($main)->firstWhere('location_type', 'district')?->name;
|
||||
$record->geography_scope = count($paths) > 1 ? 'multiple' : ($main ? 'specific' : 'unspecified');
|
||||
DB::table('opportunity_locations')->where('opportunity_id', $id)->delete();
|
||||
foreach ($preview['input']['territory_ids'] ?? [] as $territory) DB::table('opportunity_locations')->insert(['opportunity_id' => $id, 'geo_location_id' => $territory]);
|
||||
$record->status = 'published';
|
||||
$record->is_featured = false;
|
||||
$record->last_validated_at = null;
|
||||
$record->published_at = now();
|
||||
}
|
||||
$record->workflow_context = $context;
|
||||
$record->management_version++;
|
||||
$record->save();
|
||||
DB::table('audit_events')->insert(['source_system' => 'tempo-preparation-publication', 'entity_type' => 'opportunity',
|
||||
'actor_user_id' => $actor->id, 'action' => $action, 'data_classification' => 'restricted', 'correlation_id' => (string) Str::uuid(),
|
||||
'before_data' => json_encode(['record_id' => $id, 'record' => $before, 'territories' => $oldLocations], JSON_THROW_ON_ERROR),
|
||||
'after_data' => json_encode(['record_id' => $id, 'operation_key' => $data['operation_key'], 'request_hash' => $digest,
|
||||
'reason' => $data['reason'], 'projection' => $preview['projection'], 'execution_approved' => false,
|
||||
'record' => $record->getRawOriginal()], JSON_THROW_ON_ERROR)]);
|
||||
return ['id' => $id, 'replayed' => false, 'message' => $preview['action'] === 'withdraw' ? 'Resumo retirado; cadastro e histórico preservados.' : 'Resumo divulgado como Em preparação. Não autoriza execução do investimento.'];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
|
||||
/** Presentation only: originals, editorial fields and audit records remain intact. */
|
||||
final class OpportunityPublicText
|
||||
{
|
||||
public static function description(Opportunity $record, string $text): string
|
||||
{
|
||||
$source = data_get($record->workflow_context, 'corridor_agribusiness');
|
||||
if (! is_array($source) || ($source['source_sha256'] ?? null) !== CorridorAgribusinessOpportunities::SOURCE_HASH) return $text;
|
||||
|
||||
$hash = CorridorAgribusinessOpportunities::SOURCE_HASH;
|
||||
$sourceLine = 'Fonte: '.$source['source'].'; SHA-256 '.$hash;
|
||||
$paragraphs = explode("\n\n", $text);
|
||||
$result = [];
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
$paragraph = trim($paragraph);
|
||||
if (in_array($paragraph, [$sourceLine, $sourceLine.'.', 'Edição: Não indicada.', 'Data-base: Não indicada.',
|
||||
'Limitações e lacunas: '.CorridorAgribusinessOpportunities::LIMITATIONS,
|
||||
OpportunityPromotion::NOTICE], true)) continue;
|
||||
if ($paragraph === 'Enquadramento documental: '.$source['corridor'].'.') $paragraph = $source['corridor'].'.';
|
||||
if ($paragraph === CorridorAgribusinessOpportunities::LIMITATIONS) {
|
||||
$paragraph = 'A localização exacta, a disponibilidade de terreno e as condições do investimento serão definidas durante a preparação do projecto.';
|
||||
}
|
||||
if ($paragraph !== '') $result[] = $paragraph;
|
||||
}
|
||||
return implode("\n\n", $result);
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,9 @@ class PortalProfileAccess
|
||||
|
||||
public function canCreate(User $actor, ?string $organization, string $type): bool
|
||||
{
|
||||
$included = in_array($type, OpportunityCanonicalContract::BASE_CATEGORIES, true) || app(OpportunityModularProfiles::class)->enabled();
|
||||
// Draft preparation is allowed within the assigned modality. Executable
|
||||
// submission support is checked separately by the canonical contract.
|
||||
$included = in_array($type, OpportunityManagement::CATEGORIES, true);
|
||||
|
||||
// This is creation as an authorized Promotor, never an exceptional SU act.
|
||||
return $included && $this->available() && $this->scoped($actor, 'promoter', $organization, $type) && ! $this->has($actor, 'auditor');
|
||||
@@ -370,7 +372,8 @@ class PortalProfileAccess
|
||||
foreach (json_decode($grant->opportunity_types, true, 512, JSON_THROW_ON_ERROR) as $type) {
|
||||
$key = ($grant->organization_id ?? 'individual').'|'.$type;
|
||||
if (! isset($seen[$key]) && $this->canCreate($actor, $grant->organization_id, $type)) {
|
||||
$contexts[] = ['organization_id' => $grant->organization_id, 'type' => $type];
|
||||
$contexts[] = ['organization_id' => $grant->organization_id,
|
||||
'organization_name' => $grant->organization_id ? DB::table('organizations')->where('id', $grant->organization_id)->value('name') : null, 'type' => $type];
|
||||
$seen[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Brick\Math\BigDecimal;
|
||||
|
||||
/** Adapter of the dossier's declared context to preparation of a private-investment request. No tax or entitlement decision. */
|
||||
final class PrivateInvestmentPreparation
|
||||
{
|
||||
public const VERSION = 'private-investment-preparation-2026-v1';
|
||||
|
||||
public const SOURCE = 'https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf';
|
||||
|
||||
public const HASH = 'ba1e9eaabfec5d0c4cb2d9f2444a6860f7a90f222034764197829be402d408ed';
|
||||
|
||||
public const FIELDS = ['investment_form', 'investment_preparation', 'investment_activity_scope', 'implementation_mode', 'capital_origin', 'site_tenure', 'project_concession', 'significant_implications', 'land_extension', 'forest_concession', 'forest_concession_hectares', 'total_investment', 'area_required', 'area_unit'];
|
||||
|
||||
public const DOCUMENTS = ['identity', 'registration', 'name_reservation', 'site_plan', 'lease', 'duat', 'representation_license', 'feasibility', 'financial_capacity', 'foreign_capital'];
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
return (bool) config('investor-path.private_preparation', false);
|
||||
}
|
||||
|
||||
public function questions(): array
|
||||
{
|
||||
$tri = ['unknown' => 'Por determinar', 'yes' => 'Sim', 'no' => 'Não'];
|
||||
|
||||
return [
|
||||
['key' => 'investment_preparation', 'label' => 'Pretende preparar o pedido de garantias/incentivos ao investimento privado?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 2'],
|
||||
['key' => 'investment_activity_scope', 'label' => 'Natureza efectiva da actividade do projecto', 'options' => ['unknown' => 'Por determinar', 'general' => 'Outra actividade económica', 'extractive' => 'Prospecção, pesquisa ou produção extractiva', 'processing' => 'Processamento industrial mineiro/petrolífero', 'public_nonprofit' => 'Investimento público ou exclusivamente social/sem fins lucrativos'], 'source' => 'Lei 8/2023, artigos 2 e 22'],
|
||||
['key' => 'investment_form', 'label' => 'Tipo deste investimento', 'options' => ['unknown' => 'Por determinar', 'direct' => 'Directo', 'indirect' => 'Indirecto'], 'source' => 'Lei 8/2023, artigo 14(b); Regulamento, artigo 13', 'help' => 'O limiar monetário do artigo 13 aplica-se ao investimento directo. A forma indirecta não é convertida silenciosamente em directa.'],
|
||||
['key' => 'implementation_mode', 'label' => 'Modalidade da empresa implementadora', 'options' => ['unknown' => 'Por determinar', 'domestic_entity' => 'Empresa constituída/em constituição em Moçambique', 'foreign_representation' => 'Representação comercial estrangeira'], 'source' => 'Regulamento 8/2024, artigo 9(2)'],
|
||||
['key' => 'capital_origin', 'label' => 'Origem dos capitais/recursos deste investimento', 'options' => ['unknown' => 'Por determinar', 'national' => 'Nacionais', 'external' => 'Provenientes do exterior', 'mixed' => 'Mistos'], 'source' => 'Lei 8/2023, glossário; Regulamento, artigos 4–6'],
|
||||
['key' => 'site_tenure', 'label' => 'Fundamento declarado da utilização das instalações', 'options' => ['unknown' => 'Por determinar', 'lease' => 'Arrendamento', 'duat' => 'DUAT'], 'source' => 'Regulamento, artigos 9(1)(d) e 14(1)(d)'],
|
||||
['key' => 'project_concession', 'label' => 'O empreendimento é PPP ou concessão empresarial?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(ii)'],
|
||||
['key' => 'significant_implications', 'label' => 'Existem previsíveis implicações económicas, ambientais, de segurança ou saúde pública que exijam autorização?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(i)', 'help' => 'Declare o enquadramento conhecido; o nome do sector não responde a esta pergunta. Sem fundamento suficiente, mantenha por determinar.'],
|
||||
['key' => 'land_extension', 'label' => 'O projecto requer extensão de terra?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(iii)'],
|
||||
['key' => 'forest_concession', 'label' => 'O projecto requer concessão florestal?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(iv)'],
|
||||
['key' => 'forest_concession_hectares', 'label' => 'Área da concessão florestal (hectares)', 'type' => 'number', 'when' => ['field' => 'forest_concession', 'equals' => 'yes'], 'source' => 'Lei 8/2023, artigo 22(1)(b)(iv)'],
|
||||
['key' => 'total_investment', 'label' => 'Valor total do investimento na moeda do projecto', 'type' => 'number', 'source' => 'Regulamento, artigo 13(2)'],
|
||||
['key' => 'area_required', 'label' => 'Área de terra requerida', 'type' => 'number', 'when' => ['field' => 'land_extension', 'equals' => 'yes'], 'source' => 'Lei 8/2023, artigo 22(1)(b)(iii)'],
|
||||
['key' => 'area_unit', 'label' => 'Unidade da área requerida', 'options' => ['ha' => 'Hectares', 'm2' => 'Metros quadrados'], 'when' => ['field' => 'land_extension', 'equals' => 'yes'], 'source' => 'Conversão de unidade exacta; não é benefício ou condição adicional'],
|
||||
];
|
||||
}
|
||||
|
||||
private function number(array $f, string $key): ?BigDecimal
|
||||
{
|
||||
$value = $f[$key] ?? '';
|
||||
|
||||
return is_scalar($value) && preg_match('/^\d+(?:\.\d+)?$/D', (string) $value) ? BigDecimal::of((string) $value) : null;
|
||||
}
|
||||
|
||||
public function assess(array $context, array $documents = []): array
|
||||
{
|
||||
$f = $context['fields'] ?? [];
|
||||
$missing = [];
|
||||
$answer = function (string $key) use ($f, &$missing): ?bool {
|
||||
if (! in_array($f[$key] ?? '', ['yes', 'no'], true)) {
|
||||
$missing[] = $key;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $f[$key] === 'yes';
|
||||
};
|
||||
$enabled = ($f['investment_preparation'] ?? '') === 'yes';
|
||||
$date = $f['expected_start_date'] ?? '';
|
||||
$period = preg_match('/^2026-\d{2}-\d{2}$/D', $date) === 1;
|
||||
$scope = $f['investment_activity_scope'] ?? 'unknown';
|
||||
$covered = in_array($scope, ['general', 'processing'], true);
|
||||
$mode = 'indeterminate';
|
||||
$reasons = [];
|
||||
if ($enabled) {
|
||||
if (! $date) {
|
||||
$missing[] = 'expected_start_date';
|
||||
}
|
||||
if ($scope === 'unknown' || $scope === '') {
|
||||
$missing[] = 'investment_activity_scope';
|
||||
}
|
||||
if (empty($f['currency'])) {
|
||||
$missing[] = 'currency';
|
||||
}
|
||||
if ($covered && $period) {
|
||||
$triggers = [];
|
||||
if ($scope === 'processing') {
|
||||
$triggers['processing'] = true;
|
||||
$reasons[] = 'Processamento industrial mineiro/petrolífero: Lei 8/2023, artigo 22(1)(b)(v).';
|
||||
}
|
||||
$amount = $this->number($f, 'total_investment');
|
||||
$triggers['amount'] = $amount && ($f['currency'] ?? '') === 'MZN' && ($f['investment_form'] ?? '') === 'direct' ? $amount->isGreaterThan('32000000000') : null;
|
||||
if (! in_array($f['investment_form'] ?? '', ['direct', 'indirect'], true)) {
|
||||
$missing[] = 'investment_form';
|
||||
}
|
||||
if (($f['investment_form'] ?? '') === 'indirect') {
|
||||
$reasons[] = 'Investimento indirecto: a classificação pela dimensão e o encaminhamento monetário não são determinados por este lote. As restantes condições documentadas continuam avaliadas.';
|
||||
}
|
||||
if (! $amount) {
|
||||
$missing[] = 'total_investment';
|
||||
}
|
||||
if (($f['currency'] ?? '') !== 'MZN') {
|
||||
$reasons[] = 'A condição monetária não é determinada noutra moeda: não se fez conversão.';
|
||||
}
|
||||
foreach (['project_concession', 'significant_implications'] as $key) {
|
||||
$triggers[$key] = $answer($key);
|
||||
}
|
||||
$land = $answer('land_extension');
|
||||
if ($land === true) {
|
||||
$area = $this->number($f, 'area_required');
|
||||
$unit = $f['area_unit'] ?? '';
|
||||
if (! $area) {
|
||||
$missing[] = 'area_required';
|
||||
}if (! in_array($unit, ['ha', 'm2'], true)) {
|
||||
$missing[] = 'area_unit';
|
||||
}
|
||||
$triggers['land_extension'] = $area && in_array($unit, ['ha', 'm2'], true) ? $area->isGreaterThanOrEqualTo($unit === 'ha' ? '10000' : '100000000') : null;
|
||||
} else {
|
||||
$triggers['land_extension'] = $land;
|
||||
}
|
||||
$forest = $answer('forest_concession');
|
||||
if ($forest === true) {
|
||||
$area = $this->number($f, 'forest_concession_hectares');
|
||||
if (! $area) {
|
||||
$missing[] = 'forest_concession_hectares';
|
||||
}$triggers['forest_concession'] = $area ? $area->isGreaterThan('100000') : null;
|
||||
} else {
|
||||
$triggers['forest_concession'] = $forest;
|
||||
}
|
||||
foreach (['amount' => 'Valor superior a 32 mil milhões MZN — R13(2), R21(2)(a)', 'project_concession' => 'PPP/concessão empresarial — Lei 8/2023, art. 22(1)(b)(ii), R21(2)(b)', 'significant_implications' => 'Implicações significativas declaradas — Lei 8/2023, art. 22(1)(b)(i), R21(1)', 'land_extension' => 'Extensão de terra igual/superior a 10 mil hectares — Lei 8/2023, art. 22(1)(b)(iii), R21(2)(c)', 'forest_concession' => 'Concessão florestal superior a 100 mil hectares — Lei 8/2023, art. 22(1)(b)(iv), R21(2)(d)'] as $condition => $description) {
|
||||
if (($triggers[$condition] ?? null) === true) {
|
||||
$reasons[] = $description.'.';
|
||||
}
|
||||
}
|
||||
$mode = in_array(true, $triggers, true) ? 'authorization' : (in_array(null, $triggers, true) ? 'indeterminate' : 'registration');
|
||||
$reasons[] = $mode === 'authorization' ? 'Foi identificada uma condição de autorização; as restantes condições desconhecidas não foram presumidas negativas.' : ($mode === 'registration' ? 'As condições de autorização deste lote foram declaradas negativas e o montante não ultrapassa o limite.' : 'Falta resolver pelo menos uma condição de autorização.');
|
||||
}
|
||||
foreach (['implementation_mode', 'capital_origin', 'site_tenure', 'entity_stage'] as $key) {
|
||||
if (empty($f[$key]) || $f[$key] === 'unknown') {
|
||||
$missing[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($mode === 'authorization' && in_array(true, array_intersect_key($triggers, array_flip(['amount', 'project_concession', 'land_extension', 'forest_concession'])), true)) {
|
||||
$missing = array_values(array_diff($missing, ['investment_form', 'project_concession', 'significant_implications', 'land_extension', 'forest_concession', 'forest_concession_hectares', 'area_required', 'area_unit', 'total_investment']));
|
||||
}
|
||||
$base = $enabled && $covered && $period ? true : null;
|
||||
if (! $enabled || in_array($scope, ['extractive', 'public_nonprofit'], true) || ($date && ! $period)) {
|
||||
$base = false;
|
||||
}
|
||||
$rules = [
|
||||
['identity', 'Identificação do investidor', $base, 'S06', 'R9(1)(a) / R14(1)(a)', 41, 'Identificação do proponente do pedido.'],
|
||||
['registration', 'Certidão de registo comercial', $base === true ? (($f['entity_stage'] ?? '') === 'existing' ? true : (($f['entity_stage'] ?? '') === 'being_formed' ? false : null)) : $base, 'S05', 'R9(1)(b) / R14(1)(b)', 41, 'Alternativa para empresa já registada.'],
|
||||
['name_reservation', 'Reserva da denominação social', $base === true ? (($f['entity_stage'] ?? '') === 'being_formed' ? true : (($f['entity_stage'] ?? '') === 'existing' ? false : null)) : $base, 'S05', 'R9(1)(b) / R14(1)(b)', 41, 'Alternativa para empresa em constituição.'],
|
||||
['site_plan', 'Planta ou esboço do local', $base, 'S06', 'R9(1)(c) / R14(1)(c)', 42, 'Local de implantação identificado; não certifica disponibilidade.'],
|
||||
];
|
||||
foreach (['lease' => 'Contrato de arrendamento', 'duat' => 'Título DUAT'] as $key => $label) {
|
||||
$rules[] = [$key, $label, $base === true ? (($f['site_tenure'] ?? 'unknown') === 'unknown' ? null : ($f['site_tenure'] ?? '') === $key) : $base, 'S06', 'R9(1)(d) / R14(1)(d)', 42, 'Alternativa correspondente à utilização declarada das instalações.'];
|
||||
}
|
||||
$rules[] = ['representation_license', 'Licença de representação comercial', $base === true ? (($f['implementation_mode'] ?? 'unknown') === 'unknown' ? null : ($f['implementation_mode'] ?? '') === 'foreign_representation') : $base, 'S06', 'R9(2)', 42, 'Só para a modalidade representação comercial estrangeira; não decorre da nacionalidade.'];
|
||||
foreach (['feasibility' => 'Estudo de viabilidade técnica e económico-financeira', 'financial_capacity' => 'Comprovação da capacidade financeira'] as $key => $label) {
|
||||
$rules[] = [$key, $label, $base === true ? ($mode === 'indeterminate' ? null : $mode === 'authorization') : $base, 'S02', 'R14(1)–(2)', 45, 'Integra a preparação do pedido de autorização.'];
|
||||
}
|
||||
$rules[] = ['foreign_capital', 'Prova de realização/registo do aporte externo', $base === true ? (($f['capital_origin'] ?? 'unknown') === 'unknown' ? null : in_array($f['capital_origin'] ?? '', ['external', 'mixed'], true)) : $base, 'S06', 'R5(1)–(5)', 39, 'Preparação específica do aporte externo; formalidades cambiais concretas não calculadas.'];
|
||||
$links = $context['preparation_documents'] ?? [];
|
||||
$requirements = [];
|
||||
foreach ($rules as [$key,$label,$applies,$stage,$article,$page,$reason]) {
|
||||
$doc = isset($links[$key]) ? collect($documents)->firstWhere('document_id', $links[$key]) : null;
|
||||
$requirements[] = ['key' => $key, 'label' => $label, 'state' => $applies === false ? 'not_applicable' : ($applies === null ? 'indeterminate' : ($doc ? 'associated' : 'missing')), 'required' => $applies === true, 'document_id' => $links[$key] ?? null, 'document' => $doc, 'stage' => $stage, 'reason' => $reason, 'source' => ['reference' => $article, 'url' => self::SOURCE.'#page='.$page, 'sha256' => self::HASH, 'period' => '2026; Regulamento desde 07/03/2024'], 'legal_verification' => 'not_inferred'];
|
||||
}
|
||||
$authoritySource = ['reference' => 'R13(1)', 'url' => self::SOURCE.'#page=45'];
|
||||
$zone = $context['selection']['park'] ?? null;
|
||||
$amount = $this->number($f, 'total_investment');
|
||||
$authority = 'Por determinar neste lote';
|
||||
if ($base === true && $mode === 'registration' && $amount && ($f['currency'] ?? '') === 'MZN') {
|
||||
$special = in_array($zone['fields']['investment_regime'] ?? '', ['zee', 'zfi'], true);
|
||||
$authority = $special ? ($amount->isLessThanOrEqualTo('6500000000') ? 'APIEX — condicionado ao enquadramento ZEE/ZFI efectivamente reconhecido' : 'Ministro que superintende Finanças') : ($amount->isLessThanOrEqualTo('3500000000') ? 'Governador da Província' : ($amount->isLessThanOrEqualTo('6500000000') ? 'APIEX' : 'Ministro que superintende Finanças'));
|
||||
}
|
||||
if ($base === true && $mode === 'authorization') {
|
||||
$council = array_intersect_key($triggers, array_flip(['amount', 'project_concession', 'land_extension', 'forest_concession']));
|
||||
$authority = in_array(true, $council, true) ? 'Conselho de Ministros' : (in_array(null, $council, true) ? 'Autorização identificada; completar as condições em falta para distinguir a entidade decisória' : 'Ministro que superintende Finanças; eventual submissão ao Conselho de Ministros nos termos do R21(3)');
|
||||
$authoritySource = ['reference' => 'R21(1)–(3)', 'url' => self::SOURCE.'#page=50'];
|
||||
}
|
||||
|
||||
$labels = array_column($this->questions(), 'label', 'key') + ['currency' => 'Moeda do projecto', 'expected_start_date' => 'Data prevista de início', 'entity_stage' => 'Situação da entidade'];
|
||||
$missingLabels = array_map(fn ($key) => $labels[$key] ?? $key, array_values(array_unique($missing)));
|
||||
|
||||
return ['version' => self::VERSION, 'enabled' => $enabled, 'period_covered' => $period, 'scope_covered' => $covered, 'mode' => $mode, 'reason' => $reasons, 'missing_fields' => array_values(array_unique($missing)), 'missing_labels' => $missingLabels, 'questions' => $this->questions(), 'requirements' => $requirements, 'authority' => $authority, 'authority_source' => $authoritySource, 'next' => $base === false ? 'Rever o âmbito/período; este lote não cobre o pedido' : ($mode === 'indeterminate' ? 'Completar condições do enquadramento' : ($mode === 'authorization' ? 'Preparar estudo e documentos do pedido de autorização' : 'Preparar documentos do pedido de mero registo')), 'calculation_notice' => 'O regime procedimental não altera a taxa de IVA. Reutilize a simulação geral apenas no seu âmbito e período; benefícios, apuramento final e conversão cambial não são presumidos.', 'source' => ['url' => self::SOURCE, 'sha256' => self::HASH], 'notice' => 'Preparação informativa baseada em declarações; documentos associados não constituem validação jurídica, certificado ou submissão à Administração.'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** Publication of notices; never receives bids or changes the linked investment. */
|
||||
final class Procurement
|
||||
{
|
||||
public const TEXT = ['title', 'reference', 'responsible_unit', 'modality', 'object', 'object_kind', 'quantities', 'financier', 'instrument', 'edition', 'rule_source', 'financing_agreement', 'eligibility', 'requirements', 'proposal_validity', 'guarantee_status', 'guarantees', 'delivery_channel', 'delivery_address', 'delivery_url', 'public_contact', 'dissemination', 'origin_url', 'origin_published_at', 'clarifications_at', 'deadline', 'opening_at', 'timezone', 'minimum_source', 'minimum_edition', 'minimum_modality', 'minimum_day_basis', 'result', 'internal_notes', 'public_reason', 'minimum_working_days', 'minimum_holidays', 'minimum_calendar_source'];
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
return config('procurement.enabled') && Schema::hasTable('portal_tenders');
|
||||
}
|
||||
|
||||
public function access(?User $actor): bool
|
||||
{
|
||||
if (! $actor || ! $actor->hasVerifiedEmail()) {
|
||||
return false;
|
||||
}
|
||||
$docs = app(DocumentManagementAccess::class);
|
||||
|
||||
return ! app(PortalProfileAccess::class)->has($actor, 'auditor') && ($docs->full($actor) || $docs->editorial($actor));
|
||||
}
|
||||
|
||||
public function guard(?User $actor): void
|
||||
{
|
||||
abort_unless($this->available(), 404);
|
||||
abort_unless($this->access($actor), 403, 'Use a autorização editorial já atribuída.');
|
||||
}
|
||||
|
||||
public function references(User $actor): array
|
||||
{
|
||||
$this->guard($actor);
|
||||
$editable = app(DocumentManagementAccess::class)->documents($actor)->select('id');
|
||||
|
||||
return ['regimes' => config('procurement.regimes'), 'acts' => config('procurement.acts'),
|
||||
'organizations' => DB::table('organizations')->whereNull('deleted_at')->orderBy('name')->get(['id', 'name']),
|
||||
'sectors' => DB::table('sectors')->orderBy('name')->get(['id', 'name']),
|
||||
'territories' => app(InvestmentGeography::class)->references(true)->orderBy('name')->get(['id', 'name', 'location_type']),
|
||||
'opportunities' => app(PortalProfileAccess::class)->scope(Opportunity::query(), $actor)->orderBy('title')->get(['id', 'title']),
|
||||
'projects' => DB::table('projects')->whereNull('deleted_at')->whereIn('data_classification', ['internal', 'public'])->orderBy('name')->get(['id', 'name']),
|
||||
'documents' => DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at')
|
||||
->where(fn ($q) => $q->whereIn('d.id', $editable)->orWhere(fn ($p) => $p->where('r.published', true)->where('d.data_classification', 'public')))
|
||||
->orderBy('r.title')->get(['r.id', 'r.title', 'r.published', 'd.malware_scan_status'])];
|
||||
}
|
||||
|
||||
public function record(string $id): object
|
||||
{
|
||||
abort_unless($this->available(), 404);
|
||||
|
||||
return DB::table('portal_tenders')->where('id', $id)->firstOrFail();
|
||||
}
|
||||
|
||||
public function save(User $actor, ?string $id, array $input): string
|
||||
{
|
||||
$this->guard($actor);
|
||||
$v = Validator::make($input, ['version' => 'required|integer|min:0', 'reason' => 'required|string|min:20|max:1000', 'draft' => 'required|array']);
|
||||
$v->validate();
|
||||
$draft = $this->normalize($actor, $input['draft']);
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $input, $draft) {
|
||||
$old = $id ? DB::table('portal_tenders')->where('id', $id)->lockForUpdate()->firstOrFail() : null;
|
||||
abort_unless((int) ($old->version ?? 0) === (int) $input['version'], 409, 'A versão mudou; recupere o concurso antes de guardar.');
|
||||
$id ??= (string) Str::uuid();
|
||||
$version = ($old->version ?? 0) + 1;
|
||||
if ($old) {
|
||||
DB::table('portal_tenders')->where('id', $id)->update(['draft' => json_encode($draft, JSON_THROW_ON_ERROR), 'version' => $version, 'updated_at' => now()]);
|
||||
} else {
|
||||
DB::table('portal_tenders')->insert(['id' => $id, 'draft' => json_encode($draft, JSON_THROW_ON_ERROR), 'version' => $version, 'created_by' => $actor->id, 'created_at' => now(), 'updated_at' => now()]);
|
||||
}
|
||||
$this->version($actor, $id, $version, 'draft', $draft, $input['reason'], false);
|
||||
|
||||
return $id;
|
||||
});
|
||||
}
|
||||
|
||||
private function normalize(User $actor, array $input): array
|
||||
{
|
||||
$rules = array_fill_keys(self::TEXT, 'nullable|string|max:12000');
|
||||
$rules = [...$rules, 'regime' => 'nullable|in:state,external,private,other', 'organization_id' => 'nullable|uuid|exists:organizations,id',
|
||||
'opportunity_id' => 'nullable|integer|exists:opportunities,id', 'project_id' => 'nullable|uuid|exists:projects,id', 'sector_id' => 'nullable|integer|exists:sectors,id',
|
||||
'territory_id' => 'nullable|uuid|exists:geo_locations,id', 'contact_authorized' => 'boolean', 'minimum_days' => 'nullable|integer|min:0|max:3650',
|
||||
'lots' => 'array|max:100', 'lots.*' => 'array:code,description,quantities', 'lots.*.code' => 'required|string|max:80|distinct', 'lots.*.description' => 'required|string|max:3000', 'lots.*.quantities' => 'nullable|string|max:1500',
|
||||
'attachments' => 'array|max:100', 'attachments.*' => 'array:resource_id,public,purpose', 'attachments.*.resource_id' => 'required|integer|exists:portal_resources,id', 'attachments.*.public' => 'required|boolean', 'attachments.*.purpose' => 'required|string|max:150'];
|
||||
foreach (['origin_url', 'delivery_url', 'rule_source'] as $url) {
|
||||
$rules[$url] = 'nullable|url:http,https|max:2000';
|
||||
}
|
||||
$data = Validator::make($input, $rules)->validate();
|
||||
if (! empty($data['opportunity_id'])) {
|
||||
abort_unless(app(PortalProfileAccess::class)->scope(Opportunity::query(), $actor)->whereKey($data['opportunity_id'])->exists(), 403);
|
||||
}
|
||||
if (! empty($data['project_id'])) {
|
||||
abort_unless(DB::table('projects')->where('id', $data['project_id'])->whereNull('deleted_at')->whereIn('data_classification', ['internal', 'public'])->exists(), 422);
|
||||
}
|
||||
foreach ($data['attachments'] ?? [] as $attachment) {
|
||||
$resource = DB::table('portal_resources')->where('id', $attachment['resource_id'])->firstOrFail();
|
||||
abort_unless($this->publicDocuments()->where('r.id', $resource->id)->exists() || app(DocumentManagementAccess::class)->documents($actor)->where('id', $resource->document_id)->exists(), 403);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function preview(User $actor, string $id, string $act): array
|
||||
{
|
||||
$this->guard($actor);
|
||||
$record = $this->record($id);
|
||||
abort_unless(array_key_exists($act, config('procurement.acts')), 422);
|
||||
$p = json_decode($record->draft, true, 512, JSON_THROW_ON_ERROR);
|
||||
Validator::make($p, array_fill_keys(['title', 'reference', 'organization_id', 'modality', 'object', 'regime', 'instrument', 'edition', 'rule_source', 'origin_published_at', 'deadline', 'delivery_channel', 'timezone'], 'required'))->validate();
|
||||
Validator::make($p, ['timezone' => 'timezone', 'object_kind' => 'nullable|in:works,goods,services,mixed,other', 'guarantee_status' => 'nullable|in:required,not_required,unknown'])->validate();
|
||||
Validator::make($p, ['public_reason' => 'required|string|min:20|max:1000'])->validate();
|
||||
$dates = [];
|
||||
foreach (['origin_published_at', 'clarifications_at', 'deadline', 'opening_at'] as $field) {
|
||||
if (empty($p[$field])) {
|
||||
continue;
|
||||
}
|
||||
$validator = Validator::make([$field => $p[$field]], [$field => 'date_format:Y-m-d\TH:i']);
|
||||
$validator->validate();
|
||||
$dates[$field] = CarbonImmutable::createFromFormat('!Y-m-d\TH:i', $p[$field], $p['timezone']);
|
||||
}
|
||||
$this->require($dates['deadline']->greaterThan($dates['origin_published_at']), 'deadline', 'Entrega anterior à publicação de origem.');
|
||||
if (isset($dates['clarifications_at'])) {
|
||||
$this->require($dates['clarifications_at']->betweenIncluded($dates['origin_published_at'], $dates['deadline']), 'clarifications_at', 'Esclarecimentos fora do período de publicação/entrega.');
|
||||
}
|
||||
if (isset($dates['opening_at'])) {
|
||||
$this->require($dates['opening_at']->greaterThanOrEqualTo($dates['deadline']), 'opening_at', 'A abertura indicada é anterior à entrega.');
|
||||
}
|
||||
if (($p['guarantee_status'] ?? '') === 'required') {
|
||||
$this->require(! empty($p['guarantees']), 'guarantees', 'Descreva a garantia documentada exigida.');
|
||||
}
|
||||
if (isset($p['minimum_days'])) {
|
||||
foreach (['minimum_source', 'minimum_edition', 'minimum_modality', 'minimum_day_basis'] as $field) {
|
||||
$this->require(! empty($p[$field]), $field, 'Identifique fonte, edição, modalidade e contagem do prazo mínimo.');
|
||||
}
|
||||
$this->require($p['minimum_modality'] === $p['modality'], 'minimum_modality', 'A regra indicada pertence a outra modalidade.');
|
||||
$earliest = $dates['origin_published_at'];
|
||||
if ($p['minimum_day_basis'] === 'business') {
|
||||
$this->require(! empty($p['minimum_calendar_source']), 'minimum_calendar_source', 'Identifique a fonte do calendário de dias úteis e feriados.');
|
||||
$weekdays = array_filter(array_map('trim', explode(',', (string) ($p['minimum_working_days'] ?? ''))));
|
||||
$this->require(count($weekdays) > 0 && ! array_diff($weekdays, ['1', '2', '3', '4', '5', '6', '7']), 'minimum_working_days', 'Indique os dias de trabalho documentados (1=segunda, 7=domingo).');
|
||||
$holidays = array_values(array_filter(preg_split('/[\s,;]+/', (string) ($p['minimum_holidays'] ?? ''))));
|
||||
Validator::make(['dates' => $holidays], ['dates' => 'array|max:1000', 'dates.*' => 'date_format:Y-m-d'])->validate();
|
||||
for ($left = (int) $p['minimum_days'],$iterations = 0; $left > 0; $iterations++) {
|
||||
$this->require($iterations < 36500, 'minimum_days', 'Calendário insuficiente para calcular o prazo indicado.');
|
||||
$earliest = $earliest->addDay();
|
||||
if (in_array((string) $earliest->isoWeekday(), $weekdays, true) && ! in_array($earliest->format('Y-m-d'), $holidays, true)) {
|
||||
$left--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->require($p['minimum_day_basis'] === 'calendar', 'minimum_day_basis', 'Identifique dias corridos ou calendário de dias úteis documentado.');
|
||||
$earliest = $earliest->addDays((int) $p['minimum_days']);
|
||||
}
|
||||
$this->require($dates['deadline']->greaterThanOrEqualTo($earliest), 'deadline', 'Prazo inferior ao mínimo documentado para esta modalidade/edição/calendário.');
|
||||
}
|
||||
$previous = $record->published_version_id ? $this->publishedVersion($record->published_version_id) : null;
|
||||
$this->require($previous ? $act !== 'initial' : $act === 'initial', 'act', $previous ? 'O anúncio inicial já existe; publique um acto associado.' : 'Publique primeiro o anúncio inicial.');
|
||||
if ($previous) {
|
||||
$before = $previous['payload'];
|
||||
if (($before['deadline'] ?? '') !== $p['deadline'] || ($before['timezone'] ?? '') !== $p['timezone']) {
|
||||
$this->require($act === 'extension', 'act', 'Uma mudança do prazo exige acto de prorrogação, com data anterior e nova.');
|
||||
}
|
||||
if ($act === 'extension') {
|
||||
$this->require($dates['deadline']->greaterThan(CarbonImmutable::parse($before['deadline'], $before['timezone'])), 'deadline', 'A prorrogação deve avançar o prazo anterior.');
|
||||
$p['deadline_previous'] = $before['deadline'];
|
||||
$p['timezone_previous'] = $before['timezone'];
|
||||
}
|
||||
$p['extended'] = ($before['extended'] ?? false) || $act === 'extension';
|
||||
$p['procedural_state'] = $before['procedural_state'] ?? null;
|
||||
}
|
||||
if ($act === 'award') {
|
||||
$p['procedural_state'] = 'results';
|
||||
}
|
||||
if ($act === 'cancellation') {
|
||||
$p['procedural_state'] = 'cancelled';
|
||||
}
|
||||
if (in_array($act, ['award', 'cancellation', 'other'], true)) {
|
||||
$this->require(! empty($p['result']), 'result', 'Descreva o acto ou resultado documentado.');
|
||||
}
|
||||
$p['organization_name'] = DB::table('organizations')->where('id', $p['organization_id'])->whereNull('deleted_at')->value('name');
|
||||
$this->require((bool) $p['organization_name'], 'organization_id', 'Entidade eliminada; recupere a referência.');
|
||||
$p['sector_name'] = empty($p['sector_id']) ? null : DB::table('sectors')->where('id', $p['sector_id'])->value('name');
|
||||
$p['territory_name'] = empty($p['territory_id']) ? null : app(InvestmentGeography::class)->references(true)->where('id', $p['territory_id'])->value('name');
|
||||
if (! empty($p['territory_id'])) {
|
||||
$this->require((bool) $p['territory_name'], 'territory_id', 'Referência territorial indisponível.');
|
||||
}
|
||||
unset($p['internal_notes']);
|
||||
if (! ($p['contact_authorized'] ?? false)) {
|
||||
unset($p['public_contact']);
|
||||
}
|
||||
$attachments = [];
|
||||
foreach ($p['attachments'] ?? [] as $item) {
|
||||
if (! $item['public']) {
|
||||
continue;
|
||||
}
|
||||
$doc = $this->publicDocuments()->where('r.id', $item['resource_id'])->first(['r.id', 'r.title', 'd.sha256', 'd.id as document_id']);
|
||||
$this->require((bool) $doc, 'attachments', 'Anexo público exige publicação autorizada, scan limpo e direitos sem restrição na biblioteca.');
|
||||
$attachments[] = [...$item, 'title' => $doc->title, 'sha256' => trim($doc->sha256), 'document_id' => $doc->document_id];
|
||||
}
|
||||
$p['attachments'] = $attachments;
|
||||
if (! empty($p['opportunity_id'])) {
|
||||
$o = Opportunity::published()->whereKey($p['opportunity_id'])->first(['id', 'title', 'slug']);
|
||||
$p['opportunity'] = $o ? ['title' => $o->title, 'url' => '/oportunidades/'.$o->slug] : null;
|
||||
}
|
||||
if (! empty($p['project_id'])) {
|
||||
$p['project_name'] = DB::table('projects')->where('id', $p['project_id'])->whereNull('deleted_at')->where('data_classification', 'public')->value('name');
|
||||
}
|
||||
unset($p['opportunity_id'],$p['project_id']);
|
||||
$plan = ['id' => $id, 'version' => $record->version, 'act' => $act, 'payload' => $p];
|
||||
$plan['hash'] = hash_hmac('sha256', json_encode($plan, JSON_THROW_ON_ERROR), config('app.key'));
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
public function publish(User $actor, string $id, array $input): void
|
||||
{
|
||||
$this->guard($actor);
|
||||
Validator::make($input, ['hash' => 'required|string|size:64', 'act' => 'required|string', 'reason' => 'required|string|min:20|max:1000'])->validate();
|
||||
DB::transaction(function () use ($actor, $id, $input) {
|
||||
$record = DB::table('portal_tenders')->where('id', $id)->lockForUpdate()->firstOrFail();
|
||||
$plan = $this->preview($actor, $id, $input['act']);
|
||||
abort_unless(hash_equals($plan['hash'], $input['hash']), 409, 'Prévia alterada; reveja o conteúdo antes de publicar.');
|
||||
abort_unless($input['reason'] === $plan['payload']['public_reason'], 409, 'O motivo público deve coincidir com a prévia.');
|
||||
$version = $record->version + 1;
|
||||
$vid = $this->version($actor, $id, $version, $input['act'], $plan['payload'], $input['reason'], true);
|
||||
DB::table('portal_tenders')->where('id', $id)->update(['published_version_id' => $vid, 'version' => $version, 'updated_at' => now()]);
|
||||
});
|
||||
}
|
||||
|
||||
private function version(User $actor, string $id, int $version, string $act, array $payload, string $reason, bool $published): string
|
||||
{
|
||||
$vid = (string) Str::uuid();
|
||||
DB::table('portal_tender_versions')->insert(['id' => $vid, 'tender_id' => $id, 'version' => $version, 'act' => $act, 'payload' => json_encode($payload, JSON_THROW_ON_ERROR), 'reason' => $reason, 'actor_id' => $actor->id, 'published_at' => $published ? now() : null, 'created_at' => now()]);
|
||||
DB::table('audit_events')->insert(['source_system' => 'procurement', 'entity_type' => 'portal_tenders', 'entity_id' => $id, 'actor_user_id' => $actor->id, 'action' => $published ? 'notice_published' : 'tender_draft_saved', 'data_classification' => 'internal', 'after_data' => json_encode(['version_id' => $vid, 'act' => $act, 'reason' => $reason], JSON_THROW_ON_ERROR)]);
|
||||
|
||||
return $vid;
|
||||
}
|
||||
|
||||
public function publicDocuments(): Builder
|
||||
{
|
||||
return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->where('r.published', true)->where('r.rights_status', '!=', 'restricted')->where('d.rights_status', '!=', 'restricted')->where('d.data_classification', 'public')->where('d.status', 'active')->whereNull('d.deleted_at')->where('d.malware_scan_status', 'clean')->whereColumn('d.sha256', 'd.malware_scanned_sha256')
|
||||
->where(fn ($q) => $q->whereNull('d.source_system')->orWhereNotIn('d.source_system', ['opportunity-resources', 'opportunity-image-derivatives']))
|
||||
->whereNotExists(fn ($q) => $q->selectRaw('1')->from('opportunity_documents as od')->whereColumn('od.document_id', 'd.id')->whereRaw("NOT jsonb_exists(COALESCE(od.payload,'{}'::jsonb),'library_source')"))
|
||||
->whereNotExists(fn ($q) => $q->selectRaw('1')->from('opportunity_images as oi')->whereColumn('oi.document_id', 'd.id')->whereRaw("NOT jsonb_exists(COALESCE(oi.payload,'{}'::jsonb),'library_source')"));
|
||||
}
|
||||
|
||||
public function publishedVersion(string $id): array
|
||||
{
|
||||
$v = DB::table('portal_tender_versions')->where('id', $id)->whereNotNull('published_at')->firstOrFail();
|
||||
$p = json_decode($v->payload, true, 512, JSON_THROW_ON_ERROR);
|
||||
foreach ($p['attachments'] ?? [] as $i => $item) {
|
||||
$p['attachments'][$i]['available'] = $this->publicDocuments()->where('r.id', $item['resource_id'])->where('d.id', $item['document_id'])->where('d.sha256', $item['sha256'])->exists();
|
||||
}
|
||||
|
||||
return ['id' => $v->id, 'version' => $v->version, 'act' => $v->act, 'payload' => $p, 'published_at' => $v->published_at, 'reason' => $v->reason];
|
||||
}
|
||||
|
||||
public function state(array $version): string
|
||||
{
|
||||
$p = $version['payload'];
|
||||
if (in_array($p['procedural_state'] ?? '', ['cancelled', 'results'], true)) {
|
||||
return $p['procedural_state'];
|
||||
}
|
||||
if (CarbonImmutable::parse($p['deadline'], $p['timezone'])->isPast()) {
|
||||
return 'deadline_ended';
|
||||
}
|
||||
|
||||
return ($p['extended'] ?? false) ? 'extended' : 'open';
|
||||
}
|
||||
|
||||
private function require(bool $condition, string $field, string $message): void
|
||||
{
|
||||
if (! $condition) {
|
||||
throw ValidationException::withMessages([$field => $message]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,9 +60,11 @@ final class SimulationFormDrafts
|
||||
throw new HttpException(409, 'Já existe um rascunho activo. Carregue-o antes de guardar novas alterações.');
|
||||
}
|
||||
|
||||
if ($case) $data['economic_snapshot'] = $this->landContext($data['economic_snapshot'], $case);
|
||||
$snapshotHash = hash('sha256', json_encode($this->canonical($data), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
$draft = SimulationFormDraft::query()->create([
|
||||
'id' => (string) Str::uuid(), 'owner_user_id' => $actorId, 'anonymous_token_hash' => $tokenHash,
|
||||
'version_no' => 1, 'create_key_hash' => $keyHash, 'create_payload_hash' => $payloadHash, 'snapshot_sha256' => $payloadHash,
|
||||
'version_no' => 1, 'create_key_hash' => $keyHash, 'create_payload_hash' => $payloadHash, 'snapshot_sha256' => $snapshotHash,
|
||||
'personal_snapshot' => SimulationFormDraft::encryptSnapshot($data['personal_snapshot']),
|
||||
'economic_snapshot' => SimulationFormDraft::encryptSnapshot($data['economic_snapshot']),
|
||||
'expires_at' => $actorId === null ? now()->addDays(30) : null,
|
||||
@@ -89,6 +91,7 @@ final class SimulationFormDrafts
|
||||
$this->checkContext($request, $draft);
|
||||
$this->requireMatch($request, $draft);
|
||||
$previousEconomic = SimulationFormDraft::decryptSnapshot($draft->economic_snapshot);
|
||||
$data['economic_snapshot'] = $this->preserveInactiveLandStatus($draft, $data['economic_snapshot'], $previousEconomic, $actorId);
|
||||
if (($data['economic_snapshot']['reuse_simulation'] ?? null) === 'no') {
|
||||
unset($data['economic_snapshot']['simulation_sources']);
|
||||
} elseif (is_array($previousEconomic['simulation_sources'] ?? null)) {
|
||||
@@ -456,6 +459,39 @@ final class SimulationFormDrafts
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function preserveInactiveLandStatus(SimulationFormDraft $draft, array $economic, array $previous, ?int $actorId): array
|
||||
{
|
||||
if (! $actorId || ! config('investor-path.enabled') || ! app(VirtualDesk::class)->installed()) return $economic;
|
||||
// Use the real owner-bound dossier even if a request omits its context parameter.
|
||||
$case = DB::table('desk_cases')->where('simulation_id', $draft->id)->where('user_id', $actorId)->first();
|
||||
if (! $case) return $economic;
|
||||
return $this->landContext($economic, $case, $previous);
|
||||
}
|
||||
|
||||
private function landContext(array $economic, \stdClass $case, ?array $previous = null): array
|
||||
{
|
||||
$snapshot = json_decode($economic['context_applicability_snapshot'] ?? $previous['context_applicability_snapshot'] ?? '', true);
|
||||
$key = $economic['context_opportunity'] ?? $snapshot['consumer_opportunity'] ?? null;
|
||||
$key = app(InvestmentConsumerScope::class)->key(app(InvestorContext::class)->state($case), is_string($key) ? $key : null);
|
||||
$decision = app(InvestorContext::class)->landPreparation($case, $key);
|
||||
if ($decision['need'] !== 'yes' && $previous !== null) {
|
||||
if (array_key_exists('land_status', $previous)) $economic['land_status'] = $previous['land_status'];
|
||||
else unset($economic['land_status']);
|
||||
if (isset($previous['selection_labels']['land_status'])) $economic['selection_labels']['land_status'] = $previous['selection_labels']['land_status'];
|
||||
else unset($economic['selection_labels']['land_status']);
|
||||
}
|
||||
$snapshot = json_decode($economic['context_applicability_snapshot'] ?? $previous['context_applicability_snapshot'] ?? '', true);
|
||||
if (! is_array($snapshot) || ($snapshot['version'] ?? null) !== 1) {
|
||||
$snapshot = ['version' => 1, 'context_revision' => app(InvestorContext::class)->state($case)['revision']];
|
||||
}
|
||||
$snapshot['land_preparation'] = $decision;
|
||||
$snapshot['consumer_opportunity'] = $key;
|
||||
if ($key) $economic['context_opportunity'] = $key;
|
||||
$economic['context_applicability_snapshot'] = json_encode($snapshot, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return $economic;
|
||||
}
|
||||
|
||||
private function checkContext(Request $request, SimulationFormDraft $draft): void
|
||||
{
|
||||
$context = $request->input('context');
|
||||
|
||||
@@ -14,16 +14,31 @@ final class TerritoryGeometry
|
||||
abort_unless($geography->references($management)->where('id', $id)->exists(), 404);
|
||||
$path = $geography->path($id, $management);
|
||||
$ids = array_map(fn ($node) => $node->id, $path);
|
||||
$approved = app(ApprovedAdministrativeBoundaries::class);
|
||||
$rows = DB::table('geo_geometries as g')->join('geo_locations as l', 'l.id', '=', 'g.geo_location_id')
|
||||
->whereIn('g.geo_location_id', $ids)->whereNull('g.deleted_at')->whereNull('l.deleted_at')
|
||||
->whereIn('g.data_classification', $management ? ['internal', 'public'] : ['public'])
|
||||
->where(function ($query) use ($management, $approved) {
|
||||
$query->where('g.data_classification', 'public');
|
||||
$query->orWhere(function ($internal) use ($management, $approved) {
|
||||
$internal->where('g.data_classification', 'internal');
|
||||
if (! $management) {
|
||||
$internal->whereIn('g.id', array_keys($approved->snapshots()));
|
||||
}
|
||||
});
|
||||
})
|
||||
->whereRaw("g.properties->>'purpose' = 'administrative_boundary'")
|
||||
->whereIn('g.geometry_type', ['polygon', 'multipolygon'])
|
||||
->whereRaw('ST_IsValid(g.geom) AND NOT ST_IsEmpty(g.geom) AND ST_SRID(g.geom)=4326')
|
||||
->orderBy('g.geo_location_id')->orderBy('g.id')->limit(101)
|
||||
->get(['g.id', 'g.geo_location_id', 'l.name', 'l.location_type', 'g.properties', 'g.data_classification',
|
||||
DB::raw('ST_AsGeoJSON(ST_SimplifyPreserveTopology(g.geom,0.0001),6) as geometry')]);
|
||||
$areas = $rows->take(100)->map(function ($row) use ($id) {
|
||||
DB::raw('ST_AsGeoJSON(ST_SimplifyPreserveTopology(g.geom,0.0001),6) as geometry'),
|
||||
DB::raw("encode(sha256(ST_AsEWKB(g.geom)),'hex') as geometry_sha256"),
|
||||
DB::raw("encode(sha256(convert_to(g.properties::text,'UTF8')),'hex') as properties_sha256")]);
|
||||
if (! $management) {
|
||||
$rows = $rows->filter(fn ($row) => $row->data_classification === 'public'
|
||||
|| $approved->allows($row->id, $row->geo_location_id, $row->geometry_sha256, $row->properties_sha256))->values();
|
||||
}
|
||||
$areas = $rows->take(100)->map(function ($row) use ($id, $management) {
|
||||
$source = json_decode($row->properties, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return ['id' => $row->id, 'territory_id' => $row->geo_location_id, 'location_type' => $row->location_type,
|
||||
@@ -31,7 +46,7 @@ final class TerritoryGeometry
|
||||
'title' => 'Limite administrativo: '.$row->name, 'purpose' => 'administrative_boundary',
|
||||
'source_reference' => $source['authority'] ?? null, 'source_date' => $source['source_version'] ?? null,
|
||||
'source_url' => $source['source_url'] ?? null, 'source_sha256' => $source['source_sha256'] ?? null,
|
||||
'data_classification' => $row->data_classification, 'legal_currentness' => $source['legal_currentness'] ?? 'not_determined', 'url' => ''];
|
||||
'data_classification' => $management ? $row->data_classification : 'public', 'legal_currentness' => $source['legal_currentness'] ?? 'not_determined', 'url' => ''];
|
||||
})->all();
|
||||
|
||||
return ['territory_id' => $id, 'territory_registered' => true,
|
||||
|
||||
@@ -20,7 +20,7 @@ class TerritoryGeometryImport
|
||||
|
||||
private function guard(User $actor, bool $write = false): void
|
||||
{
|
||||
abort_unless($actor->can('manage-portal'), 403);
|
||||
abort_unless($actor->can('manage-portal') || app(ScopedPortalAdministration::class)->administrative($actor), 403);
|
||||
abort_unless(config('geometry-import.enabled'), 503, 'Importação cartográfica não disponibilizada.');
|
||||
if (! $write) {
|
||||
return;
|
||||
|
||||
@@ -170,11 +170,11 @@ class VirtualDesk
|
||||
});
|
||||
}
|
||||
|
||||
public function createRequest(User $actor, string $caseId, string $serviceId, string $clientKey): \stdClass
|
||||
public function createRequest(User $actor, string $caseId, string $serviceId, string $clientKey, ?string $opportunity = null): \stdClass
|
||||
{
|
||||
abort_unless(Str::isUuid($serviceId) && Str::isUuid($clientKey), 422);
|
||||
|
||||
return DB::transaction(function () use ($actor, $caseId, $serviceId, $clientKey) {
|
||||
return DB::transaction(function () use ($actor, $caseId, $serviceId, $clientKey, $opportunity) {
|
||||
$case = $this->case($actor, $caseId, true, true);
|
||||
abort_if($case->closed_at, 409, 'Dossier encerrado.');
|
||||
$service = DB::table('desk_service_versions')->where('id', $serviceId)->where('status', 'published')->first();
|
||||
@@ -197,7 +197,10 @@ class VirtualDesk
|
||||
$this->event($case, $actor, 'request_prefill_prepared', ['draft_id' => $draft->id, 'draft_version' => $draft->version_no, 'snapshot_sha256' => $draft->snapshot_sha256, 'fields' => ['title', 'description']]);
|
||||
}
|
||||
$context = app(InvestorContext::class)->state($case);
|
||||
$mapped = app(InvestorContext::class)->prefill($context)['desk'];
|
||||
if (count($context['opportunity_choices'] ?? []) > 1 && !$opportunity) $payload['description'] = '';
|
||||
$mapped = app(InvestorContext::class)->prefill($context, $opportunity)['desk'];
|
||||
if ($opportunity) $payload['context_opportunity'] = $opportunity;
|
||||
$payload = app(InvestmentConsumerScope::class)->bind($actor, $caseId, $payload);
|
||||
$serviceFields = json_decode($service->definition, true)['fields'];
|
||||
foreach (['territory_id' => 'territory_id', 'location_source' => 'location_source', 'title' => 'title', 'description' => 'description', 'sector' => 'sector', 'province_id' => 'province_id', 'district_id' => 'district_id', 'admin_post_id' => 'admin_post_id', 'locality_id' => 'locality_id'] as $field => $source) {
|
||||
if (isset($serviceFields[$field],$mapped[$source]) && (empty($payload[$field]) || in_array($field, ['title', 'description'], true) && $context['revision'] > 0)) {
|
||||
@@ -230,6 +233,12 @@ class VirtualDesk
|
||||
$draft = SimulationFormDraft::query()->whereKey($draft->id)->lockForUpdate()->firstOrFail();
|
||||
}
|
||||
$economic = SimulationFormDraft::decryptSnapshot($draft->economic_snapshot);
|
||||
$contextChoices = app(InvestorContext::class)->state($case)['opportunity_choices'] ?? [];
|
||||
if (count($contextChoices) > 1) {
|
||||
$snapshot = json_decode($economic['context_applicability_snapshot'] ?? '{}', true);
|
||||
$draftChoices = $snapshot['opportunity_choices'] ?? [];
|
||||
if (count($draftChoices) !== 1 || ($draftChoices[0]['opportunity']['key'] ?? null) !== ($payload['context_opportunity'] ?? null)) return null;
|
||||
}
|
||||
$description = (string) ($economic['project_summary'] ?? '');
|
||||
if (trim($description) === '') {
|
||||
return null;
|
||||
@@ -250,7 +259,9 @@ class VirtualDesk
|
||||
* @return array<string,mixed> */
|
||||
public function validateFields(array $payload, array $definition, bool $submitting): array
|
||||
{
|
||||
$rules = ['payload' => ['required', 'array:'.implode(',', [...array_keys($definition['fields']), 'country', 'declared_origin_country_code', 'context_reuse'])],
|
||||
$rules = ['payload' => ['required', 'array:'.implode(',', [...array_keys($definition['fields']), 'country', 'declared_origin_country_code', 'context_reuse', 'context_opportunity', 'context_opportunity_snapshot'])],
|
||||
'payload.context_opportunity' => ['nullable', 'string', 'max:100'],
|
||||
'payload.context_opportunity_snapshot' => ['nullable', 'string', 'max:200000'],
|
||||
'payload.context_reuse' => ['nullable', 'string', 'max:10000'],
|
||||
'payload.country' => ['nullable', 'string', 'max:100', app(CountryCatalogue::class)->legacyCountryRule()],
|
||||
'payload.declared_origin_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()]];
|
||||
@@ -334,6 +345,9 @@ class VirtualDesk
|
||||
}
|
||||
}
|
||||
$payload = $this->validateFields($data['payload'], $this->definition($request), $submit);
|
||||
if ($request->status !== 'returned' || in_array('context_opportunity', json_decode($request->editable_fields ?? '[]', true), true)) {
|
||||
$payload = app(InvestmentConsumerScope::class)->bind($actor, $request->case_id, $payload, $submit);
|
||||
}
|
||||
if ($request->status === 'returned') {
|
||||
$editable = json_decode($request->editable_fields ?? '[]', true);
|
||||
$previous = $this->decode($request->payload);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
// Functional configuration expressly defined by Tempo, 23/10/2026.
|
||||
// Codes identify the existing national territorial registry; no geometry is inferred.
|
||||
return [
|
||||
'pemba-lichinga' => ['name' => 'Corredor de Pemba–Lichinga', 'provinces' => ['02', '01']],
|
||||
'nacala' => ['name' => 'Corredor de Nacala', 'provinces' => ['03']],
|
||||
'vale-zambeze' => ['name' => 'Corredor do Vale do Zambeze', 'provinces' => ['05', '04']],
|
||||
'beira' => ['name' => 'Corredor da Beira', 'provinces' => ['07', '06']],
|
||||
'limpopo' => ['name' => 'Corredor do Limpopo', 'provinces' => ['09']],
|
||||
// Tempo's final clarification: both Maputo Province and Maputo City.
|
||||
'maputo' => ['name' => 'Corredor de Maputo', 'provinces' => ['10', '11']],
|
||||
];
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
return [
|
||||
// Explicit opt-in after authenticated installation; never infer activation from schema/ready.
|
||||
'private_preparation' => env('PRIVATE_INVESTMENT_PREPARATION_ENABLED', false),
|
||||
'enabled' => env('INVESTOR_PATH_ENABLED', false),
|
||||
'definition_id' => '61000919-0000-4000-8000-000000000001',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
[
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62413-20170626",
|
||||
"sha256": "b471c1afd7235f359acdcc16a93088f77e1f988706c8d3c4d4f11c5d560aea47",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-73945-20170731",
|
||||
"sha256": "2aa0ce0eb305bfdf93e390a978d3694d0fbc0129034077b68cebce418f878415",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-76474-20170808",
|
||||
"sha256": "8f769aec4715449d056c8c25294060b2ff290ec0893c28e9bdcc4e8092fc75f5",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62414-20170626",
|
||||
"sha256": "a040c0b7cb95dd7f053f4d13662dd58b2b06453b6fa61a2d531d0257a8db8c17",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62418-20170626",
|
||||
"sha256": "e8ccafa68f0d1aa80f2e41d9e09f7b13e7c653a8472a731798cfd5e04ce2963e",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-75002-20170804",
|
||||
"sha256": "7b6fbd998be2b47b96e9a4724560c192c502709fa308d6590c0797ddcdb10544",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-73246-20170728",
|
||||
"sha256": "430ee8674c71104fc126bed2911d676f150ac623857a1ada75662987fd9ac390",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-77232-20170811",
|
||||
"sha256": "27544d3127803b7495bef9fd633147d0225d8c31816c4d59ed05a2edd59de070",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62416-20170626",
|
||||
"sha256": "ac575ce340571192552c89b63f0bd62a84dbc47828e6930133af5ff70e249319",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62929-20170627",
|
||||
"sha256": "488017b062c6760659d9147b7f12aea4afd8d824121adb9462e2c7a2ae58437b",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62649-20170626",
|
||||
"sha256": "38a858713472759a2d20a308bdd9d8afa9cfb76dcaf8d11ca709e89261573e42",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62739-20170626",
|
||||
"sha256": "5870fc0fad9bb7af537b7b1468151cde138b1b886ce846c7be25647b630245dd",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62854-20170627",
|
||||
"sha256": "44b1ba9d593bb625cf53de8909e78eb0b3129742c3ec9d58ec68c984e8930bce",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63128-20170627",
|
||||
"sha256": "74be8f29e5053f53ab1a39a2f63208145c30f23357ca6d78b637dd02bf9c1fb4",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63181-20170627",
|
||||
"sha256": "3d0f9ac14d36156d8178e33f8616d6406f31ea4cd69c8308cbf4227586d7ccb3",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63146-20170627",
|
||||
"sha256": "542353466cc6d82b651619c495f138b0576a4be41d21aad194ae2005050cb1bc",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62688-20170626",
|
||||
"sha256": "a210a789b06d425822f4124f74226a2b27246c086c5ec5f041f1b2237d7c2b65",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63248-20170628",
|
||||
"sha256": "6c30e61197b8568332af6d9b19bbba7ad8665b3873bb031a8c8aedd41536b060",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173220-20260616",
|
||||
"sha256": "5b76c3d84dcf7f9a489686d90e3857ae6536705c918e841310bb42b354e80976",
|
||||
"kind": "setorial",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173222-20260616",
|
||||
"sha256": "e64d7063d1ac4dda028c97e1ba64938d2a9e426a0b630c57ee5df52a41312cc7",
|
||||
"kind": "setorial",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173482-20260915",
|
||||
"sha256": "6f888e133a779c719073246f89bcae150198e5897c0e7d9b7ba75f54c54a7830",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173484-20260915",
|
||||
"sha256": "297df455a9c6fd1f0ab7ca7bbe124ce483c86fe55be26baa695f03089b3ac049",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173478-20260915",
|
||||
"sha256": "d4e320ed99dd9d53d205e47932f73651976d43081d5d39f2bfc2996901408644",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173480-20260915",
|
||||
"sha256": "c3ee6401eda5b582c61969b5e3b74fdf26ef34ca481b1aca8c4c309d845147ae",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173476-20260915",
|
||||
"sha256": "72fbf28391db4804e0637b64e7f59e9fa597812a2681b4562dc3d6dfb4093a30",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173470-20260915",
|
||||
"sha256": "26feeae6b8d04972c7a64ee360583cccdcaeb4b0d8795fd7bc2fd3c38866cc19",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173474-20260915",
|
||||
"sha256": "9fa741a17b746bfa2f3ce7b6ce1d2bb34d3a35d3b58330b0554f9e0a6844f049",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173463-20260915",
|
||||
"sha256": "4f34b7bac4b3030c0614844d379f6e3cfad1b7f6ea3db575a58c33ae033b0e73",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -23,7 +23,7 @@ return [
|
||||
'opportunity_other_qa_subtypes' => [],
|
||||
'operator_name' => env('PORTAL_OPERATOR_NAME'),
|
||||
// Tempo, 19/10/2026: institutional contact; commercial department remains separate.
|
||||
'institutional_contact_email' => env('PORTAL_INSTITUTIONAL_CONTACT_EMAIL', 'info@tempo.co.mz'),
|
||||
'institutional_contact_email' => env('PORTAL_INSTITUTIONAL_CONTACT_EMAIL', 'comercial@tempo.co.mz'),
|
||||
'support' => [
|
||||
'email' => env('PORTAL_SUPPORT_EMAIL'),
|
||||
'phone' => env('PORTAL_SUPPORT_PHONE'),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'enabled' => (bool) env('PROCUREMENT_ENABLED', false),
|
||||
'regimes' => ['state' => 'Contratação pública do Estado', 'external' => 'Financiamento externo', 'private' => 'Concurso privado', 'other' => 'Outro regime documentado'],
|
||||
'acts' => ['initial' => 'Anúncio inicial', 'clarification' => 'Esclarecimento', 'correction' => 'Rectificação', 'extension' => 'Prorrogação', 'award' => 'Resultado / adjudicação', 'cancellation' => 'Cancelamento', 'other' => 'Outro acto'],
|
||||
];
|
||||
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"source": "investimentos nos corredores.docx",
|
||||
"source_sha256": "1473b06c48aa234e771660e3f2a51b9d2e4e1943bd52d878e0ae854ee6657055",
|
||||
"introduction": "Os corredores de desenvolvimento integram cadeias de valor prioritárias que podem ser apresentadas no portal InvesteMoz como oportunidades concretas de investimento na produção, transformação, logística e comercialização. Para cada corredor, são destacadas abaixo as oportunidades associadas aos produtos com maior potencial.",
|
||||
"segments": [
|
||||
"Produção primária e fornecimento de insumos",
|
||||
"Processamento e transformação agro-industrial",
|
||||
"Armazenagem, cadeia de frio e logística",
|
||||
"Embalagem, distribuição, comercialização e exportação",
|
||||
"Infraestruturas e serviços de apoio às cadeias de valor"
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"name": "Corredor de Pemba–Lichinga",
|
||||
"opportunities": [
|
||||
{
|
||||
"chain": "Batata, trigo, feijão, milho e soja",
|
||||
"description": "Produção comercial, armazenamento e processamento de batata, trigo, feijão, milho e soja."
|
||||
},
|
||||
{
|
||||
"chain": "Algodão",
|
||||
"description": "Descaroçamento, fiação e produção de óleo e derivados de algodão."
|
||||
},
|
||||
{
|
||||
"chain": "Sésamo",
|
||||
"description": "Limpeza, prensagem, embalagem e exportação de sésamo."
|
||||
},
|
||||
{
|
||||
"chain": "Florestas",
|
||||
"description": "Viveiros, plantações comerciais, madeira processada e produtos florestais sustentáveis."
|
||||
},
|
||||
{
|
||||
"chain": "Avicultura",
|
||||
"description": "Produção de rações, incubação, criação, abate e cadeia de frio para avicultura."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Corredor de Nacala",
|
||||
"opportunities": [
|
||||
{
|
||||
"chain": "Mandioca",
|
||||
"description": "Produção e transformação de mandioca em farinha, amido e outros derivados."
|
||||
},
|
||||
{
|
||||
"chain": "Milho, amendoim e sésamo",
|
||||
"description": "Produção, armazenagem e processamento de milho, amendoim e sésamo."
|
||||
},
|
||||
{
|
||||
"chain": "Algodão",
|
||||
"description": "Descaroçamento e transformação de algodão."
|
||||
},
|
||||
{
|
||||
"chain": "Frutas e caju",
|
||||
"description": "Pomares, processamento, embalagem e exportação de frutas e caju."
|
||||
},
|
||||
{
|
||||
"chain": "Florestas",
|
||||
"description": "Silvicultura comercial, madeira processada e produtos florestais."
|
||||
},
|
||||
{
|
||||
"chain": "Avicultura",
|
||||
"description": "Produção de rações, criação, abate e distribuição de produtos avícolas."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Corredor do Vale do Zambeze",
|
||||
"opportunities": [
|
||||
{
|
||||
"chain": "Arroz",
|
||||
"description": "Produção irrigada, moagem, embalagem e comercialização de arroz."
|
||||
},
|
||||
{
|
||||
"chain": "Milho e batata",
|
||||
"description": "Produção, secagem, armazenagem e transformação de milho e batata."
|
||||
},
|
||||
{
|
||||
"chain": "Sésamo",
|
||||
"description": "Limpeza, prensagem e exportação de sésamo."
|
||||
},
|
||||
{
|
||||
"chain": "Algodão",
|
||||
"description": "Descaroçamento e transformação de algodão."
|
||||
},
|
||||
{
|
||||
"chain": "Avicultura",
|
||||
"description": "Produção de rações, incubação, criação e abate de aves."
|
||||
},
|
||||
{
|
||||
"chain": "Bovinos e caprinos",
|
||||
"description": "Criação, engorda, abate, processamento de carne e cadeia de frio para bovinos e caprinos."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Corredor da Beira",
|
||||
"opportunities": [
|
||||
{
|
||||
"chain": "Milho, trigo e arroz",
|
||||
"description": "Produção, silos, moagem e processamento de milho, trigo e arroz."
|
||||
},
|
||||
{
|
||||
"chain": "Soja",
|
||||
"description": "Esmagamento de soja, produção de óleo vegetal e fabrico de rações."
|
||||
},
|
||||
{
|
||||
"chain": "Hortícolas",
|
||||
"description": "Produção irrigada, conservação, embalagem e distribuição de hortícolas."
|
||||
},
|
||||
{
|
||||
"chain": "Sésamo",
|
||||
"description": "Limpeza, prensagem e exportação de sésamo."
|
||||
},
|
||||
{
|
||||
"chain": "Açúcar",
|
||||
"description": "Produção e refinação de açúcar e aproveitamento de subprodutos."
|
||||
},
|
||||
{
|
||||
"chain": "Florestas",
|
||||
"description": "Plantações comerciais, processamento de madeira e produtos florestais."
|
||||
},
|
||||
{
|
||||
"chain": "Carne bovina",
|
||||
"description": "Criação, engorda, abate, processamento e cadeia de frio para carne bovina."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Corredor do Limpopo",
|
||||
"opportunities": [
|
||||
{
|
||||
"chain": "Arroz",
|
||||
"description": "Produção irrigada, moagem, embalagem e comercialização de arroz."
|
||||
},
|
||||
{
|
||||
"chain": "Hortícolas",
|
||||
"description": "Produção em estufas e campo aberto, conservação, embalagem e distribuição de hortícolas."
|
||||
},
|
||||
{
|
||||
"chain": "Carne vermelha",
|
||||
"description": "Criação, engorda, abate, processamento e cadeia de frio para carne vermelha."
|
||||
},
|
||||
{
|
||||
"chain": "Avicultura",
|
||||
"description": "Produção de rações, incubação, criação, abate e distribuição de produtos avícolas."
|
||||
},
|
||||
{
|
||||
"chain": "Infraestruturas e serviços de apoio",
|
||||
"description": "Infraestruturas de irrigação, armazenagem, transporte refrigerado e centros de comercialização."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Corredor de Maputo",
|
||||
"opportunities": [
|
||||
{
|
||||
"chain": "Arroz",
|
||||
"description": "Produção irrigada, moagem, embalagem e fornecimento de arroz aos mercados urbanos e regionais."
|
||||
},
|
||||
{
|
||||
"chain": "Hortícolas",
|
||||
"description": "Produção, conservação, embalagem, transformação e distribuição de hortícolas."
|
||||
},
|
||||
{
|
||||
"chain": "Carne vermelha",
|
||||
"description": "Criação, engorda, abate, processamento, distribuição e exportação regional de carne vermelha."
|
||||
},
|
||||
{
|
||||
"chain": "Avicultura",
|
||||
"description": "Produção de rações, incubação, criação, abate e comercialização de produtos avícolas."
|
||||
},
|
||||
{
|
||||
"chain": "Infraestruturas e serviços de apoio",
|
||||
"description": "Plataformas logísticas, centros de distribuição, cadeia de frio e serviços de exportação."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"sector": "Agronegócio"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('portal_tenders', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary();
|
||||
$t->jsonb('draft');
|
||||
$t->unsignedInteger('version')->default(1);
|
||||
$t->uuid('published_version_id')->nullable();
|
||||
$t->foreignId('created_by')->constrained('users')->restrictOnDelete();
|
||||
$t->timestampsTz();
|
||||
});
|
||||
Schema::create('portal_tender_versions', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignUuid('tender_id')->constrained('portal_tenders')->restrictOnDelete();
|
||||
$t->unsignedInteger('version');
|
||||
$t->string('act', 24);
|
||||
$t->jsonb('payload');
|
||||
$t->text('reason');
|
||||
$t->foreignId('actor_id')->constrained('users')->restrictOnDelete();
|
||||
$t->timestampTz('published_at')->nullable();
|
||||
$t->timestampTz('created_at')->useCurrent();
|
||||
$t->unique(['tender_id', 'version']);
|
||||
});
|
||||
Schema::create('portal_tender_watches', function (Blueprint $t) {
|
||||
$t->foreignUuid('tender_id')->constrained('portal_tenders')->restrictOnDelete();
|
||||
$t->foreignId('user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->uuid('seen_version_id')->nullable();
|
||||
$t->timestampsTz();
|
||||
$t->primary(['tender_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::table('portal_tenders')->exists()) {
|
||||
throw new RuntimeException('Preserve procedures, acts and history; use a reviewed data reversal.');
|
||||
}
|
||||
Schema::dropIfExists('portal_tender_watches');
|
||||
Schema::dropIfExists('portal_tender_versions');
|
||||
Schema::dropIfExists('portal_tenders');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM node:22-bookworm AS node
|
||||
|
||||
FROM php:8.4-cli-bookworm
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libpq-dev libzip-dev unzip git curl \
|
||||
&& docker-php-ext-install -j"$(nproc)" pdo_pgsql pgsql zip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=node /usr/local/bin/node /usr/local/bin/node
|
||||
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
||||
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
|
||||
&& ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
COPY . .
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM php:8.4-cli-bookworm
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libpq-dev libzip-dev unzip git curl \
|
||||
&& docker-php-ext-install -j"$(nproc)" pdo_pgsql pgsql zip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
WORKDIR /var/www/html
|
||||
COPY composer.json composer.lock ./
|
||||
RUN composer install --no-interaction --prefer-dist --no-progress --no-scripts
|
||||
COPY . .
|
||||
RUN composer dump-autoload --optimize
|
||||
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>InvestMoz — Levantamento funcional e modelos</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 18mm 17mm 18mm 17mm; }
|
||||
body { font-family: Aptos, Calibri, Arial, sans-serif; color:#1d2939; font-size:10.5pt; line-height:1.42; }
|
||||
h1 { color:#123d68; font-size:26pt; margin:0 0 8pt; }
|
||||
h2 { color:#123d68; font-size:17pt; border-bottom:2px solid #13a789; padding-bottom:5pt; margin-top:24pt; }
|
||||
h3 { color:#145b82; font-size:13pt; margin:17pt 0 5pt; }
|
||||
p { margin:5pt 0 8pt; }
|
||||
.cover { padding-top:100pt; page-break-after:always; }
|
||||
.kicker { text-transform:uppercase; letter-spacing:2px; color:#138a77; font-weight:bold; }
|
||||
.subtitle { color:#52657a; font-size:15pt; }
|
||||
.meta { margin-top:35pt; padding:12pt; background:#eff6fb; border-left:4px solid #1d75bd; }
|
||||
.callout { background:#fff7e8; border-left:4px solid #ee9b20; padding:9pt 12pt; margin:12pt 0; }
|
||||
.note { color:#52657a; font-size:9pt; }
|
||||
table { border-collapse:collapse; width:100%; margin:8pt 0 14pt; font-size:9.2pt; }
|
||||
th { background:#123d68; color:white; text-align:left; padding:6pt; }
|
||||
td { border:1px solid #cbd5e1; padding:5pt 6pt; vertical-align:top; }
|
||||
tr:nth-child(even) td { background:#f5f8fb; }
|
||||
.diagram { text-align:center; margin:12pt 0; page-break-inside:avoid; }
|
||||
.diagram svg { width:100%; height:auto; }
|
||||
.legend { color:#52657a; font-size:9pt; }
|
||||
.small { font-size:9pt; }
|
||||
.pagebreak { page-break-before:always; }
|
||||
ul { margin-top:3pt; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section class="cover">
|
||||
<p class="kicker">Documento de análise funcional</p>
|
||||
<h1>InvestMoz</h1>
|
||||
<p class="subtitle">Levantamento profissional do projecto, casos de uso reais, actores e diagramas</p>
|
||||
<div class="meta"><b>Data:</b> 9 de Outubro de 2026<br><b>Âmbito:</b> aplicação Laravel/React e documentação do repositório Investidor-V2<br><b>Versão:</b> 1.0 · análise do código e dos documentos disponíveis</div>
|
||||
<p style="margin-top:42pt;font-size:13pt">Portal de descoberta de oportunidades e serviços de investimento, ligado a fluxos autenticados de candidatura, tratamento institucional, evidência documental e acompanhamento.</p>
|
||||
<p class="note" style="margin-top:55pt">Documento técnico-funcional. A presença de uma rota, tabela, job ou descrição não demonstra, por si só, activação em produção.</p>
|
||||
</section>
|
||||
|
||||
<h2>1. Sumário executivo</h2>
|
||||
<p>O InvestMoz é uma plataforma digital que aproxima investidores, promotores e instituições. A experiência começa no portal público, onde se consultam sectores, oportunidades, negócios existentes, território, zonas e serviços. A partir daí, a pessoa pode manifestar interesse, iniciar uma simulação ou pedir apoio. Os percursos que exigem continuidade passam para a área autenticada, onde se mantêm perfil, rascunhos, candidaturas, anexos e comunicação.</p>
|
||||
<p>Do lado institucional, os pedidos podem ser revistos e acompanhados por CRM local, tarefas, revisões e eventos. A plataforma também contém domínios de catálogo territorial, zonas de investimento, documentação, governação fiscal, educação e competências, assistência de terra, parcerias, balcão virtual e IA baseada em fontes.</p>
|
||||
<div class="callout"><b>Leitura funcional:</b> trata-se de uma plataforma de encaminhamento e gestão de jornadas de investimento, e não apenas de um catálogo de anúncios. A camada pública informa e capta pedidos; a camada autenticada conserva o trabalho; a camada de gestão analisa, valida e decide segundo permissões e evidência.</div>
|
||||
<p>O README descreve a base inicial de sectores, oportunidades e manifestações de interesse. Rotas, controladores, migrations e documentação de domínio mostram uma solução mais abrangente. O levantamento de casos detalhados tem 24 casos de uso; este relatório acrescenta cenários concretos, diagramas UML/ER e a leitura da frequência dos eventos.</p>
|
||||
|
||||
<h2>2. O que o projecto faz</h2>
|
||||
<table><tr><th>Capacidade</th><th>Valor para o utilizador</th><th>Exemplos de dados</th></tr>
|
||||
<tr><td>Descoberta pública</td><td>Encontrar oportunidades, negócios, sectores, serviços e referências territoriais.</td><td>sectors, opportunities, existing_businesses, investment_zones</td></tr>
|
||||
<tr><td>Captação de interesse</td><td>Enviar manifestação associada a uma oportunidade ou negócio, com consentimento.</td><td>interest_requests, consents, receipts</td></tr>
|
||||
<tr><td>Jornada do investidor</td><td>Guardar itens, criar dossier/submissão, anexar documentos e acompanhar mensagens.</td><td>investment_submissions, revisions, messages, attachments</td></tr>
|
||||
<tr><td>Tratamento institucional</td><td>Qualificar pedidos, atribuir responsável, criar tarefas e acompanhar a negociação.</td><td>leads, contacts, crm_opportunities, tasks, activities</td></tr>
|
||||
<tr><td>Planeamento territorial</td><td>Relacionar projectos, clusters, corredores, localizações e zonas.</td><td>projects, economic_clusters, development_corridors, geo_locations</td></tr>
|
||||
<tr><td>Documentos e evidência</td><td>Preservar origem, versões, hash, scan, classificação e autorização de divulgação.</td><td>documents, document_versions, malware_scans, associations</td></tr>
|
||||
<tr><td>Simulação e regras fiscais</td><td>Guardar rascunhos, executar simulações com parâmetros/fontes rastreáveis e governar versões.</td><td>simulations, scenarios, results, tax_rule_versions, legal_sources</td></tr>
|
||||
<tr><td>Serviços complementares</td><td>Registar educação, assistência de terra, parcerias, balcão virtual e apoio com IA.</td><td>training needs, land cases, partnerships, desk cases, ai runs</td></tr></table>
|
||||
|
||||
<h2>3. Actores e fronteiras de acesso</h2>
|
||||
<table><tr><th>Actor</th><th>O que pretende</th><th>Zona típica</th></tr>
|
||||
<tr><td>Visitante</td><td>Consultar conteúdos públicos e iniciar interesse, apoio ou simulação.</td><td>Portal público</td></tr>
|
||||
<tr><td>Investidor / utilizador registado</td><td>Manter perfil, favoritos, rascunhos, submissões, anexos e mensagens.</td><td>Área autenticada</td></tr>
|
||||
<tr><td>Promotor / proprietário de negócio</td><td>Registar ou actualizar negócio e responder a pedidos de informação.</td><td>Área autenticada e workflow de revisão</td></tr>
|
||||
<tr><td>Gestor de portal / CRM</td><td>Rever interesses, qualificar leads, atribuir tarefas e gerir conteúdos.</td><td>Gestão interna</td></tr>
|
||||
<tr><td>Gestor territorial</td><td>Manter projectos, relações, zonas, geometria e referências territoriais.</td><td>Gestão interna</td></tr>
|
||||
<tr><td>Revisor documental / gestor fiscal</td><td>Validar metadados, evidência, versões e regras sob as respectivas funções.</td><td>Gestão restrita</td></tr>
|
||||
<tr><td>Agente de serviço / parceiro institucional</td><td>Tratar casos ou colaborar em parcerias e salas.</td><td>Serviços e colaboração</td></tr>
|
||||
<tr><td>Administrador / worker</td><td>Gerir segurança/configuração ou executar trabalho assíncrono autorizado.</td><td>Administração / sistema</td></tr></table>
|
||||
<p class="note">Os nomes acima são papéis funcionais consolidados. A autorização efectiva depende das policies, middleware, MFA, configuração e permissões em vigor.</p>
|
||||
|
||||
<h2>4. Diagrama de casos de uso com actores</h2>
|
||||
<p>O diagrama destaca actores (“bonecos”) e os principais serviços que usam. As associações mostram interacções típicas; não indicam que todos os serviços estejam activos na mesma instalação.</p>
|
||||
<div class="diagram">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1050 610" role="img" aria-label="Diagrama UML de casos de uso InvestMoz com actores">
|
||||
<defs><style>.actor{stroke:#173b60;stroke-width:3;fill:none}.line{stroke:#728397;stroke-width:1.7}.uc{fill:#eef6fc;stroke:#2778b8;stroke-width:2}.txt{font:16px Arial;fill:#17324d}.label{font:bold 15px Arial;fill:#17324d}.boundary{fill:#fbfdff;stroke:#9bb0c3;stroke-width:2;stroke-dasharray:8 5}</style></defs>
|
||||
<rect x="220" y="22" width="795" height="558" rx="14" class="boundary"/><text x="245" y="52" class="label">Sistema InvestMoz</text>
|
||||
<!-- stick actors -->
|
||||
<g class="actor"><circle cx="95" cy="115" r="17"/><path d="M95 132v65m-30-38h60m-30 38-27 42m27-42 27 42"/></g><text x="50" y="245" class="label">Visitante</text>
|
||||
<g class="actor"><circle cx="95" cy="335" r="17"/><path d="M95 352v65m-30-38h60m-30 38-27 42m27-42 27 42"/></g><text x="37" y="465" class="label">Investidor</text>
|
||||
<g class="actor"><circle cx="95" cy="510" r="17"/><path d="M95 527v45m-30-25h60m-30 25-22 33m22-33 22 33"/></g><text x="15" y="606" class="label">Gestor</text>
|
||||
<g class="actor"><circle cx="1025" cy="165" r="15"/><path d="M1025 180v55m-25-33h50m-25 33-22 36m22-36 22 36"/></g><text x="979" y="267" class="label">Revisor</text>
|
||||
<g class="actor"><circle cx="1025" cy="400" r="15"/><path d="M1025 415v55m-25-33h50m-25 33-22 36m22-36 22 36"/></g><text x="967" y="502" class="label">Parceiro</text>
|
||||
<!-- use cases -->
|
||||
<g><ellipse cx="410" cy="112" rx="145" ry="31" class="uc"/><text x="410" y="118" text-anchor="middle" class="txt">Consultar catálogo e mapa</text>
|
||||
<ellipse cx="730" cy="112" rx="145" ry="31" class="uc"/><text x="730" y="118" text-anchor="middle" class="txt">Pesquisar oportunidade/negócio</text>
|
||||
<ellipse cx="410" cy="205" rx="145" ry="31" class="uc"/><text x="410" y="211" text-anchor="middle" class="txt">Manifestar interesse</text>
|
||||
<ellipse cx="730" cy="205" rx="145" ry="31" class="uc"/><text x="730" y="211" text-anchor="middle" class="txt">Executar simulação</text>
|
||||
<ellipse cx="410" cy="300" rx="145" ry="31" class="uc"/><text x="410" y="306" text-anchor="middle" class="txt">Criar candidatura / dossier</text>
|
||||
<ellipse cx="730" cy="300" rx="145" ry="31" class="uc"/><text x="730" y="306" text-anchor="middle" class="txt">Anexar e acompanhar documentos</text>
|
||||
<ellipse cx="410" cy="397" rx="145" ry="31" class="uc"/><text x="410" y="403" text-anchor="middle" class="txt">Tratar interesse no CRM</text>
|
||||
<ellipse cx="730" cy="397" rx="145" ry="31" class="uc"/><text x="730" y="403" text-anchor="middle" class="txt">Validar e publicar conteúdo</text>
|
||||
<ellipse cx="410" cy="495" rx="145" ry="31" class="uc"/><text x="410" y="501" text-anchor="middle" class="txt">Gerir território e zonas</text>
|
||||
<ellipse cx="730" cy="495" rx="145" ry="31" class="uc"/><text x="730" y="501" text-anchor="middle" class="txt">Colaborar em parceria/serviço</text></g>
|
||||
<!-- actor associations -->
|
||||
<g class="line"><path d="M112 115H264M112 115 585 112M112 145 265 195M112 355 265 300M112 370 265 300M112 388 585 300M112 515 265 397M112 525 265 495M112 540 585 495M1008 175 875 397M1008 190 875 300M1008 410 875 495"/></g>
|
||||
</svg></div>
|
||||
<p class="legend">Exemplo do percurso mais comum: visitante pesquisa → manifesta interesse → investidor cria/acompanha candidatura → gestão trata o pedido → revisão e decisão.</p>
|
||||
|
||||
<h2>5. Casos de uso reais — jornadas ilustrativas</h2>
|
||||
<h3>UC-A — Investidor encontra oportunidade e manifesta interesse</h3>
|
||||
<ol><li>O visitante consulta sectores, filtra oportunidades por localização e abre o detalhe.</li><li>Escolhe “manifestar interesse”, informa contacto, modalidade e mensagem, e aceita o consentimento aplicável.</li><li>O sistema valida o alvo, regista o pedido e, quando configurado, um recibo/idempotência.</li><li>O investidor pode autenticar-se para guardar a oportunidade ou avançar para uma submissão.</li><li>Um gestor consulta o pedido, verifica consentimentos, atribui responsável, qualifica e cria tarefa de seguimento.</li><li>As alterações relevantes ficam no histórico; notificações e integrações só são consideradas entregues quando o disparo e o serviço estiverem confirmados.</li></ol>
|
||||
<p><b>Resultado:</b> pedido rastreável ligado à oportunidade e encaminhado para tratamento. <b>Entidades:</b> Opportunity, Investor/User, InterestRequest, Consent, Lead, Task, AuditEvent.</p>
|
||||
<h3>UC-B — Proprietário submete um negócio para divulgação</h3>
|
||||
<ol><li>O proprietário cria rascunho e informa modalidade, objectos, localização e documentos de suporte.</li><li>Submete o registo; o estado passa para revisão conforme o workflow aplicável.</li><li>O gestor analisa e pode aprovar, rejeitar ou pedir informação complementar.</li><li>O revisor documental confere original, hash, versão, scan e autorização de divulgação.</li><li>Só após as validações necessárias o conteúdo pode ser publicado ao público; publicação não altera a situação jurídica do documento.</li></ol>
|
||||
<p><b>Resultado:</b> negócio com histórico e decisão rastreável. <b>Estados documentados:</b> draft → submitted → in_review → approved → published, com ramos needs_information, rejected, withdrawn, suspended e archived.</p>
|
||||
<h3>UC-C — Gestor territorial valida zona e relações</h3>
|
||||
<ol><li>O gestor regista a zona e a referência/fonte pertinente.</li><li>Associa localização, operador, categoria, geometria e relações com projectos, clusters ou corredores.</li><li>As relações sem evidência verificada permanecem pendentes e identificadas.</li><li>Um revisor valida a evidência e as regras de publicação; alterações ficam versionadas.</li><li>A zona só aparece no portal conforme estado e autorização de divulgação.</li></ol>
|
||||
<h3>UC-D — Utilizador guarda um rascunho e executa simulação</h3>
|
||||
<ol><li>O utilizador preenche o formulário; os dados incompletos podem ser guardados como rascunho recuperável.</li><li>Quando solicita o cálculo, o sistema selecciona questionário e parâmetros elegíveis.</li><li>O job de simulação executa após a transacção e grava cenários, resultados, obrigações e fontes usadas.</li><li>O relatório pode ser consultado; o rascunho continua distinto do cálculo executado.</li></ol>
|
||||
<div class="callout"><b>Regra de integridade:</b> importar ou publicar fonte documental não activa automaticamente uma regra fiscal. Parâmetros ausentes/ambíguos permanecem indeterminados.</div>
|
||||
<h3>UC-E — Parceiros colaboram numa parceria ou serviço</h3>
|
||||
<p>Um actor cria uma proposta; outro manifesta interesse; após aceitação, os participantes podem colaborar numa sala, trocar mensagens/documentos e acompanhar marcos. O balcão virtual segue padrão semelhante para pedidos de serviço: registo, análise, pedido de documentos, eventos, revisão e resolução.</p>
|
||||
|
||||
<h2>6. Diagrama de classes (visão lógica)</h2>
|
||||
<p>O diagrama apresenta as classes de domínio principais, atributos representativos e operações. É uma vista funcional; não reproduz todos os campos nem métodos Laravel.</p>
|
||||
<div class="diagram"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 690" role="img" aria-label="Diagrama UML de classes InvestMoz">
|
||||
<defs><style>.box{fill:#fff;stroke:#276c9b;stroke-width:2}.head{fill:#e8f2f9}.ct{font:14px Arial;fill:#17324d}.ch{font:bold 15px Arial;fill:#123d68}.rel{stroke:#60768b;stroke-width:1.8;fill:none}.mult{font:12px Arial;fill:#455b70}</style></defs>
|
||||
<!-- classes -->
|
||||
<g><rect x="30" y="30" width="220" height="120" rx="5" class="box"/><rect x="30" y="30" width="220" height="32" class="head"/><text x="140" y="52" text-anchor="middle" class="ch">User</text><text x="44" y="82" class="ct">id, email, verified_at</text><text x="44" y="105" class="ct">2FA / passkeys</text><text x="44" y="132" class="ct">+ authenticate()</text>
|
||||
<rect x="315" y="30" width="220" height="120" rx="5" class="box"/><rect x="315" y="30" width="220" height="32" class="head"/><text x="425" y="52" text-anchor="middle" class="ch">Investor</text><text x="329" y="82" class="ct">id, user_id, profile</text><text x="329" y="105" class="ct">company interests</text><text x="329" y="132" class="ct">+ manageProfile()</text>
|
||||
<rect x="600" y="30" width="220" height="120" rx="5" class="box"/><rect x="600" y="30" width="220" height="32" class="head"/><text x="710" y="52" text-anchor="middle" class="ch">Opportunity</text><text x="614" y="82" class="ct">sector, slug, status</text><text x="614" y="105" class="ct">location, investment</text><text x="614" y="132" class="ct">+ publish()</text>
|
||||
<rect x="865" y="30" width="205" height="120" rx="5" class="box"/><rect x="865" y="30" width="205" height="32" class="head"/><text x="967" y="52" text-anchor="middle" class="ch">Sector</text><text x="879" y="82" class="ct">id, name, slug</text><text x="879" y="105" class="ct">active / published</text><text x="879" y="132" class="ct">+ listOpportunities()</text>
|
||||
<rect x="30" y="230" width="235" height="130" rx="5" class="box"/><rect x="30" y="230" width="235" height="32" class="head"/><text x="147" y="252" text-anchor="middle" class="ch">InterestRequest</text><text x="44" y="282" class="ct">target, contact, consent</text><text x="44" y="305" class="ct">status, submitted_at</text><text x="44" y="338" class="ct">+ submit() / qualify()</text>
|
||||
<rect x="325" y="230" width="235" height="130" rx="5" class="box"/><rect x="325" y="230" width="235" height="32" class="head"/><text x="442" y="252" text-anchor="middle" class="ch">Lead / Task</text><text x="339" y="282" class="ct">owner, next_action</text><text x="339" y="305" class="ct">due_at, status</text><text x="339" y="338" class="ct">+ assign() / complete()</text>
|
||||
<rect x="620" y="230" width="235" height="130" rx="5" class="box"/><rect x="620" y="230" width="235" height="32" class="head"/><text x="737" y="252" text-anchor="middle" class="ch">Submission</text><text x="634" y="282" class="ct">owner, target, state</text><text x="634" y="305" class="ct">revisions, needs</text><text x="634" y="338" class="ct">+ save() / submit()</text>
|
||||
<rect x="910" y="230" width="160" height="130" rx="5" class="box"/><rect x="910" y="230" width="160" height="32" class="head"/><text x="990" y="252" text-anchor="middle" class="ch">Document</text><text x="924" y="282" class="ct">hash, source</text><text x="924" y="305" class="ct">version, status</text><text x="924" y="338" class="ct">+ scan()</text>
|
||||
<rect x="30" y="450" width="235" height="130" rx="5" class="box"/><rect x="30" y="450" width="235" height="32" class="head"/><text x="147" y="472" text-anchor="middle" class="ch">Project / Zone</text><text x="44" y="502" class="ct">territory, geometry</text><text x="44" y="525" class="ct">relations, evidence</text><text x="44" y="558" class="ct">+ validate() / publish()</text>
|
||||
<rect x="325" y="450" width="235" height="130" rx="5" class="box"/><rect x="325" y="450" width="235" height="32" class="head"/><text x="442" y="472" text-anchor="middle" class="ch">Simulation</text><text x="339" y="502" class="ct">questionnaire, input</text><text x="339" y="525" class="ct">scenario, result, sources</text><text x="339" y="558" class="ct">+ execute() / report()</text>
|
||||
<rect x="620" y="450" width="235" height="130" rx="5" class="box"/><rect x="620" y="450" width="235" height="32" class="head"/><text x="737" y="472" text-anchor="middle" class="ch">TaxRuleVersion</text><text x="634" y="502" class="ct">source, dates, parameters</text><text x="634" y="525" class="ct">review / approval state</text><text x="634" y="558" class="ct">+ resolveForPeriod()</text>
|
||||
<rect x="910" y="450" width="160" height="130" rx="5" class="box"/><rect x="910" y="450" width="160" height="32" class="head"/><text x="990" y="472" text-anchor="middle" class="ch">AuditEvent</text><text x="924" y="502" class="ct">actor, entity</text><text x="924" y="525" class="ct">action, time</text><text x="924" y="558" class="ct">+ record()</text></g>
|
||||
<!-- relations -->
|
||||
<g class="rel"><path d="M250 90H315M535 90H600M820 90H865M140 150V230M425 150V230M710 150V230M265 295H325M560 295H620M855 295H910M140 360V450M442 360V450M737 360V450M855 515H910M250 130 900 470"/></g>
|
||||
<g class="mult"><text x="270" y="82">1 : 0..1</text><text x="547" y="82">0..*</text><text x="834" y="82">1</text><text x="148" y="205">1 : 0..*</text><text x="273" y="286">0..1</text><text x="567" y="286">1..*</text><text x="861" y="286">0..*</text><text x="145" y="435">1 : 0..*</text><text x="450" y="435">1 : 0..*</text></g>
|
||||
</svg></div>
|
||||
<p class="note">A relação exacta Investor/User, a associação Submission/target e os vínculos Document/entidade podem ser opcionais ou implementados por tabelas polimórficas/de junção; confirme no schema antes de gerar código ou migrações.</p>
|
||||
|
||||
<h2>7. Diagrama de relacionamento (ER simplificado)</h2>
|
||||
<div class="diagram"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 560" role="img" aria-label="Diagrama de relacionamento das entidades InvestMoz">
|
||||
<defs><style>.e{fill:#edf5fb;stroke:#2477b6;stroke-width:2}.et{font:bold 15px Arial;fill:#123d68}.r{stroke:#61788d;stroke-width:2;fill:none}.rt{font:12px Arial;fill:#40566b}</style></defs>
|
||||
<g><rect x="35" y="45" width="155" height="60" rx="9" class="e"/><text x="112" y="81" text-anchor="middle" class="et">SECTOR</text><rect x="260" y="45" width="180" height="60" rx="9" class="e"/><text x="350" y="81" text-anchor="middle" class="et">OPPORTUNITY</text><rect x="515" y="45" width="190" height="60" rx="9" class="e"/><text x="610" y="81" text-anchor="middle" class="et">INTEREST_REQUEST</text><rect x="805" y="45" width="160" height="60" rx="9" class="e"/><text x="885" y="81" text-anchor="middle" class="et">INVESTOR</text>
|
||||
<rect x="35" y="205" width="155" height="60" rx="9" class="e"/><text x="112" y="241" text-anchor="middle" class="et">USER</text><rect x="260" y="205" width="180" height="60" rx="9" class="e"/><text x="350" y="241" text-anchor="middle" class="et">SUBMISSION</text><rect x="515" y="205" width="190" height="60" rx="9" class="e"/><text x="610" y="241" text-anchor="middle" class="et">DOCUMENT</text><rect x="805" y="205" width="160" height="60" rx="9" class="e"/><text x="885" y="241" text-anchor="middle" class="et">AUDIT_EVENT</text>
|
||||
<rect x="35" y="365" width="155" height="60" rx="9" class="e"/><text x="112" y="401" text-anchor="middle" class="et">PROJECT</text><rect x="260" y="365" width="180" height="60" rx="9" class="e"/><text x="350" y="401" text-anchor="middle" class="et">CLUSTER</text><rect x="515" y="365" width="190" height="60" rx="9" class="e"/><text x="610" y="401" text-anchor="middle" class="et">INVESTMENT_ZONE</text><rect x="805" y="365" width="160" height="60" rx="9" class="e"/><text x="885" y="401" text-anchor="middle" class="et">LEGAL_SOURCE</text>
|
||||
<rect x="145" y="490" width="180" height="55" rx="9" class="e"/><text x="235" y="523" text-anchor="middle" class="et">SIMULATION</text><rect x="450" y="490" width="180" height="55" rx="9" class="e"/><text x="540" y="523" text-anchor="middle" class="et">TAX_RULE_VERSION</text><rect x="755" y="490" width="180" height="55" rx="9" class="e"/><text x="845" y="523" text-anchor="middle" class="et">SIMULATION_RESULT</text></g>
|
||||
<g class="r"><path d="M190 75H260M440 75H515M705 75H805M190 235H260M440 235H515M705 235H805M190 395H260M440 395H515M705 395H805M235 425V490M325 518H450M630 518H755M350 105V205M610 105V205M610 265V365M885 105V205M885 425V455H540V490"/></g>
|
||||
<g class="rt"><text x="210" y="67">1 : N</text><text x="465" y="67">1 : N</text><text x="730" y="67">N : 0..1</text><text x="210" y="227">1 : N</text><text x="462" y="227">N : M* via links</text><text x="730" y="227">1 : N</text><text x="210" y="387">N : M*</text><text x="462" y="387">N : M*</text><text x="730" y="387">1 : N</text><text x="235" y="466">1 : N</text><text x="346" y="510">1 : N</text><text x="650" y="510">1 : N</text><text x="354" y="160">1 : N</text><text x="617" y="160">association</text><text x="894" y="160">actor</text><text x="670" y="448">source</text></g>
|
||||
</svg></div>
|
||||
<p class="note">N:M = muitos-para-muitos, normalmente resolvido por tabela de associação. Ligações genéricas de documentos/auditoria podem não ser FKs directas. Este mapa é uma vista funcional, não um substituto do ERD integral em docs/investmoz/erd.mmd e do dicionário de dados.</p>
|
||||
|
||||
<h2>8. Eventos e frequência</h2>
|
||||
<table><tr><th>Evento/processo</th><th>Gatilho</th><th>Frequência confirmada</th><th>Leitura operacional</th></tr>
|
||||
<tr><td>Escalar tarefas vencidas</td><td>Scheduler Laravel</td><td>A cada 15 minutos; sem sobreposição configurada</td><td>Cadência declarada em routes/console.php. O scheduler do servidor tem de estar activo.</td></tr>
|
||||
<tr><td>Executar simulação (RunSimulation)</td><td>Pedido de cálculo aceite</td><td>Por pedido, via fila, afterCommit</td><td>Assíncrono; volume depende da utilização e dos workers.</td></tr>
|
||||
<tr><td>Scan de malware (ScanDocumentForMalware)</td><td>Upload ou pedido de análise em fluxos suportados</td><td>Por documento, via fila; job único</td><td>Depende de worker e scanner. Upload não prova análise concluída.</td></tr>
|
||||
<tr><td>Comparação de compatibilidade MNE</td><td>Alteração aplicável do negócio</td><td>Por alteração aplicável, em fila de sombra</td><td>Job identificado; frequência de alterações não medida.</td></tr>
|
||||
<tr><td>Notificação de interesse</td><td>Recepção de interesse</td><td>Disparo não confirmado</td><td>Job NotifyInterestReceived existe, mas não foi localizada chamada dispatch().</td></tr>
|
||||
<tr><td>Outbox/inbox e sincronização externa</td><td>Evento/mensagem externa</td><td>Sem calendário operacional confirmado</td><td>Estrutura preparada; documentação indica sincronização real pendente.</td></tr>
|
||||
<tr><td>Pesquisas, submissões, revisões e auditoria</td><td>Acção do visitante/utilizador/gestor</td><td>Sob procura ou acção; contagem indisponível</td><td>Sem logs/analytics não se calculam ocorrências por período.</td></tr></table>
|
||||
<p><b>Frequência configurada não é frequência observada.</b> Não foram disponibilizados logs, contagens de eventos da base activa, filas, analytics nem tráfego HTTP. Assim, eventos por hora/dia/mês, sazonalidade, falhas e utilizadores activos permanecem indeterminados. Não se extrapolam estes valores a partir de testes, seeders, screenshots ou schema.</p>
|
||||
|
||||
<h2>9. Inventário dos 24 casos de uso</h2>
|
||||
<table><tr><th>Grupo</th><th>Casos</th></tr>
|
||||
<tr><td>Portal e catálogo</td><td>UC-01 Consultar página inicial e serviços; UC-02 Pesquisar sectores e oportunidades; UC-03 Consultar negócio existente.</td></tr>
|
||||
<tr><td>Conta e jornada</td><td>UC-04 Criar conta/autenticar/recuperar acesso; UC-05 Manifestar interesse numa oportunidade; UC-06 Manifestar interesse num negócio; UC-07 Guardar oportunidade; UC-08 Manter submissão; UC-09 Submeter candidatura.</td></tr>
|
||||
<tr><td>Negócios e CRM</td><td>UC-10 Gerir negócio existente; UC-11 Tratar interesses no CRM.</td></tr>
|
||||
<tr><td>Território e documentos</td><td>UC-12 Gerir catálogo territorial; UC-13 Gerir zonas; UC-14 Carregar, verificar e publicar documento.</td></tr>
|
||||
<tr><td>Simulação/fiscalidade</td><td>UC-15 Criar rascunho; UC-16 Executar simulação; UC-17 Gerir regra fiscal.</td></tr>
|
||||
<tr><td>Serviços especializados</td><td>UC-18 Gerir educação e competências; UC-19 Assistência de terra; UC-20 Gerir parceria; UC-21 Usar balcão virtual.</td></tr>
|
||||
<tr><td>Transversal</td><td>UC-22 Usar IA com evidência; UC-23 Sincronizar sistemas externos; UC-24 Auditar alterações.</td></tr></table>
|
||||
<p>Os pré-requisitos, fluxos alternativos e tabelas associadas por UC encontram-se no ficheiro <i>casos-de-uso-aplicacao-actual.md</i>.</p>
|
||||
|
||||
<h2>10. Integridade, segurança e limitações</h2>
|
||||
<ul><li>Disponibilidade para consulta, situação jurídica/documental e uso em regra/cálculo são estados independentes.</li><li>Respeitar autorização de divulgação e âmbito; relações sem evidência ficam pendentes.</li><li>Preservar original, proveniência, ID exacto, edição/data, hash e histórico. Não confirmar autenticidade, vigência, direitos ou scan sem evidência.</li><li>Importação não é publicação; publicação documental não activa cálculo fiscal; rascunho não é candidatura aprovada.</li><li>Integração desenhada não significa sincronização activa. Cobertura e estado de cada domínio devem ser verificados nos respectivos relatórios de validação.</li><li>Antes de alterações a dados activos, confirmar backup/restauro e acção autenticada. Testes e migrações usam exclusivamente base QA isolada.</li></ul>
|
||||
|
||||
<h2>11. Fontes de análise</h2>
|
||||
<ul><li>README.md do repositório;</li><li>docs/investmoz/casos-de-uso-aplicacao-actual.md;</li><li>docs/investmoz/erd.mmd, data-dictionary.csv e table-catalog.csv;</li><li>rotas, controladores, modelos, jobs, comando de escalada e migrations;</li><li>InvestMoz — Levantamento funcional, casos de uso e diagramas.docx, em Downloads.</li></ul>
|
||||
<p class="note">O inventário cobriu 981 caminhos listados por rg --files, excluindo vendor e node_modules. Isso descreve o repositório acessível, não anexos externos, conteúdo de armazenamento privado ou base de dados de produção.</p>
|
||||
</body></html>
|
||||
@@ -0,0 +1,526 @@
|
||||
# Casos de uso da aplicação actual
|
||||
|
||||
Este documento descreve os processos actualmente implementados ou previstos
|
||||
pelas rotas, controladores, serviços e migrations existentes no projecto.
|
||||
Não substitui regras legais nem autoriza publicação de dados.
|
||||
|
||||
## 1. Actores
|
||||
|
||||
| Actor | Responsabilidade |
|
||||
|---|---|
|
||||
| Visitante | Consulta o portal, oportunidades, negócios, zonas e documentos públicos. |
|
||||
| Utilizador registado | Mantém perfil, rascunhos, submissões, favoritos e mensagens. |
|
||||
| Investidor | Pesquisa oportunidades, manifesta interesse e acompanha candidaturas. |
|
||||
| Gestor | Trata interesses, leads, tarefas, oportunidades e catálogo. |
|
||||
| Revisor documental | Verifica documentos, malware, versões e autorização de divulgação. |
|
||||
| Gestor territorial | Gere projectos, clusters, corredores, geografia e zonas. |
|
||||
| Gestor fiscal | Gere fontes legais, versões de regras e parâmetros do simulador. |
|
||||
| Gestor de educação | Gere competências, necessidades, cursos e propostas. |
|
||||
| Parceiro institucional | Participa em parcerias, salas, mensagens, documentos e marcos. |
|
||||
| Administrador | Gere utilizadores, permissões, configurações e instalação de módulos. |
|
||||
| Sistema externo | Recebe/envia eventos através de outbox, inbox e sincronizações. |
|
||||
|
||||
## 2. Visão geral do ciclo da aplicação
|
||||
|
||||
```text
|
||||
Portal público
|
||||
-> consulta de catálogo
|
||||
-> interesse, apoio ou simulação
|
||||
-> autenticação/conta quando necessário
|
||||
-> candidatura ou submissão
|
||||
-> documentos e validação
|
||||
-> CRM, tarefas e comunicação
|
||||
-> decisão, parceria ou acompanhamento
|
||||
```
|
||||
|
||||
Os principais agregados são `opportunities`, `existing_businesses`,
|
||||
`interest_requests`, `investment_submissions`, `documents`, `investment_zones`
|
||||
e `simulations`.
|
||||
|
||||
## 3. UC-01 — Consultar a página inicial e serviços
|
||||
|
||||
**Actor:** Visitante.
|
||||
|
||||
**Pré-condições:** O portal está disponível.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O visitante abre `/`.
|
||||
2. O sistema apresenta a página inicial e os serviços publicados.
|
||||
3. O visitante escolhe oportunidades, negócios, zonas, simulador, documentos,
|
||||
apoio ou parcerias.
|
||||
4. O sistema aplica apenas dados que estejam disponíveis para o público.
|
||||
|
||||
**Dados:** `portal_resources`, `documents`, `opportunities`, `sectors`,
|
||||
`existing_businesses`, `investment_zones`.
|
||||
|
||||
**Resultado:** O visitante é encaminhado para o processo escolhido.
|
||||
|
||||
## 4. UC-02 — Pesquisar sectores e oportunidades
|
||||
|
||||
**Actor:** Visitante ou investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor consulta `/oportunidades` ou a API de oportunidades.
|
||||
2. O sistema filtra por sector, localização, slug, estado e outros critérios.
|
||||
3. O detalhe da oportunidade mostra perfil, território, relações, documentos
|
||||
públicos e informação de investimento.
|
||||
4. O actor pode iniciar uma manifestação de interesse ou guardar a oportunidade.
|
||||
|
||||
**Dados:** `sectors`, `opportunities`, `opportunity_profiles`,
|
||||
`opportunity_facets`, `opportunity_clusters`, `opportunity_corridors`,
|
||||
`opportunity_projects`, `opportunity_locations`, `opportunity_favourites`.
|
||||
|
||||
**Resultado:** Consulta pública ou início de UC-05/UC-06.
|
||||
|
||||
## 5. UC-03 — Consultar um negócio existente
|
||||
|
||||
**Actor:** Visitante ou investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor consulta `/negocios`.
|
||||
2. O sistema lista negócios cujo estado e autorização permitem divulgação.
|
||||
3. O actor abre o detalhe pelo `slug`.
|
||||
4. O sistema apresenta apenas título, descrição, modalidade, localização,
|
||||
objectos e documentos autorizados.
|
||||
5. O actor pode iniciar uma manifestação de interesse.
|
||||
|
||||
**Dados:** `existing_businesses`, `existing_business_objects`,
|
||||
`existing_business_locations`, `existing_business_document_links`,
|
||||
`documents`, `interest_requests`.
|
||||
|
||||
**Regra:** Dados privados, mandatos e documentos não autorizados não são
|
||||
expostos pelo portal público.
|
||||
|
||||
## 6. UC-04 — Criar conta, autenticar e recuperar acesso
|
||||
|
||||
**Actor:** Visitante/utilizador.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O visitante cria uma conta com nome, email e palavra-passe.
|
||||
2. O sistema cria `users`.
|
||||
3. O utilizador confirma o email quando aplicável.
|
||||
4. O utilizador inicia sessão.
|
||||
5. O sistema aplica as políticas de autenticação, confirmação de palavra-passe
|
||||
e, para operações protegidas, segundo factor.
|
||||
|
||||
**Dados:** `users`, `passkeys`, `sessions`, `password_reset_tokens`.
|
||||
|
||||
**Alternativas:** Registo rejeitado por email existente; palavra-passe
|
||||
incorrecta; recuperação por token; desafio de dois factores.
|
||||
|
||||
## 7. UC-05 — Manifestar interesse numa oportunidade
|
||||
|
||||
**Actor:** Visitante ou investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor abre o formulário de interesse.
|
||||
2. Informa contacto, oportunidade, modalidade, valor e mensagem.
|
||||
3. O sistema valida os dados, consentimentos e alvo.
|
||||
4. É criado `interest_requests`.
|
||||
5. O sistema regista recibo/idempotência quando aplicável.
|
||||
6. O sistema envia notificação ou evento de tratamento.
|
||||
|
||||
**Dados:** `interest_requests`, `consents`, `interest_submission_receipts`,
|
||||
`notifications`, `outbox_events`.
|
||||
|
||||
**Estado inicial:** `received`, `submitted` ou equivalente configurado.
|
||||
|
||||
**Resultado:** O interesse fica disponível para tratamento no CRM.
|
||||
|
||||
## 8. UC-06 — Manifestar interesse num negócio existente
|
||||
|
||||
**Actor:** Investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O investidor escolhe um `existing_business` publicado.
|
||||
2. Selecciona a modalidade e, se aplicável, percentagem e intervalo de valor.
|
||||
3. O sistema verifica que o negócio pode receber interesse.
|
||||
4. Cria `interest_requests.existing_business_id`.
|
||||
5. Regista consentimento para contacto e partilha da informação.
|
||||
6. O caso passa para tratamento interno.
|
||||
|
||||
**Dados:** `existing_businesses`, `interest_requests`, `investors`, `consents`.
|
||||
|
||||
**Regra:** Um pedido não pode apontar simultaneamente para uma oportunidade e
|
||||
para um negócio existente.
|
||||
|
||||
## 9. UC-07 — Guardar uma oportunidade e manter espaço do investidor
|
||||
|
||||
**Actor:** Utilizador autenticado.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O utilizador abre uma oportunidade.
|
||||
2. Escolhe guardar/iniciar no espaço do investidor.
|
||||
3. O sistema regista a relação em `opportunity_favourites` ou inicia uma
|
||||
submissão/dossier.
|
||||
4. O utilizador consulta os seus itens em `/investidor` ou `/carteira`.
|
||||
|
||||
**Dados:** `users`, `opportunity_favourites`, `investment_dossiers`,
|
||||
`investor_accounts`, `investment_accounts`.
|
||||
|
||||
## 10. UC-08 — Criar e manter uma submissão de investimento
|
||||
|
||||
**Actor:** Investidor.
|
||||
|
||||
**Pré-condições:** Utilizador autenticado e verificado.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O investidor cria uma submissão.
|
||||
2. O sistema cria `investment_submissions` com estado `draft`.
|
||||
3. O investidor grava o payload e as necessidades específicas.
|
||||
4. Pode adicionar anexos e mensagens.
|
||||
5. O sistema cria eventos e revisões para manter histórico.
|
||||
6. O investidor consulta ou actualiza a submissão.
|
||||
|
||||
**Dados:** `investment_submissions`, `investment_specific_needs`,
|
||||
`investment_submission_attachments`, `investment_submission_events`,
|
||||
`investment_submission_revisions`, `investment_messages`, `documents`.
|
||||
|
||||
## 11. UC-09 — Submeter candidatura
|
||||
|
||||
**Actor:** Investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O investidor confirma os dados da submissão.
|
||||
2. O sistema verifica campos obrigatórios, alvo e anexos.
|
||||
3. A submissão muda de `draft` para `submitted`.
|
||||
4. É associado ou criado o pedido de interesse, quando exigido.
|
||||
5. O sistema cria evento de submissão e notifica a gestão.
|
||||
|
||||
**Resultado:** A candidatura deixa de ser apenas rascunho e entra na fila de
|
||||
tratamento interno.
|
||||
|
||||
## 12. UC-10 — Gerir um negócio existente
|
||||
|
||||
**Actor:** Investidor, proprietário ou gestor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor cria ou actualiza o negócio.
|
||||
2. Regista modalidade, descrição, objectos, localização e documentos.
|
||||
3. O sistema cria revisões e mantém o snapshot anterior.
|
||||
4. Um gestor pode solicitar informação, rever ou rejeitar.
|
||||
5. O negócio percorre o workflow configurado.
|
||||
6. Apenas após validação e autorização poderá ser publicado.
|
||||
|
||||
**Dados:** `existing_businesses`, `existing_business_objects`,
|
||||
`existing_business_revisions`, `existing_business_reviews`,
|
||||
`business_workflow_transitions`, `existing_business_mandates`, `documents`.
|
||||
|
||||
**Estados possíveis:** `draft`, `submitted`, `in_review`,
|
||||
`needs_information`, `approved`, `published`, `suspended`, `rejected`,
|
||||
`withdrawn`, `archived`.
|
||||
|
||||
## 13. UC-11 — Tratar interesses no CRM
|
||||
|
||||
**Actor:** Gestor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O gestor abre a fila de interesses.
|
||||
2. Consulta o pedido, consentimentos e alvo.
|
||||
3. Atribui o caso a um responsável.
|
||||
4. Converte o interesse em `leads`, quando aplicável.
|
||||
5. Cria tarefas e actividades.
|
||||
6. Actualiza o estado e regista o resultado.
|
||||
7. Pode associar um `crm_opportunity` e continuar a negociação.
|
||||
|
||||
**Dados:** `interest_requests`, `leads`, `investors`, `contacts`,
|
||||
`crm_opportunities`, `tasks`, `activities`, `consents`.
|
||||
|
||||
**Estados de interesse:** recebimento, qualificação, contacto, diligência,
|
||||
negociação, conversão, recusa, cancelamento ou encerramento.
|
||||
|
||||
## 14. UC-12 — Gerir catálogo territorial
|
||||
|
||||
**Actor:** Gestor territorial.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O gestor cria ou actualiza projectos, clusters e corredores.
|
||||
2. Liga projectos a locais.
|
||||
3. Liga projectos a clusters e corredores.
|
||||
4. Cria cadeias de valor e lacunas de investimento.
|
||||
5. Regista a fonte e o estado da relação.
|
||||
6. Outro gestor pode validar ou revogar a relação.
|
||||
|
||||
**Dados:** `projects`, `economic_clusters`, `value_chains`,
|
||||
`value_chain_links`, `development_corridors`, `cluster_projects`,
|
||||
`corridor_projects`, `corridor_clusters`, `investment_gaps`, `geo_locations`.
|
||||
|
||||
## 15. UC-13 — Gerir zonas de investimento
|
||||
|
||||
**Actor:** Gestor territorial com permissão e MFA quando exigido.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. Regista a fonte legal em `investment_legal_sources`.
|
||||
2. Cria a zona em `investment_zones`.
|
||||
3. Associa operador, categoria e localização.
|
||||
4. Adiciona geometria, anexos e relações com projectos, clusters ou corredores.
|
||||
5. Um revisor valida a informação.
|
||||
6. A zona é publicada, suspensa ou revogada.
|
||||
7. O sistema mantém revisões imutáveis.
|
||||
|
||||
**Estados:** `draft`, `published`, `suspended`; validação `pending`,
|
||||
`validated`, `historical`.
|
||||
|
||||
**Dados:** `investment_zones`, `investment_legal_sources`,
|
||||
`investment_zone_geometries`, `investment_zone_relations`,
|
||||
`investment_zone_links`, `investment_zone_attachments`, `investment_zone_revisions`.
|
||||
|
||||
## 16. UC-14 — Carregar, verificar e publicar documento
|
||||
|
||||
**Actor:** Gestor documental.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O gestor carrega o ficheiro.
|
||||
2. O sistema preserva o original e calcula o hash.
|
||||
3. Detecta duplicados e conflitos de versão.
|
||||
4. Regista `documents` e, quando necessário, `document_versions`.
|
||||
5. Executa análise antimalware.
|
||||
6. O gestor revê classificação, datas, fonte e autorização.
|
||||
7. O documento é associado ao negócio, candidatura, zona, projecto ou portal.
|
||||
8. Só então pode ser disponibilizado publicamente.
|
||||
|
||||
**Dados:** `documents`, `document_versions`, `document_malware_scans`,
|
||||
`portal_resources`, `portal_resource_revisions` e tabelas `*_documents`.
|
||||
|
||||
**Estados documentais:** pendente, limpo, infectado, rejeitado, privado,
|
||||
publicável, publicado ou revogado.
|
||||
|
||||
## 17. UC-15 — Criar rascunho de simulação
|
||||
|
||||
**Actor:** Visitante ou utilizador.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor abre o simulador.
|
||||
2. O sistema cria ou recupera um cookie de recuperação.
|
||||
3. Guarda os dados em `simulation_form_drafts`.
|
||||
4. O actor pode continuar mais tarde.
|
||||
5. Um utilizador autenticado pode reclamar o rascunho.
|
||||
6. O rascunho pode ser ligado a um negócio existente, mediante autorização.
|
||||
|
||||
**Resultado:** Dados incompletos permanecem como rascunho e não são tratados
|
||||
como cálculo fiscal executado.
|
||||
|
||||
## 18. UC-16 — Executar simulação
|
||||
|
||||
**Actor:** Visitante ou utilizador.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor submete os dados do simulador.
|
||||
2. O sistema escolhe a versão do questionário.
|
||||
3. Resolve os parâmetros fiscais elegíveis.
|
||||
4. Cria `simulations` e `simulation_scenarios`.
|
||||
5. Executa o cálculo através de job.
|
||||
6. Guarda resultados, obrigações e fontes.
|
||||
7. Gera relatório quando solicitado.
|
||||
8. Regista auditoria e idempotência.
|
||||
|
||||
**Dados:** `questionnaire_versions`, `simulations`, `simulation_scenarios`,
|
||||
`simulation_results`, `simulation_obligations`, `simulation_reports`,
|
||||
`simulation_jobs`, `simulation_result_sources`, `simulation_audit_events`.
|
||||
|
||||
## 19. UC-17 — Gerir regra fiscal
|
||||
|
||||
**Actor:** Autor, revisor, aprovador ou publicador fiscal.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O autor cria uma versão da regra.
|
||||
2. Associa a regra a uma fonte legal.
|
||||
3. O revisor verifica expressão, datas e parâmetros.
|
||||
4. O aprovador decide.
|
||||
5. O publicador disponibiliza a versão.
|
||||
6. O sistema regista cada transição.
|
||||
|
||||
**Dados:** `simulation_tax_rule_versions`, `simulation_tax_rule_events`,
|
||||
`investment_legal_sources`, `users`.
|
||||
|
||||
**Regra:** Importar ou publicar um documento não activa automaticamente um
|
||||
cálculo fiscal.
|
||||
|
||||
## 20. UC-18 — Gerir educação e competências
|
||||
|
||||
**Actor:** Gestor de educação.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. Mantém catálogo de competências, profissões, instituições e cursos.
|
||||
2. Regista necessidades de formação de projectos.
|
||||
3. Associa ocupações e competências.
|
||||
4. Regista capacidades e oportunidades educativas.
|
||||
5. Analisa lacunas.
|
||||
6. Cria proposta de investimento educativo.
|
||||
|
||||
**Dados:** `skills`, `occupations`, `training_needs`, `training_providers`,
|
||||
`courses`, `education_opportunities`, `investment_gaps`,
|
||||
`education_investment_proposals` e tabelas `skills_edu`.
|
||||
|
||||
## 21. UC-19 — Abrir e tratar assistência de terra
|
||||
|
||||
**Actor:** Investidor e gestor de assistência.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O investidor cria um caso de assistência.
|
||||
2. Regista localização, necessidade e documentos.
|
||||
3. O gestor analisa o caso.
|
||||
4. São registadas revisões, eventos e pedidos adicionais.
|
||||
5. O caso é encaminhado ou encerrado.
|
||||
|
||||
**Dados:** `land_assistance_cases`, `land_assistance_revisions`,
|
||||
`land_assistance_events`, `land_assistance_documents`, `documents`, `users`.
|
||||
|
||||
## 22. UC-20 — Criar e gerir parceria
|
||||
|
||||
**Actor:** Investidor, organização ou parceiro institucional.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor cria um rascunho de parceria.
|
||||
2. Regista origem, sector, local e objectivo.
|
||||
3. Outro actor manifesta interesse.
|
||||
4. A candidatura é aceite ou recusada.
|
||||
5. É aberta uma sala de parceria.
|
||||
6. Os participantes trocam mensagens e documentos.
|
||||
7. São definidos marcos, pagamentos ou acordo.
|
||||
8. O sistema regista eventos e notificações.
|
||||
|
||||
**Dados:** `portal_partnerships`, `partnership_origins`,
|
||||
`partnership_locations`, `partnership_interests`, `partnership_rooms`,
|
||||
`partnership_messages`, `partnership_documents`, `partnership_milestones`,
|
||||
`partnership_events`, `partnership_notifications`.
|
||||
|
||||
## 23. UC-21 — Usar o balcão virtual
|
||||
|
||||
**Actor:** Utilizador autenticado e agente de atendimento.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O utilizador abre um pedido no balcão.
|
||||
2. O sistema cria um caso e associa o serviço.
|
||||
3. O agente recebe o pedido.
|
||||
4. São trocadas mensagens e solicitados documentos.
|
||||
5. O caso passa por estados e revisões.
|
||||
6. Eventos, notificações e pagamentos são registados.
|
||||
7. O caso é resolvido ou encerrado.
|
||||
|
||||
**Dados:** `desk_service_versions`, `desk_cases`, `desk_requests`,
|
||||
`desk_documents`, `desk_events`, `desk_revisions`, `desk_messages`,
|
||||
`desk_notifications`, `desk_institutional_links`, `desk_payments`.
|
||||
|
||||
## 24. UC-22 — Usar inteligência artificial com evidência
|
||||
|
||||
**Actor:** Gestor autorizado.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O gestor activa a capacidade em `ai_controls`.
|
||||
2. Regista uma fonte ou documento.
|
||||
3. O sistema cria chunks da fonte.
|
||||
4. O gestor inicia uma execução de IA.
|
||||
5. O sistema grava `ai_runs`.
|
||||
6. O resultado pode ser revisto ou receber feedback.
|
||||
7. A aplicação mantém a fonte que suporta a resposta.
|
||||
|
||||
**Dados:** `ai_controls`, `ai_sources`, `ai_chunks`, `ai_runs`, `ai_feedback`,
|
||||
`documents`, `users`.
|
||||
|
||||
## 25. UC-23 — Sincronizar com sistemas externos
|
||||
|
||||
**Actor:** Sistema externo ou processo de integração.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. A aplicação gera um evento em `outbox_events`.
|
||||
2. Um worker publica o evento.
|
||||
3. O sistema externo responde ou envia evento para `inbox_events`.
|
||||
4. A aplicação cria `sync_runs`.
|
||||
5. Falhas ou divergências são registadas em `reconciliation_issues`.
|
||||
6. O processo pode ser repetido de forma idempotente.
|
||||
|
||||
## 26. UC-24 — Auditoria e controlo de alterações
|
||||
|
||||
**Actor:** Sistema, gestor ou auditor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. Uma operação protegida é executada.
|
||||
2. O sistema identifica actor, entidade, acção e correlação.
|
||||
3. Regista evento de auditoria.
|
||||
4. Alterações de entidades versionadas geram revisão/snapshot.
|
||||
5. O auditor consulta o histórico sem alterar o registo original.
|
||||
|
||||
**Dados:** `audit_events`, `simulation_audit_events`, `investment_zone_revisions`,
|
||||
`existing_business_revisions`, `investment_submission_revisions`,
|
||||
`desk_revisions`, `outbox_events`.
|
||||
|
||||
## 27. Regras transversais da aplicação
|
||||
|
||||
- Dados privados exigem autenticação e autorização.
|
||||
- Divulgação pública depende do estado de publicação e autorização registada.
|
||||
- Documentos originais devem ser preservados.
|
||||
- Hash, versão e proveniência devem permanecer associados ao documento.
|
||||
- Relações jurídicas ou territoriais não validadas ficam pendentes.
|
||||
- Rascunho não é submissão aprovada.
|
||||
- Importação não é publicação.
|
||||
- Publicação de documento não é activação de regra fiscal.
|
||||
- Cálculos fiscais devem usar fonte e parâmetro identificados.
|
||||
- Histórico e auditoria não devem ser apagados por rollback destrutivo.
|
||||
- Operações de migração e testes devem usar base QA isolada.
|
||||
|
||||
## 28. Mapa resumido dos processos
|
||||
|
||||
```text
|
||||
Visitante
|
||||
-> consulta catálogo
|
||||
-> interesse / apoio / simulação
|
||||
|
||||
Investidor
|
||||
-> conta
|
||||
-> favoritos / dossier
|
||||
-> submissão
|
||||
-> documentos
|
||||
-> mensagens
|
||||
|
||||
Gestor
|
||||
-> interesse
|
||||
-> lead
|
||||
-> tarefas
|
||||
-> oportunidade CRM
|
||||
-> decisão
|
||||
|
||||
Gestor territorial
|
||||
-> projecto / cluster / corredor
|
||||
-> zona
|
||||
-> geometria
|
||||
-> fonte legal
|
||||
-> validação / publicação
|
||||
|
||||
Gestor fiscal
|
||||
-> regra
|
||||
-> revisão
|
||||
-> aprovação
|
||||
-> publicação
|
||||
-> parâmetro usado pelo simulador
|
||||
|
||||
Parceiros
|
||||
-> parceria
|
||||
-> candidatura
|
||||
-> sala
|
||||
-> mensagens / documentos / marcos
|
||||
```
|
||||
@@ -4,6 +4,10 @@ Data: 2026-10-03. Âmbito: prompt master recebido em `Pasted text.txt`, document
|
||||
|
||||
## Estado inicial e ambiente
|
||||
|
||||
**Orientação de publicação e percurso do lote, 23/10/2026:** as 34 oportunidades de Agronegócio ficam publicáveis sem relações obrigatórias com corredores/clusters/projectos. Lacuna encontrada: a comparação múltipla existente não era transportada ao dossier. Correcção aditiva no contexto cifrado existente, com escolhas identificadas e localização por ficha, confirmação de alternativa, requisitos/fontes/documentos/entidades por contexto, reutilização no simulador e conservação de resultados/histórico. Sete testes do contexto (132 asserções) e um do lote pela sessão HTTP (15) passaram. Publicação QA efectuada; instalação no destino separa código, cadastro privado e divulgação autenticada. Ver [orientação e guia no manual](unification/traceability-and-manual.md). Não altera Turismo de Inhambane nem concede benefícios por localização. Lacunas de modalidade/dimensão/acto concreto permanecem explícitas, sem nova revalidação de fontes oficiais aceites.
|
||||
|
||||
**Correcção de âmbito, 23/10/2026 — agronegócio e percurso existente:** a lista informativa instalada não encerrava o cadastro. Preparadas e efectivamente cadastradas 34 oportunidades privadas em QA, com as seis designações documentais intactas, sector, fonte/hash e lacunas explícitas. Reutilizada a candidata de divulgação promocional `9d3bfa89` no fluxo necessário, sem alterações independentes de administração de contas nem novos papéis. Equipa edita/recupera e divulga/actualiza/retira pela interface; público acede à ficha individual, manifestação e roteiro com referência confirmada no mesmo dossier. Três testes específicos, 553 asserções; tipos/build concluídos; tabelas/linhas preexistentes comparadas e preservadas. As divulgações dos testes foram revertidas; as 34 fichas permanecem privadas. Sem alteração da homologação nesta correcção. Carregamento inicial técnico QA já executado; não é dependência das operações correntes. Guia, limites e recibo na [secção de correcção do manual](unification/traceability-and-manual.md#correcção-de-âmbito--34-oportunidades-cadastradas-e-roteiro-em-qa-23102026). Não encerra viabilidade, implantação, financiamento ou execução das oportunidades.
|
||||
|
||||
| Item | Evidência observada |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Branch / commit | `main` / `4f35313d06ccd597a4ebf9ed043964d898c95813` |
|
||||
@@ -315,3 +319,37 @@ Situação comum observada: `publication_status=draft`, `validation_status=valid
|
||||
- Verificação: `php artisan test tests/Feature/PortalServicesTest.php` — 17 testes/497 asserções aprovados em QA; cobrem publicação autorizada com pesquisa textual e filtros combinados, ficha, download, associação ao módulo e links de requisitos com estados confirmados/pendentes. `npm.cmd run build` concluído. O build alerta sobre pacote opcional `fontaine` e chunk `services` acima de 500 KB; sem erro de compilação. Não houve browser autenticado disponível, portanto não se declara publicação, pesquisa ou download públicos concluídos.
|
||||
- A autorização documental não foi aplicada a zonas, parques, negócios privados ou oportunidades de demonstração. A regra de publicação de parque não exige área confirmada; divergência de área, isoladamente, não é um gate técnico. Se a ficha avançar por decisão própria, deve omitir o total disputado ou mostrar os valores reportados com fonte/escopo, sem alegar disponibilidade. Beluluane, Munhava e Topuito requerem revisão por registo para fonte/localização pública e para retirar ou qualificar a área; Topuito só tem província associada no mestre geográfico, pelo que não se pode anunciar uma associação administrativa a Larde enquanto essa referência não existir. ZEE/ZFI continuam dependentes de acto oficial estruturado e validado, regime e relações territoriais apropriadas; uma área sem confirmação não substitui essas evidências.
|
||||
- Proposta para as seis oportunidades de demonstração já públicas: preservar os registos e IDs, adicionar origem/tipo `demo` e identificá-los visivelmente como **Exemplo de demonstração**, separar a pesquisa de exemplos da pesquisa de oportunidades reais e excluir os exemplos dos resultados reais por defeito. Não os converter nem retirar em massa; cada registo real exigirá origem e proponente documentados. Nenhuma alteração foi feita nesta execução.
|
||||
|
||||
### Referência da entrada única de gestão — QA, 22/10/2026
|
||||
|
||||
Nos requisitos existentes `MASTER-9f6dd1d9-0001`, `MASTER-9f6dd1d9-0003` e `MASTER-02f44baf-0002`, acrescentar como evidência delimitada a secção «Entrada única de gestão — preparação em QA, 22/10/2026» do manual de rastreabilidade e `tests/Feature/ContentManagementEntryTest.php`: quatro testes/99 asserções, ClamAV real e reversão transaccional em QA. Origem do objectivo e das regras operacionais: decisão expressa da Tempo; navegação, filtros e recuperação do upload são escolhas de implementação. Reutilizados biblioteca, armazenamento, perfis e histórico. Não altera os critérios de aceitação nem fecha os requisitos integrais: integração/contas reais e categorias não demonstradas conservam o seu estado. Geometrias permanecem separadas.
|
||||
|
||||
|
||||
AUD-GEO-02/03: Recuperação da demarcação territorial — QA, 22/10/2026. 408/581 limites recuperados em QA; cadeia territorial guardada/reaberta no portal. Homologacao ainda sem reposicao; consultar a nova secao no manual existente. Mantidas 173 correspondencias pendentes e ausencia de geometrias proprias das localidades. Origem: pedido expresso da Tempo de recuperar funcionalidade; enquadramento e mensagens sao escolhas tecnicas. Nao encerra o criterio de reposicao no destino.
|
||||
|
||||
|
||||
AUD-GEO-02/03 - 22/10/2026: Demarcação territorial reposta na homologação — 22/10/2026. 408 limites (11/133/264) importados pela conta real/MFA, com tres recibos auditados; backup/restauro de 171 tabelas conferido, 333 ficheiros verificados. Consulta do mapa demonstrada na oportunidade real #30, sem a alterar. Acessos previstos e originais privados preservados. Reposicao no destino concluida; 26+147 correspondencias e ausencia de poligonos proprios de localidades mantidas como lacunas reais. Ver fecho e guia no manual existente; sem repetir provas QA aceites.
|
||||
|
||||
### Incremento Concursos e anúncios — QA, 22/10/2026
|
||||
|
||||
Instalação autorizada e concluída na homologação em 22/10/2026: integração funcional `f4d09709`, schema selectivo/contrato/activação conferidos após backup e restauro isolado de 171 tabelas. Gestão real com MFA: cadastro, recuperação e prévia de rascunho privado auditado; catálogo público 200, zero anúncios publicados. Preservados dados/permissões/originais e geometrias. Evidência e links finais em «Instalação concluída na homologação» no manual e `unification/procurement-installation-receipt-20261022.json`. Provas QA aceites reutilizadas. Este fecho substitui o estado de proposta pendente de instalação abaixo, sem declarar aceite pela Tempo o guia não conferido nem expandir o âmbito para recepção de propostas.
|
||||
|
||||
Proposta de entrega da candidata `1bcf34c0`: ver «Proposta de instalação compatível com deploy automático» no manual existente. Identificados três novos cadastros e o gate real de contrato de migrations do pipeline. Sequência proposta: revisão → coordenação do deploy → backup/restauro isolado → migration selectiva autenticada com flag desligada → contrato → integração/build/promoção → activação → conferência do delta no destino. Reversão conserva tabelas, actos, auditoria e originais posteriores; não restaura backup antigo sobre dados novos. Provas QA reutilizadas; nenhuma integração, migration remota, activação ou publicação real executada nesta passagem. A Tempo ainda não conferiu links locais/guia; não registar aceitação nem entrega no destino por essa razão.
|
||||
|
||||
Nos requisitos existentes de gestão/publicação `MASTER-9f6dd1d9-0001`, `MASTER-9f6dd1d9-0003` e `MASTER-02f44baf-0002`, registar o recorte solicitado pela Tempo: procedimentos com regime/edição/fontes, lotes, rascunhos recuperáveis, prévia, actos versionados, catálogo/ficha/anexos anónimos, filtros, impressão e acompanhamento por conta simples. Evidência: secção «Concursos e anúncios — implementação em QA» do manual existente e `tests/Feature/ProcurementTest.php`, com passagem adicional no navegador dos três regimes, prorrogação, cancelamento, download e acompanhamento. Reutilizadas autorizações e biblioteca; não foram criados papéis ou aprovadores. Origem funcional: pedido expresso da Tempo; flag, prévia assinada e controlo de versão são escolhas técnicas. Entrega delimitada a QA: integração em main, migração/activação no destino e anúncios reais seguem o processo existente. Não fecha requisitos integrais de contratação/recepção de propostas, nem declara parametrizado todo o articulado legal. Geometrias permanecem no recorte separado já entregue.
|
||||
|
||||
### Preservação do concluído e reposição — 23/10/2026
|
||||
|
||||
Origem: pedido expresso da Tempo de inventariar o concluído e repor funcionalidades desaparecidas antes de outra ronda de testes/validações. Inventariados os mesmos 43 componentes do manual e os 17 percursos da matriz executável existente; os componentes de interface referenciados estão presentes. Estados históricos foram reconciliados com entregas posteriores, sem revogar aceitações por falta de nova prova. Não se criou nova matriz/backlog nem se promoveu uma funcionalidade parcial a concluída.
|
||||
|
||||
Perda confirmada: carteira e projectos âncora. Os 29 dossiers existiam em homologação, todos em rascunho, apesar de os exemplares locais idênticos estarem publicados com aprovação documental do proprietário de 26/09. Conteúdo, fonte e título conferidos por identidade/código e hashes nos 29; o commit `e65cb269` de 06/10 retirou o caminho de publicação com essa aprovação do importador. Isso explica a diferença do processo de instalação; não prova quando a visibilidade desapareceu de um endereço específico. Reposta a visibilidade dos 15 projectos, 8 clusters e 6 corredores em QA e homologação, com versões e 29 eventos de reposição; sem importação, reescrita de conteúdos, alteração dos mestres ou novas aprovações/papéis.
|
||||
|
||||
Backup/restauro fresco de 174 tabelas antes da escrita activa; depois da reposição, 171 tabelas inalteradas. Diferenças limitadas a dossiers, auditoria e sessões. Biblioteca, 20 artigos públicos, 28 privados, geometrias e permissões preservados. Conferência pública delimitada: carteira 29, projectos 15, clusters 8, corredores 6. Sem suite geral de testes nesta passagem. Evidência, limites e regra de preservação em `unification/preservation-inventory-20261023.json`, secção «Preservação do concluído — inventário e reposição» do manual e instrução permanente de 23/10 em `AGENTS.md`.
|
||||
|
||||
### Acervo legado — correspondência e preparação QA, 22/10/2026
|
||||
|
||||
Fecho do delta privado na homologação: candidata `fb4e7d89` integrada em `65eddf6b`; backup/restauro isolado, transporte privado e cadastro autenticado dos 28 concluídos, sem publicação. Pesquisa/editor/prévia/recuperação conferidos para os 28; gravação/recuperação visual da revisão 3 pela conta real no recurso 32. Originais, registos anteriores e permissões preservados. Recibo `unification/legacy-editorial-installation-receipt-20261022.json` e secção «Legado — cadastro privado concluído na homologação» do manual. Publicação e revisão editorial efectiva cabem à equipa pela interface; dois originais continuam incompletos. Não fecha nem diagnostica a falha separada da carteira/projectos âncora, nem constitui aceitação do commit/pacote pela Tempo.
|
||||
|
||||
Actualização do mesmo requisito: os 28 rascunhos têm edição, leitura privada, prévia sem guardar, recuperação e publicação/retirada pela interface em QA, preservando originais, origem e revisões. Seis testes/1.000 asserções, TypeScript e build passaram; publicação positiva apenas em transacções revertidas. Pacote privado v2 preparado com 28 originais e proveniência; não transportado nem publicado na homologação. Guia, permissões existentes, lacunas de imagens, dependências da entrega e reversão na secção «Legado — edição e leitura dos 28 rascunhos em QA» do manual. Pedido funcional expresso da Tempo; hash de âmbito, prévia e controlo de versão são escolhas técnicas, sem novo papel/aprovador. Conserva o saldo de 20 públicos aceites e dois originais incompletos, sem repetir provas ou fechar a integração integral.
|
||||
|
||||
Referência do acervo legado, 22/10/2026: a lista única `v2/auditoria-portal/legado-correspondencia-integral.md` e o CSV existente reconciliam 50 identidades (20 publicados, 28 por integrar no destino, um vazio, um sem título). 20/20 páginas conferidas por HTTP anónimo/identidade/origem; cartões de Turismo observados no destino. Os 28 restantes avançaram para rascunhos internos QA, com ClamAV real e repetição idempotente, sem publicação. Recibo e limites no manual, secção «Legado — correspondência integral e avanço privado QA». As duas lacunas editoriais foram confirmadas no original. Não fecha a integração integral nem substitui revisão editorial por prova HTTP; nenhuma alteração activa nesta passagem.
|
||||
|
||||
@@ -78,3 +78,6 @@
|
||||
"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"67067";"5C69B6F189A1D3F5307E0CF7DD8224554705158A682637D3105AFAD615DB26E2";"b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"Private stored copy already represented by its document record";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
|
||||
"Formulario fornecido Tempo";"\\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\portal do investidor\Notas\Novo Modelo A.docx";"Novo Modelo A.docx";"532008";"62C198E90D8C6792F7D5CCEFC6386DF2E4B6089573067D3A6E1EAE7FC2B9572B";"IVA Modelo A - exemplar Tempo";"";"Original privado na biblioteca QA; sem publicacao ou activacao";"Acessivel / importado QA / privado / scan limpo";"Imagem incorporada lida visualmente; edicao/vigencia nao certificada. Correspondencia na reconciliacao Modelo A, AUD-SIM-INT02. Variante de bytes preservada, nao duplicado por nome."
|
||||
"Formulario fornecido Tempo";"\\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\portal do investidor\Notas\Novo Modelo A v1.docx";"Novo Modelo A v1.docx";"532100";"63D45FA80DB17A60B43619D55A25813229D670D14C758273F86316285DB8E02B";"IVA Modelo A - exemplar Tempo";"";"Original privado na biblioteca QA; sem publicacao ou activacao";"Acessivel / importado QA / privado / scan limpo";"Imagem incorporada lida visualmente; edicao/vigencia nao certificada. Correspondencia na reconciliacao Modelo A, AUD-SIM-INT02. Variante de bytes preservada, nao duplicado por nome."
|
||||
"Documento oficial localizado para consultaQA";"C:\laragon\www\portal-investidor\outputs\iva-l1-g5d-offer-drafts-20261024\BR-11-I-2010-03-17-despacho-modelos.pdf";"BR-11-I-2010-03-17-despacho-modelos.pdf";"376928";"384fb46354e1888df43f72288dfed7933e30c7534211e0dcb6994f43b10e0059";"BR11/I Série17/03/2010,DespachoMF06/01/2010,ModelosRCBFDecreto56/2009";"";"Preservar original e origem;sem publicação/importação/activação de regra";"Acessível";"PDF1–5 lidos;ModeloI.1versoPDF3 localizado;âmbito ligação14(3)/alterações por conferir,sem nova autenticação da fonte oficial"
|
||||
"Fonte oficial localizada para cotejoQA";"C:\laragon\www\portal-investidor\outputs\iva-l1-g6e-supplies-20261024\Decreto56-2009-BR40.pdf";"Decreto56-2009-BR40.pdf";"422005";"6ea1b8d93daed48c6d590481d874010fa1708211d8f27c319dc9a31328456017";"Decreto56/2009/CBFLei4/2009 — originalBR40 ecompilaçãoMIREME";"";"Preservar original/origem;sem publicação ou activação de cálculo";"Acessível";"CotejoModeloI.1;âmbitoCBF não demonstra remissãoCIVA14(3);não autenticação adicional"
|
||||
"Fonte oficial localizada para cotejoQA";"C:\laragon\www\portal-investidor\outputs\iva-l1-g6e-supplies-20261024\MIREME-legislacao-investimentos.pdf";"MIREME-legislacao-investimentos.pdf";"1674566";"df50f6a489ffb35a4252fddec0ffa1f9c58d7b8cb4e54c9f25b7904bd39218b7";"Decreto56/2009/CBFLei4/2009 — originalBR40 ecompilaçãoMIREME";"";"Preservar original/origem;sem publicação ou activação de cálculo";"Acessível";"CotejoModeloI.1;âmbitoCBF não demonstra remissãoCIVA14(3);não autenticação adicional"
|
||||
|
||||
|
@@ -24,3 +24,6 @@
|
||||
"docs/investmoz/simulations/official-source-register.md";"sumário oficial INM do BR 134/2024";"https://www.inm.gov.mz/pt-br/content/sum%C3%A1rio-br-n%C2%BA-134-de-110724-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-2827";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
|
||||
"docs/investmoz/simulations/official-source-register.md";"ficha oficial INM do aviso";"https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-br-n%C2%BA-89-de-080617-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-569";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
|
||||
"docs/investmoz/simulations/official-source-register.md";"reprodução integral de terceiro, auxiliar";"https://www.lexlink.eu/FileGet.aspx?FileId=2999523";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
|
||||
"docs/investmoz/simulations/iva-l1-p084-p085-draft-preparation-qa-20261024.json";"BR11/2010,DespachoMF06/01/2010,ModeloI.1versoPDF3";"https://www.cipmoz.org/wp-content/uploads/2023/parcerias-publico-privadas/9PP%29%20Despacho_Mecanismos%20Implementacao%20do%20Regulamento%20do%20Codigo%20dos%20Beneficios%20Fiscais.pdf";"C:\laragon\www\portal-investidor\outputs\iva-l1-g5d-offer-drafts-20261024\BR-11-I-2010-03-17-despacho-modelos.pdf";"Original oficialBR obtido/hash;âmbito específico ainda por conferir";"Consulta autorizada;sem publicação ou activação fiscal"
|
||||
"docs/investmoz/simulations/iva-l1-p084-p085-model-i1-cotejo-20261024.json";"Decreto56/2009/RegCBFLei4/2009,origemModeloI.1";"https://fracessoriasa.co.mz/wp-content/uploads/2021/03/10-Decreto-n_56.2009-REGULAMENTO-DOS-BENEFICIOS-FISCAIS.pdf";"C:\laragon\www\portal-investidor\outputs\iva-l1-g6e-supplies-20261024\Decreto56-2009-BR40.pdf";"OriginalBR ecompilaçãooficial obtidos/hash,âmbito cotejado";"Sem correspondênciaCIVA14(3)demonstrada;sem publicação ou activação fiscal"
|
||||
"docs/investmoz/simulations/iva-l1-p084-p085-model-i1-cotejo-20261024.json";"Decreto56/2009/RegCBFLei4/2009,origemModeloI.1";"https://mireme.gov.mz/wp-content/uploads/2022/03/Lei-e-Regualamento-das-zonas-francas-industriais-investimentos.pdf";"C:\laragon\www\portal-investidor\outputs\iva-l1-g6e-supplies-20261024\MIREME-legislacao-investimentos.pdf";"OriginalBR ecompilaçãooficial obtidos/hash,âmbito cotejado";"Sem correspondênciaCIVA14(3)demonstrada;sem publicação ou activação fiscal"
|
||||
|
||||
|
@@ -0,0 +1,211 @@
|
||||
# Levantamento funcional, entidades e eventos — InvestMoz
|
||||
|
||||
**Data do levantamento:** 9 de Outubro de 2026
|
||||
**Âmbito:** aplicação Laravel/React presente neste repositório.
|
||||
**Base de evidência:** README, rotas, controladores, modelos, migrations e documentação em `docs/investmoz/`. A existência de uma tabela, rota ou documento não prova que a função esteja activa em produção.
|
||||
|
||||
## 1. Resumo executivo
|
||||
|
||||
O projecto é uma plataforma de investimento com área pública e área autenticada. Abrange descoberta de oportunidades e negócios, manifestações de interesse, gestão de candidaturas e carteira, CRM local, geografia e zonas de investimento, simulador e governação fiscal, catálogo documental, educação, assistência de terra, parcerias, balcão virtual, IA com fontes e integrações preparadas.
|
||||
|
||||
O README apresenta a base inicial do portal (sectores, oportunidades e manifestações). O código actual e os documentos de domínio registam a extensão funcional posterior. Para o inventário detalhado dos fluxos, consultar [casos-de-uso-aplicacao-actual.md](casos-de-uso-aplicacao-actual.md), que identifica 24 casos de uso. Este documento complementa-o com uma visão de domínio, um diagrama resumido de relações e a frequência observável dos eventos.
|
||||
|
||||
## 2. Âmbito e método
|
||||
|
||||
Foi feito inventário dos caminhos acessíveis no repositório: **981 ficheiros** por `rg --files`, excluindo dependências geradas `vendor` e `node_modules`. Dentro da aplicação foram consultados, em particular, `routes/`, `app/Models/`, `app/Http/Controllers/`, `database/migrations/`, `database/seeders/` e `docs/investmoz/`. O número é uma contagem do inventário local nessa data, não um inventário de anexos externos ou de dados de produção.
|
||||
|
||||
Classificação usada:
|
||||
|
||||
- **Implementado no código:** rota, controlador, job, comando ou schema identificável.
|
||||
- **Documentado/planeado:** descrito em documentação, sem prova suficiente de execução integrada.
|
||||
- **Frequência de negócio desconhecida:** não há amostra de eventos de produção nem telemetria para calcular volume por hora/dia/mês.
|
||||
- **Cadência técnica explícita:** periodicidade configurada no scheduler ou no mecanismo de fila.
|
||||
|
||||
## 3. Actores e sistemas
|
||||
|
||||
| Actor | Objectivo principal | Áreas usadas |
|
||||
|---|---|---|
|
||||
| Visitante | Consultar conteúdos públicos e iniciar pedidos | Portal, oportunidades, negócios, zonas, documentos, simulador |
|
||||
| Investidor/utilizador | Manter perfil, favoritos, rascunhos, submissões, documentos e mensagens | Espaço do investidor, carteira, assistência, parcerias |
|
||||
| Gestor de portal/CRM | Rever interesses, gerir leads, tarefas, catálogo e publicação | CRM local, gestão de portal |
|
||||
| Gestor territorial | Manter projectos, clusters, corredores, relações, geografia e zonas | Catálogo territorial, GIS, zonas |
|
||||
| Revisor documental | Conferir proveniência, versão, hash, autorização e análise antimalware | Biblioteca documental |
|
||||
| Gestor fiscal | Rever fontes, parâmetros e versões das regras fiscais | Governação fiscal, simulador |
|
||||
| Gestor de educação | Manter competências, necessidades, cursos, capacidades e propostas | Educação |
|
||||
| Gestor/agente de assistência | Tratar pedidos de terra e pedidos do balcão virtual | Assistência de terra, balcão |
|
||||
| Parceiro institucional | Participar em oportunidades/parcerias e colaborar em salas | Parcerias |
|
||||
| Administrador | Gerir autenticação, segurança, configurações e permissões | Administração |
|
||||
| Sistema externo/worker | Receber/enviar mensagens ou executar trabalho assíncrono | Outbox/inbox, filas, integrações |
|
||||
|
||||
Papéis são uma leitura funcional dos fluxos. A autorização efectiva depende das policies, middleware e permissões implementadas, não apenas desta tabela.
|
||||
|
||||
## 4. Catálogo de casos de uso
|
||||
|
||||
O documento [Casos de uso da aplicação actual](casos-de-uso-aplicacao-actual.md) detalha pré-condições, fluxos e dados para cada caso. Índice consolidado:
|
||||
|
||||
| ID | Caso de uso | Actor principal | Resultado |
|
||||
|---|---|---|---|
|
||||
| UC-01 | Consultar página inicial e serviços | Visitante | Entrada no serviço público autorizado |
|
||||
| UC-02 | Pesquisar sectores e oportunidades | Visitante/investidor | Lista/detalhe e possível favorito/interesse |
|
||||
| UC-03 | Consultar negócio existente | Visitante/investidor | Consulta da oferta divulgável |
|
||||
| UC-04 | Criar conta, autenticar e recuperar acesso | Utilizador | Sessão autenticada/verificada |
|
||||
| UC-05 | Manifestar interesse numa oportunidade | Visitante/investidor | Pedido recebido para tratamento |
|
||||
| UC-06 | Manifestar interesse num negócio existente | Investidor | Pedido ligado ao negócio e consentimento |
|
||||
| UC-07 | Guardar oportunidade e manter espaço do investidor | Utilizador | Favorito, dossier ou item na carteira |
|
||||
| UC-08 | Criar e manter submissão de investimento | Investidor | Submissão em rascunho com histórico |
|
||||
| UC-09 | Submeter candidatura | Investidor | Entrada em fila de tratamento |
|
||||
| UC-10 | Gerir negócio existente | Proprietário/gestor | Registo revisto e eventualmente publicável |
|
||||
| UC-11 | Tratar interesses no CRM | Gestor | Qualificação, lead, tarefa e seguimento |
|
||||
| UC-12 | Gerir catálogo territorial | Gestor territorial | Projectos, clusters, corredores e relações |
|
||||
| UC-13 | Gerir zonas de investimento | Gestor territorial/revisor | Zona validada, publicada ou suspensa |
|
||||
| UC-14 | Carregar, verificar e publicar documento | Revisor documental | Documento preservado com estados independentes |
|
||||
| UC-15 | Criar/retomar rascunho de simulação | Visitante/utilizador | Rascunho recuperável, sem cálculo implícito |
|
||||
| UC-16 | Executar simulação | Visitante/utilizador | Resultado, obrigações, fontes e relatório |
|
||||
| UC-17 | Gerir regra fiscal | Gestor fiscal | Versão de regra passa por revisão/aprovação |
|
||||
| UC-18 | Gerir educação e competências | Gestor de educação | Catálogo, análise de lacunas e proposta |
|
||||
| UC-19 | Abrir/tratar assistência de terra | Investidor/gestor | Caso acompanhado até encaminhamento/fecho |
|
||||
| UC-20 | Criar e gerir parceria | Investidor/parceiro | Interesse, sala, mensagens, documentos e marcos |
|
||||
| UC-21 | Usar balcão virtual | Utilizador/agente | Pedido de serviço acompanhado até resolução |
|
||||
| UC-22 | Usar IA com evidência | Gestor autorizado | Execução rastreável ligada a fontes e feedback |
|
||||
| UC-23 | Sincronizar sistemas externos | Worker/sistema externo | Eventos recebidos/enviados e divergências registadas |
|
||||
| UC-24 | Auditar alterações | Sistema/gestor/auditor | Histórico e correlação para operações relevantes |
|
||||
|
||||
## 5. Diagrama de entidades e relações
|
||||
|
||||
Diagrama lógico resumido por domínios. As cardinalidades representam a estrutura documentada; algumas relações polimórficas, opcionais ou em tabelas de junção estão simplificadas. Para o ERD de schema detalhado, consultar [erd.mmd](erd.mmd), [data-dictionary.csv](data-dictionary.csv) e [table-catalog.csv](table-catalog.csv).
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER ||--o{ INVESTOR : possui
|
||||
USER ||--o{ SUBMISSION : cria
|
||||
USER ||--o{ SIMULATION : executa
|
||||
USER ||--o{ AUDIT_EVENT : origina
|
||||
SECTOR ||--o{ OPPORTUNITY : classifica
|
||||
OPPORTUNITY ||--o{ INTEREST_REQUEST : recebe
|
||||
EXISTING_BUSINESS ||--o{ INTEREST_REQUEST : recebe
|
||||
INVESTOR ||--o{ INTEREST_REQUEST : manifesta
|
||||
INTEREST_REQUEST ||--o| LEAD : converte
|
||||
LEAD ||--o{ CRM_TASK : acompanha
|
||||
OPPORTUNITY ||--o{ SUBMISSION : referencia
|
||||
SUBMISSION ||--o{ SUBMISSION_REVISION : versiona
|
||||
SUBMISSION ||--o{ SUBMISSION_EVENT : regista
|
||||
SUBMISSION ||--o{ DOCUMENT_LINK : anexa
|
||||
PROJECT }o--o{ ECONOMIC_CLUSTER : associa
|
||||
PROJECT }o--o{ DEVELOPMENT_CORRIDOR : associa
|
||||
PROJECT ||--o{ INVESTMENT_GAP : identifica
|
||||
INVESTMENT_ZONE ||--o{ ZONE_REVISION : versiona
|
||||
INVESTMENT_ZONE ||--o{ ZONE_RELATION : liga
|
||||
INVESTMENT_ZONE ||--o{ ZONE_ATTACHMENT : documenta
|
||||
DOCUMENT ||--o{ DOCUMENT_VERSION : versiona
|
||||
DOCUMENT ||--o{ MALWARE_SCAN : analisa
|
||||
DOCUMENT }o--o{ PORTAL_RESOURCE : disponibiliza
|
||||
SIMULATION ||--o{ SIMULATION_SCENARIO : contém
|
||||
SIMULATION_SCENARIO ||--o{ SIMULATION_RESULT : produz
|
||||
SIMULATION_RESULT ||--o{ SIMULATION_OBLIGATION : discrimina
|
||||
SIMULATION_RESULT ||--o{ RESULT_SOURCE : fundamenta
|
||||
TAX_RULE_VERSION }o--o{ LEGAL_SOURCE : fundamenta
|
||||
PROJECT ||--o{ TRAINING_NEED : requer
|
||||
SKILL }o--o{ OCCUPATION : relacionada
|
||||
TRAINING_PROVIDER ||--o{ COURSE : oferece
|
||||
USER ||--o{ LAND_ASSISTANCE_CASE : requer
|
||||
PARTNERSHIP ||--o{ PARTNERSHIP_INTEREST : recebe
|
||||
PARTNERSHIP ||--o| PARTNERSHIP_ROOM : abre
|
||||
PARTNERSHIP_ROOM ||--o{ PARTNERSHIP_MESSAGE : contém
|
||||
DESK_CASE ||--o{ DESK_EVENT : regista
|
||||
AI_SOURCE ||--o{ AI_CHUNK : divide
|
||||
AI_SOURCE ||--o{ AI_RUN : suporta
|
||||
OUTBOX_EVENT }o..|| SYNC_RUN : processa
|
||||
SYNC_RUN ||--o{ RECONCILIATION_ISSUE : detecta
|
||||
```
|
||||
|
||||
### 5.1 Domínios e entidades nucleares
|
||||
|
||||
| Domínio | Entidades nucleares | Relações principais |
|
||||
|---|---|---|
|
||||
| Identidade e acesso | `users`, `passkeys`, sessões, organizações, memberships | Utilizador possui perfil e inicia operações protegidas |
|
||||
| Catálogo público | `sectors`, `opportunities`, catálogos de referência, recursos do portal | Sector classifica oportunidades; publicação controla exposição |
|
||||
| Negócios existentes | `existing_businesses`, objectos, locais, revisões, mandatos | Negócio tem objectos/locais/documentos e workflow próprio |
|
||||
| Investidor e CRM | `investors`, `interest_requests`, `leads`, `contacts`, oportunidades CRM, tarefas | Interesse pode converter-se em lead e gerar tarefas/actividades |
|
||||
| Carteira e submissões | dossiers, `investment_submissions`, necessidades, revisões, eventos, mensagens, anexos | Submissão pertence a utilizador e liga-se a oportunidade/negócio |
|
||||
| Território e GIS | `projects`, `economic_clusters`, `development_corridors`, `geo_locations`, `value_chains`, `investment_gaps` | Relações muitos-para-muitos documentam associação territorial/económica |
|
||||
| Zonas de investimento | `investment_zones`, fontes legais, geometrias, relações, anexos, revisões | Zona tem localização, evidência, relações e estado de validação/publicação |
|
||||
| Documentos | `documents`, versões, scans, associações por domínio, recursos do portal | Documento pode ser associado a várias entidades; disponibilidade, estado documental e uso em cálculo são independentes |
|
||||
| Simulação e fiscalidade | rascunhos, `simulations`, cenários, resultados, obrigações, fontes, regras versionadas | Execução usa versões/parâmetros elegíveis e preserva as fontes do resultado |
|
||||
| Educação | skills, occupations, providers, courses, training needs/capacity, propostas | Projectos expressam necessidades relacionadas com competências e formação |
|
||||
| Serviços e colaboração | casos de terra, balcão virtual, parcerias, salas, mensagens, documentos, marcos | Casos e salas mantêm eventos e participantes próprios |
|
||||
| IA e governação | controlos, fontes/chunks, execuções, feedback, auditoria | Execução de IA deve poder ser rastreada às fontes e ao actor |
|
||||
| Integrações | `outbox_events`, `inbox_events`, `sync_runs`, reconciliações | Eventos são idempotentes/correlacionáveis; desenho de schema não prova integração activa |
|
||||
|
||||
## 6. Eventos e frequência
|
||||
|
||||
### 6.1 Frequência explicitamente configurada
|
||||
|
||||
| Evento/processo | Gatilho | Frequência/cadência no código | Natureza e observação |
|
||||
|---|---|---|---|
|
||||
| Escalar tarefas vencidas (`investmoz:escalate-overdue-tasks`) | Scheduler Laravel | **A cada 15 minutos**, com `withoutOverlapping()` | Única periodicidade de scheduler encontrada em `routes/console.php`; execução real requer scheduler/cron activo |
|
||||
| Executar simulação (`RunSimulation`) | Pedido de cálculo aceite | Por submissão, via fila, `afterCommit` | Assíncrono; frequência depende da procura e de workers activos |
|
||||
| Analisar documento (`ScanDocumentForMalware`) | Upload/solicitação de scan em fluxos suportados | Por documento, via fila, `afterCommit`; job único por identidade configurada | Cadência sob procura; requer worker e scanner configurados. Upload/publicação não confirma scan limpo |
|
||||
| Comparar compatibilidade MNE (`CompareMneBusiness`) | Alteração de negócio que agenda comparação | Por alteração aplicável, em fila específica e `afterCommit` | Trabalho assíncrono/sombra; volume real desconhecido |
|
||||
| Notificar recepção de interesse (`NotifyInterestReceived`) | Disparo esperado após recepção de interesse | Job existe, mas não foi localizada chamada a `dispatch()` no código consultado; frequência de execução não confirmada | Tratar como capacidade prevista até localizar/activar o disparo; entrega efectiva também depende de configuração e worker |
|
||||
| Auditoria, revisões e eventos de domínio | Acção de utilizador/sistema | No momento da operação que os gera | Orientado a evento; não há taxa histórica neste levantamento |
|
||||
| Outbox/inbox e sincronização externa | Evento de domínio ou mensagem externa | Sem periodicidade operacional confirmada neste código/documentação consultada | Estrutura preparada; documentação aponta sincronização externa real como pendente/não activada |
|
||||
|
||||
### 6.2 Frequência por categoria de actividade
|
||||
|
||||
| Categoria | Frequência funcional esperada | O que é possível afirmar |
|
||||
|---|---|---|
|
||||
| Leituras públicas (home, catálogo, oportunidade, negócio, mapas/referências) | Sob procura do visitante | Há endpoints/páginas; sem logs ou analytics não se quantificam consultas |
|
||||
| Escritas de investidor (interesses, favoritos, submissões, mensagens, perfil) | Sob acção do utilizador | São iniciadas por pedido explícito; não há volume observado disponível |
|
||||
| Gestão (revisão, publicação, CRM, regras, zonas, catálogo) | Sob trabalho de gestores autorizados | Fluxos e protecções documentados; frequência por papel não medida |
|
||||
| Simulação e rascunhos | Sob utilização do simulador; rascunhos podem ser actualizados/retomados | Rota e jobs encontrados; não assumir execução periódica nem cálculo fiscal por guardar rascunho |
|
||||
| Uploads e análise documental | Sob envio/reanálise de documento | Job de scan é disparado por fluxos suportados; scan e publicação são estados distintos |
|
||||
| Auditoria | Sob operações auditadas | A frequência acompanha as operações instrumentadas; cobertura total precisa de confirmação por fluxo |
|
||||
| Regras fiscais e fontes | Sob processo de revisão/aprovação e alteração normativa | Não há calendário fixo; fonte publicada não activa automaticamente cálculo |
|
||||
| Integrações externas | Não determinável/activação pendente | Tabelas de integração e documentação não são prova de entrega ou sincronização real |
|
||||
|
||||
### 6.3 Frequência observada versus frequência de execução
|
||||
|
||||
Não foram disponibilizados logs, contagens de `audit_events`, filas, analytics, pedidos HTTP ou exportação da base activa. Por isso, **não é possível apresentar eventos por hora/dia/mês, sazonalidade, taxa de falhas ou utilizadores activos**. A cadência de 15 minutos é apenas o intervalo configurado para o comando agendado. Para apurar frequência real, recolher métricas autorizadas por tipo de evento, intervalo temporal e resultado, sem expor dados pessoais; documentar período da amostra, filtros, retenção e origem. Não inferir métricas a partir de seeders, testes, screenshots ou schema.
|
||||
|
||||
## 7. Sequência de alto nível
|
||||
|
||||
```text
|
||||
Visita pública
|
||||
-> pesquisa catálogo/oportunidades/negócios/território
|
||||
-> interesse, candidatura, assistência ou simulação
|
||||
-> autenticação e consentimento quando exigidos
|
||||
-> gravação do pedido/rascunho e histórico
|
||||
-> trabalho assíncrono quando aplicável (scan, cálculo, notificação)
|
||||
-> revisão por responsável autorizado
|
||||
-> decisão, comunicação ou publicação com autorização
|
||||
-> auditoria e eventual integração externa homologada
|
||||
```
|
||||
|
||||
## 8. Regras transversais que afectam os fluxos
|
||||
|
||||
- Disponibilidade para consulta, situação jurídica/documental e uso em regra/cálculo são estados independentes.
|
||||
- Importar ou publicar documento não activa regra nem cálculo fiscal.
|
||||
- Relações sem citação/evidência verificada devem ficar pendentes e identificadas.
|
||||
- Guardar um rascunho não equivale a submeter ou aprovar.
|
||||
- A divulgação obedece à autorização registada e ao seu âmbito.
|
||||
- Documento original, proveniência, identificador, edição/data, hash e histórico devem ser preservados; não marcar autenticidade, vigência, direitos ou scan como confirmados sem evidência.
|
||||
- Integração desenhada (outbox/inbox) não equivale a sincronização activa.
|
||||
- Alterações a dados activos exigem confirmação de backup/restauro e acção autenticada necessária; testes/migrações devem usar exclusivamente base QA isolada, conforme `AGENTS.md`.
|
||||
|
||||
## 9. Limitações e pontos para validação
|
||||
|
||||
1. A cobertura implementada de cada UC varia; alguns documentos descrevem trabalho parcial, proposto ou bloqueado. Consultar `docs/investmoz/validation.md`, `completion-audit-2026-10-04.md`, `delivery-gaps.md` e os README de cada domínio antes de declarar operação concluída.
|
||||
2. O ERD resumido não substitui constraints/FKs, cardinalidades exactas, escopos de acesso ou estados enumerados no schema e OpenAPI.
|
||||
3. A frequência real de utilização e dos eventos de negócio permanece **indeterminada** sem telemetria operacional.
|
||||
4. Execução do scheduler, workers, ClamAV, notificações, SuiteCRM e restantes conectores depende de configuração e operação externa verificável.
|
||||
5. O inventário desta tarefa é do repositório, não dos ficheiros/anexos em armazenamento externo nem da base de dados de produção.
|
||||
|
||||
## 10. Fontes locais principais
|
||||
|
||||
- [README do projecto](../../README.md)
|
||||
- [Casos de uso detalhados](casos-de-uso-aplicacao-actual.md)
|
||||
- [ERD do schema reconciliado](erd.mmd)
|
||||
- [Arquitectura e migrations](architecture-and-migrations.md)
|
||||
- [Validação](validation.md) e [auditoria de conclusão](completion-audit-2026-10-04.md)
|
||||
- Rotas: `routes/web.php`, `routes/api.php`, `routes/investment.php`, `routes/partnerships.php`, `routes/land-assistance.php`, `routes/investment-zones.php`, `routes/territorial-registry.php`
|
||||
- Cadência: `routes/console.php`; trabalhos assíncronos em `app/Jobs/`
|
||||
@@ -0,0 +1,710 @@
|
||||
{
|
||||
"date": "2026-10-23",
|
||||
"decision": "Pedido expresso da Tempo: aplicar o anexo de identidade visual aos cinco cartões de Encontre o contexto do seu investimento.",
|
||||
"instructionSource": {
|
||||
"path": "C:\\Users\\jorge\\.codex\\attachments\\637b878f-1f58-490d-bd07-d5cd10ae2377\\Pasted text.txt",
|
||||
"sha256": "fcc24dcce4b5df3f10e29add48052808d48ddcf674d052287854e11910e01d69"
|
||||
},
|
||||
"source": {
|
||||
"type": "AI-generated photorealistic illustrative images",
|
||||
"tool": "image_gen, built-in mode",
|
||||
"skill": "C:/Users/jorge/.codex/skills/.system/imagegen/SKILL.md",
|
||||
"externalPhotographsUsed": false,
|
||||
"namedRealLocationsDepicted": false,
|
||||
"note": "Cenas ilustrativas plausíveis, sem atribuição a instalações, pessoas, instituições ou projectos reais."
|
||||
},
|
||||
"analysisAndPlan": "docs/investmoz/public-portal-sections.md#fotografias-nos-cartões-de-contexto-do-investimento--23102026",
|
||||
"filesChanged": [
|
||||
"resources/js/pages/PortalInvestidor.tsx",
|
||||
"resources/js/components/investment-context-cards.tsx",
|
||||
"tools/portal-context-cards-qa.mjs",
|
||||
"docs/investmoz/public-portal-sections.md",
|
||||
"docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
|
||||
"docs/investmoz/public-portal-context-images.json"
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"key": "clusters-agroindustria",
|
||||
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a modest, well maintained agro-processing packing facility linking commercial agriculture to local industry. Wide three-quarter view into an airy open-sided processing shed with pale walls and a few orderly wooden or reusable produce crates, stainless processing/sorting benches and an unobtrusive commercial agricultural field visible beyond. Two Black southern African workers in clean practical workwear sorting harvested vegetables naturally, faces not prominent. Show production and transformation together in ONE genuine-looking continuous scene, no montage. Clear simple geometry, warm-neutral sunlight, subtle vegetation.",
|
||||
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-f72e66b0-d788-4476-8b20-c39b22d5d6dc.png",
|
||||
"original": "outputs/investment-context-20261023/originals/clusters-agroindustria.png",
|
||||
"originalSha256": "75e04a694081ab757831fc9b37323949d09806e622c83d07588fc47c423c9033",
|
||||
"originalDimensions": {
|
||||
"width": 1672,
|
||||
"height": 941
|
||||
},
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/investment-context/clusters-agroindustria.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 116796,
|
||||
"sha256": "12494b31ab551cffb79f81c7d1519c0c4a2d4236e7f129bec1ce32251b39d847"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/clusters-agroindustria-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 42060,
|
||||
"sha256": "76e3152c99ed188f748542dcf0464787307dbdc1c6299e7264dfd1bd86ad75e3"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/clusters-agroindustria-fallback.webp",
|
||||
"width": 240,
|
||||
"height": 135,
|
||||
"bytes": 9596,
|
||||
"sha256": "9f668da50f7d64955b31940d239b420ac18dbb76a8120c8bedab3f2c04710288"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "corredores-logisticos",
|
||||
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a commercial freight terminal serving a southern African logistics corridor. Elevated medium-wide viewpoint showing an orderly short line of unbranded freight containers, one realistic container-handling crane and a freight truck on a paved lane, with a calm strip of water and low coastal industrial vegetation in the distance. Infrastructure of credible modest contemporary scale, generic and unidentifiable; do not reproduce or name Maputo, Beira, Nacala or any real port. Spatially coherent containers, road and handling equipment; clean clear subject with no excessive machinery.",
|
||||
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-0363da23-f9cb-413b-bb64-024539085c4b.png",
|
||||
"original": "outputs/investment-context-20261023/originals/corredores-logisticos-v2.png",
|
||||
"originalSha256": "2151d1a59265c6f7151d18af6ef6ad78f9fc6e0cd58ac9cc5c64b50a25c5058e",
|
||||
"originalDimensions": {
|
||||
"width": 1672,
|
||||
"height": 941
|
||||
},
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/investment-context/corredores-logisticos-v2.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 85184,
|
||||
"sha256": "1939c26df297588b6c3e5ea08b700d4cda5d1183cf3e5faed06a1ca060d2475e"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/corredores-logisticos-v2-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 26222,
|
||||
"sha256": "06dc0e7126cb2c13eb3f9a98f6ee58f5d64f9d07c28620975f744f263744fe15"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/corredores-logisticos-v2-fallback.webp",
|
||||
"width": 240,
|
||||
"height": 135,
|
||||
"bytes": 5334,
|
||||
"sha256": "f9fd6e6c3a2d4654df1a8fdb287e122f1889b5d318c104c4816ee393c793e242"
|
||||
}
|
||||
],
|
||||
"selectedSource": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-c2cff7da-d602-4191-9aca-380918a09619.png",
|
||||
"editPrompt": "Edit the provided photograph: REMOVE THE ENTIRE FOREGROUND TRUCK, including cab, wheels, long trailer and the red freight container on that trailer. The big truck at lower left/centre must be completely gone. Replace the removed truck's area with a continuous empty realistic paved road surface matching the surrounding pavement and road markings. Leave the stacked stationary freight containers along the quay, the water, the large port crane, the lifted container and workers. All remaining equipment and containers should have plain surfaces without readable lettering, logos, numbers or brands. No new truck, vehicle, symbol, text or overlay. Keep a single clean editorial photograph, natural lighting and horizontal 16:9 framing. Generic commercial port scene, not a named real facility."
|
||||
},
|
||||
{
|
||||
"key": "projectos-ancora",
|
||||
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a small team of two Black southern African engineering professionals, one woman and one man, in realistic hard hats and restrained protective workwear, seen candidly from a three-quarter rear angle reviewing an unmarked folded technical plan at a utility-scale solar installation. Low rows of realistic blue solar panels and a simple low service building recede into a dry green southern African landscape. No readable drawing or numbers on the plan. Focus on credible engineering and investment project scale; clean composition, quiet professional atmosphere, no landmark or company identifiers.",
|
||||
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-19fcd882-542b-43c9-81e7-593fd75083f6.png",
|
||||
"original": "outputs/investment-context-20261023/originals/projectos-ancora.png",
|
||||
"originalSha256": "f23cf6e64307a280f3aa58741454a23480ac28fcae01a0810adf134b783a3a74",
|
||||
"originalDimensions": {
|
||||
"width": 1672,
|
||||
"height": 941
|
||||
},
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/investment-context/projectos-ancora.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 77114,
|
||||
"sha256": "de0b59774246fee982761de59618b3e7707c3fe30f2b1bcbb6017d4dce53c4dc"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/projectos-ancora-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 29484,
|
||||
"sha256": "268e405112ed341fce3c9792c3caa47053984100cd4a204453e31bbe5db6e890"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/projectos-ancora-fallback.webp",
|
||||
"width": 240,
|
||||
"height": 135,
|
||||
"bytes": 6750,
|
||||
"sha256": "db7e4b4382e09eb6b4f9f2a4603ccac4b4eacc53ba0f6776429e33dbf5131954"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "educacao-formacao",
|
||||
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a candid technical training classroom with three Black southern African young adult students of different genders and one instructor working together at a practical electronics and computing workbench. Bright modest well maintained vocational classroom, natural window light, simple laboratory equipment and unbranded closed or angled laptops whose screens have no visible interface or text. Focus on two students engaged in a realistic small hands-on electronics exercise with normal anatomy and correct equipment proportions. Dignified contemporary clothing, natural concentration, professional skills and human capital investment, no posed smiles or stereotypes.",
|
||||
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-d21314d7-c726-41ad-862c-3c8b64df6b53.png",
|
||||
"original": "outputs/investment-context-20261023/originals/educacao-formacao.png",
|
||||
"originalSha256": "e59ec599e8c64ab84195d0cb37871d3102df178a13275806ca3b7f98e6bfa895",
|
||||
"originalDimensions": {
|
||||
"width": 1672,
|
||||
"height": 941
|
||||
},
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/investment-context/educacao-formacao.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 68224,
|
||||
"sha256": "e3e5dbd292cac555a45bcbd94a0fa6b60e6b710debc0c827c68603f40bfb8a28"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/educacao-formacao-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 27384,
|
||||
"sha256": "d3b743fbba7a8cb40e45999ee8c345cbfc0332cf85f5c5c2d6a5cf1240a395e4"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/educacao-formacao-fallback.webp",
|
||||
"width": 240,
|
||||
"height": 135,
|
||||
"bytes": 7156,
|
||||
"sha256": "0b2a12898043c57cf4354b54ebcba8adec8d41b3886bc574584ccc12af7b1e4c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "documentos-formularios",
|
||||
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a close-medium editorial photograph of a tidy modern administrative office desk where a Black southern African professional in a plain light blue shirt is naturally reviewing organised business documentation. Frame hands, desk and partial torso, no prominent portrait. Neat neutral folders, a few sheets with only faint out-of-focus non-readable printing, a pen and an unbranded laptop angled away with no visible UI. Soft window light, white and pale neutral surfaces with subtle blue accents. Absolutely no readable personal data, invented official seals or highlighted fake text; realistic fingers and documents, calm minimal composition.",
|
||||
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-f1c8e22e-6885-4578-ac7d-02692c5cc071.png",
|
||||
"original": "outputs/investment-context-20261023/originals/documentos-formularios.png",
|
||||
"originalSha256": "cfbd38a9cc9b9acf35f38220f5bd9c1ab1feafe411bc1d4bbdb3658de557e052",
|
||||
"originalDimensions": {
|
||||
"width": 1672,
|
||||
"height": 941
|
||||
},
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/investment-context/documentos-formularios.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 43002,
|
||||
"sha256": "70ddea3256e6a8d981b1fae5ee7b4de6c910d757137b79a59306c268c278402f"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/documentos-formularios-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 17996,
|
||||
"sha256": "436865c948acbb7b6e9e482bef8f5bd626c6960f2f155608e40dfcd0e76b84cb"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-context/documentos-formularios-fallback.webp",
|
||||
"width": 240,
|
||||
"height": 135,
|
||||
"bytes": 4958,
|
||||
"sha256": "fad6a6ab825a82ec2541d2e0c6de9d85486e4bbca159b912f87bec9cc0aeeba5"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"primaryImagesBytesTotal": 390320,
|
||||
"responsive480ImagesBytesTotal": 143146,
|
||||
"implementation": {
|
||||
"gridColumns": [
|
||||
1,
|
||||
2,
|
||||
4
|
||||
],
|
||||
"aspectRatio": "16:9",
|
||||
"responsiveImages": true,
|
||||
"lazyLoading": true,
|
||||
"altLanguage": "pt",
|
||||
"fallback": "Derivado pequeno da própria imagem; se também falhar, estado acessível Imagem indisponível, com dimensão reservada e link mantido.",
|
||||
"globalCSSChanged": false,
|
||||
"businessLogicChanged": false,
|
||||
"databaseChanged": false,
|
||||
"heroChanged": false
|
||||
},
|
||||
"visualReview": "Cinco composições individuais distintas, cores naturais e luz editorial; sem títulos/colagens ou mapas. A composição de Corredores foi revista para remover o camião com marca. Capturas em computador e telemóvel inspeccionadas.",
|
||||
"verification": {
|
||||
"browser": {
|
||||
"results": [
|
||||
{
|
||||
"width": 320,
|
||||
"dpr": 1,
|
||||
"columns": 1,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"dpr": 2,
|
||||
"columns": 1,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"dpr": 1,
|
||||
"columns": 2,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"dpr": 1,
|
||||
"columns": 4,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"dpr": 2,
|
||||
"columns": 4,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"dpr": 1,
|
||||
"columns": 4,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"fail": "primary",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"fail": "all",
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"navigation": [
|
||||
"/clusters",
|
||||
"/corredores",
|
||||
"/projectos-ancora",
|
||||
"/educacao",
|
||||
"/documentos"
|
||||
],
|
||||
"databaseAccess": false
|
||||
},
|
||||
"script": "tools/portal-context-cards-qa.mjs",
|
||||
"screenshots": [
|
||||
"outputs/investment-context-20261023/cards-1024-dpr1.png",
|
||||
"outputs/investment-context-20261023/cards-1440-dpr2.png",
|
||||
"outputs/investment-context-20261023/cards-1920-dpr1.png",
|
||||
"outputs/investment-context-20261023/cards-320-dpr1.png",
|
||||
"outputs/investment-context-20261023/cards-390-dpr2.png",
|
||||
"outputs/investment-context-20261023/cards-768-dpr1.png",
|
||||
"outputs/investment-context-20261023/fallback-all.png",
|
||||
"outputs/investment-context-20261023/fallback-primary.png"
|
||||
],
|
||||
"types": {
|
||||
"command": "npm run types:check",
|
||||
"exitCode": 0
|
||||
},
|
||||
"lint": {
|
||||
"files": [
|
||||
"resources/js/components/investment-context-cards.tsx",
|
||||
"resources/js/pages/PortalInvestidor.tsx",
|
||||
"tools/portal-context-cards-qa.mjs"
|
||||
],
|
||||
"exitCode": 0
|
||||
},
|
||||
"build": {
|
||||
"command": "npm run build",
|
||||
"exitCode": 0,
|
||||
"pageAsset": "assets/PortalInvestidor-fh5VZanx.js",
|
||||
"log": "outputs/investment-context-20261023/build-local.log"
|
||||
}
|
||||
},
|
||||
"scope": "Aplicado no código e pacote locais. API e navegação simuladas na verificação visual; sem dados activos ou publicação externa.",
|
||||
"matrixReferences": [
|
||||
"MASTER-efd550fd-0104",
|
||||
"MASTER-55050f60-0001"
|
||||
],
|
||||
"acceptanceLimit": "Prova restrita à apresentação e navegação dos cinco cartões; não encerra critérios funcionais mais amplos da matriz.",
|
||||
"layoutRevision": {
|
||||
"date": "2026-10-23",
|
||||
"decision": "Pedido expresso da Tempo: título a vermelho e menos espaço em branco dentro dos cartões.",
|
||||
"changes": "Título no vermelho #ed1c24; removidas alturas mínimas de título e descrição; preenchimento 20→16 px, intervalo 8→6 px e entrelinhas 28/24→24/20 px. Mantidos imagens, conteúdos, links e alinhamento por linha da grelha.",
|
||||
"browser": {
|
||||
"results": [
|
||||
{
|
||||
"width": 320,
|
||||
"dpr": 1,
|
||||
"columns": 1,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
|
||||
"width": 320,
|
||||
"height": 180
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"dpr": 2,
|
||||
"columns": 1,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios.webp",
|
||||
"width": 389,
|
||||
"height": 219
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"dpr": 1,
|
||||
"columns": 2,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
|
||||
"width": 384,
|
||||
"height": 216
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"dpr": 1,
|
||||
"columns": 4,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
|
||||
"width": 256,
|
||||
"height": 144
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"dpr": 2,
|
||||
"columns": 4,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"dpr": 1,
|
||||
"columns": 4,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
|
||||
"width": 266,
|
||||
"height": 149
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"fail": "primary",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"fail": "all",
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"navigation": [
|
||||
"/clusters",
|
||||
"/corredores",
|
||||
"/projectos-ancora",
|
||||
"/educacao",
|
||||
"/documentos"
|
||||
],
|
||||
"databaseAccess": false
|
||||
},
|
||||
"lintExitCode": 0,
|
||||
"buildExitCode": 0,
|
||||
"pageAsset": "assets/PortalInvestidor-CYCMhSzW.js",
|
||||
"screenshots": [
|
||||
"outputs/investment-context-compact-20261023/cards-1024-dpr1.png",
|
||||
"outputs/investment-context-compact-20261023/cards-1440-dpr2.png",
|
||||
"outputs/investment-context-compact-20261023/cards-1920-dpr1.png",
|
||||
"outputs/investment-context-compact-20261023/cards-320-dpr1.png",
|
||||
"outputs/investment-context-compact-20261023/cards-390-dpr2.png",
|
||||
"outputs/investment-context-compact-20261023/cards-768-dpr1.png"
|
||||
],
|
||||
"databaseChanged": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
{
|
||||
"date": "2026-10-23",
|
||||
"decision": "Pedido expresso da Tempo: adaptar hero, imagens e pesquisa da referência, preservando a informação e os comportamentos equivalentes do V2.",
|
||||
"sourceProject": "D:/Projects/Tempo_Investidor/investidor",
|
||||
"targetProject": "D:/Projects/Tempo_Investidor/Investidor-V2",
|
||||
"planAndAnalysis": "docs/investmoz/public-portal-sections.md#adequação-do-hero--23102026",
|
||||
"implementation": [
|
||||
"resources/js/components/portal-hero.tsx",
|
||||
"resources/js/pages/PortalInvestidor.tsx"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\investidor\\public\\images\\hero\\hero-1.png",
|
||||
"original": "public\\images\\hero\\hero-1.png",
|
||||
"sha256": "3c9246169025b9b1b4d1d10fb51c1f644123984bde56c6c2d9bf869d851024fc",
|
||||
"bytes": 2315768,
|
||||
"webpBytes": 94012,
|
||||
"mobileBytes": 38804
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\investidor\\public\\images\\hero\\hero-3.jpg",
|
||||
"original": "public\\images\\hero\\hero-3.jpg",
|
||||
"sha256": "91377213b6286aca7ee4af90860a7dc7a75fa08a165ef7b3192081db43f92dff",
|
||||
"bytes": 9875137,
|
||||
"webpBytes": 140342,
|
||||
"mobileBytes": 32998
|
||||
}
|
||||
],
|
||||
"desktopBytesTotal": 234354,
|
||||
"mobileBytesTotal": 71802,
|
||||
"originalBytesTotal": 12190905,
|
||||
"verification": {
|
||||
"browser": {
|
||||
"results": [
|
||||
{
|
||||
"width": 320,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"reducedMotion": "no-preference",
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"requests": [
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=",
|
||||
"/oportunidades",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique"
|
||||
],
|
||||
"databaseAccess": false
|
||||
},
|
||||
"browserScript": "tools/portal-hero-qa.mjs",
|
||||
"screenshots": [
|
||||
"outputs/portal-hero-20261023/after-1440-no-preference.png",
|
||||
"outputs/portal-hero-20261023/after-1440-reduce.png",
|
||||
"outputs/portal-hero-20261023/after-1440-second-image.png",
|
||||
"outputs/portal-hero-20261023/after-1920-reduce.png",
|
||||
"outputs/portal-hero-20261023/after-320-reduce.png",
|
||||
"outputs/portal-hero-20261023/after-390-reduce.png",
|
||||
"outputs/portal-hero-20261023/after-768-reduce.png",
|
||||
"outputs/portal-hero-20261023/before-1440-reduce.png",
|
||||
"outputs/portal-hero-20261023/before-390-reduce.png"
|
||||
],
|
||||
"additionalCases": [
|
||||
"pesquisa por Enter e botão",
|
||||
"ordem do teclado: campo → botão",
|
||||
"query acentuada e pontuação preservadas",
|
||||
"remoção de espaços exteriores",
|
||||
"pesquisa vazia abre catálogo",
|
||||
"CTA abre oportunidades",
|
||||
"API indisponível mantém indicadores indeterminados",
|
||||
"contagens zero preservadas",
|
||||
"duas imagens carregadas",
|
||||
"selecção manual",
|
||||
"pausa e retoma explícitas",
|
||||
"foco pausa rotação",
|
||||
"rotação automática após perda de foco",
|
||||
"movimento reduzido",
|
||||
"entrada documental e cabeçalho preservados"
|
||||
],
|
||||
"existingFrontendTests": {
|
||||
"command": "node --test tests/frontend/portal-api.test.mjs tests/frontend/portal-navigation.test.mjs",
|
||||
"passed": 20
|
||||
},
|
||||
"typecheck": {
|
||||
"command": "npm run types:check",
|
||||
"exitCode": 0
|
||||
},
|
||||
"lint": {
|
||||
"files": [
|
||||
"resources/js/components/portal-hero.tsx",
|
||||
"resources/js/pages/PortalInvestidor.tsx",
|
||||
"tools/portal-hero-qa.mjs"
|
||||
],
|
||||
"exitCode": 0
|
||||
},
|
||||
"qaBuild": {
|
||||
"command": "INVESTMOZ_QA_BUILD=1 npm run build",
|
||||
"exitCode": 0
|
||||
},
|
||||
"localBuild": {
|
||||
"command": "npm run build",
|
||||
"exitCode": 0,
|
||||
"pageAsset": "assets/PortalInvestidor-MsUeKoJQ.js"
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
"file": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
|
||||
"referencesAdded": [
|
||||
"MASTER-9dee08d0-0016",
|
||||
"MASTER-efd550fd-0104",
|
||||
"MASTER-55050f60-0001"
|
||||
],
|
||||
"rowCount": 3422,
|
||||
"criteriaAndStatesPreserved": true
|
||||
},
|
||||
"scope": {
|
||||
"appliedToLocalCodeAndBuild": true,
|
||||
"activeDatabaseChanged": false,
|
||||
"migrationsRun": false,
|
||||
"externalDeploymentPerformed": false,
|
||||
"backendSearchReimplemented": false
|
||||
},
|
||||
"limits": "Verificação visual com dados sintéticos; pesquisa continua limitada ao catálogo de oportunidades. Os critérios mais amplos da matriz permanecem no estado anterior.",
|
||||
"latestRevision": {
|
||||
"id": "hero-no-controls-20261023",
|
||||
"date": "2026-10-23",
|
||||
"decision": "Pedido expresso da Tempo: remover do hero as duas bolinhas e o botão de pausa das imagens.",
|
||||
"implementation": "Removidos grupo de controlos, ícones Pause/Play e estado de pausa manual. Preservadas troca automática, escrita dinâmica, pesquisa e preferência de movimento reduzido.",
|
||||
"browser": {
|
||||
"results": [
|
||||
{
|
||||
"width": 320,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"reducedMotion": "no-preference",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"reducedMotion": "no-preference",
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"requests": [
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=",
|
||||
"/oportunidades",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=",
|
||||
"/oportunidades",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique"
|
||||
],
|
||||
"databaseAccess": false
|
||||
},
|
||||
"screenshots": [
|
||||
"outputs/portal-hero-no-controls-20261023/after-1440-no-preference.png",
|
||||
"outputs/portal-hero-no-controls-20261023/after-1440-reduce.png",
|
||||
"outputs/portal-hero-no-controls-20261023/after-1920-reduce.png",
|
||||
"outputs/portal-hero-no-controls-20261023/after-320-reduce.png",
|
||||
"outputs/portal-hero-no-controls-20261023/after-390-no-preference.png",
|
||||
"outputs/portal-hero-no-controls-20261023/after-390-reduce.png",
|
||||
"outputs/portal-hero-no-controls-20261023/after-768-reduce.png",
|
||||
"outputs/portal-hero-no-controls-20261023/second-image-1440.png",
|
||||
"outputs/portal-hero-no-controls-20261023/second-image-390.png",
|
||||
"outputs/portal-hero-no-controls-20261023/typed-1440.png",
|
||||
"outputs/portal-hero-no-controls-20261023/typed-390.png"
|
||||
],
|
||||
"checks": {
|
||||
"lintExitCode": 0,
|
||||
"buildExitCode": 0,
|
||||
"pageAsset": "assets/PortalInvestidor-T0B6aoV_.js"
|
||||
},
|
||||
"scope": {
|
||||
"appliedLocally": true,
|
||||
"databaseAccess": false,
|
||||
"externalDeployment": false
|
||||
}
|
||||
},
|
||||
"revisionHistory": [
|
||||
{
|
||||
"id": "hero-typing-20261023",
|
||||
"date": "2026-10-23",
|
||||
"decision": "Pedido expresso da Tempo: título e frase dinâmicos como na referência; remover Termos de uso do hero.",
|
||||
"plan": [
|
||||
"Analisar a sequência de escrita da referência.",
|
||||
"Animar o título e depois a frase, mantendo os textos do V2 e reservando a dimensão final.",
|
||||
"Reiniciar a escrita na troca de imagem e integrar pausa/retoma e preferência de movimento reduzido.",
|
||||
"Remover o link e o ícone jurídico do hero.",
|
||||
"Demonstrar comportamento em computador e telemóvel e compilar o pacote local."
|
||||
],
|
||||
"files": [
|
||||
"resources/js/components/portal-hero.tsx",
|
||||
"resources/css/app.css",
|
||||
"tools/portal-hero-qa.mjs"
|
||||
],
|
||||
"browser": {
|
||||
"results": [
|
||||
{
|
||||
"width": 320,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"reducedMotion": "reduce",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"reducedMotion": "no-preference",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"reducedMotion": "no-preference",
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"requests": [
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=",
|
||||
"/oportunidades",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
|
||||
"/oportunidades?q=",
|
||||
"/oportunidades",
|
||||
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique"
|
||||
],
|
||||
"databaseAccess": false
|
||||
},
|
||||
"screenshots": [
|
||||
"outputs/portal-hero-typing-20261023/after-1440-no-preference.png",
|
||||
"outputs/portal-hero-typing-20261023/after-1440-reduce.png",
|
||||
"outputs/portal-hero-typing-20261023/after-1440-second-image.png",
|
||||
"outputs/portal-hero-typing-20261023/after-1920-reduce.png",
|
||||
"outputs/portal-hero-typing-20261023/after-320-reduce.png",
|
||||
"outputs/portal-hero-typing-20261023/after-390-no-preference.png",
|
||||
"outputs/portal-hero-typing-20261023/after-390-reduce.png",
|
||||
"outputs/portal-hero-typing-20261023/after-768-reduce.png",
|
||||
"outputs/portal-hero-typing-20261023/typed-1440.png",
|
||||
"outputs/portal-hero-typing-20261023/typed-390.png",
|
||||
"outputs/portal-hero-typing-20261023/typing-1440.png",
|
||||
"outputs/portal-hero-typing-20261023/typing-390.png"
|
||||
],
|
||||
"demonstrated": [
|
||||
"escrita progressiva",
|
||||
"título antes da frase",
|
||||
"texto completo no modo de movimento reduzido",
|
||||
"nome acessível completo durante a escrita",
|
||||
"pesquisa sem deslocamento durante a escrita",
|
||||
"reinício na selecção manual e automática de imagem",
|
||||
"pausa e retoma da escrita",
|
||||
"cursor com pausa",
|
||||
"Termos de uso ausente do hero",
|
||||
"pesquisa, teclado, CTA, API indisponível e contagens zero preservados"
|
||||
],
|
||||
"checks": {
|
||||
"types": {
|
||||
"command": "npm run types:check",
|
||||
"exitCode": 0
|
||||
},
|
||||
"lint": {
|
||||
"command": "vp lint resources/js/components/portal-hero.tsx tools/portal-hero-qa.mjs",
|
||||
"exitCode": 0
|
||||
},
|
||||
"build": {
|
||||
"command": "npm run build",
|
||||
"exitCode": 0,
|
||||
"pageAsset": "assets/PortalInvestidor-BbVHeTeI.js",
|
||||
"log": "outputs/portal-hero-typing-20261023/build-local.log"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"appliedLocally": true,
|
||||
"databaseAccess": false,
|
||||
"externalDeployment": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -48,3 +48,91 @@ Edge headless: 11 páginas públicas/autenticação verificadas a 390×844, com
|
||||
Página inicial também verificada a 1440×1000. Cenário fiscal exercitado no navegador: base 1.000 × taxa 10% × (1 − redução 50%) = 50. Resultados em `public-portal-browser-validation.json`.
|
||||
|
||||
Antes da migration local foi criado e restaurado o backup de 74 tabelas em `storage/app/private/portal-services-backup-20260927-090641/`. O backup completo anterior permanece em `C:/laragon/backups/portal-investidor-20260927-101224/` e representa o estado anterior a esta entrega.
|
||||
|
||||
## Adequação do hero — 23/10/2026
|
||||
|
||||
Origem: pedido expresso da Tempo nesta sessão para aproveitar o hero, as imagens e a barra de pesquisa de `D:/Projects/Tempo_Investidor/investidor`, usando a informação existente no V2. Aplicam-se as orientações permanentes do `AGENTS.md`, incluindo a distinção entre decisão da Tempo e escolha técnica, a preservação de dados e a separação de QA e activo.
|
||||
|
||||
Análise prévia: a página de entrada real do V2 é `PortalInvestidor.tsx`, servida por `PortalServicesController::home`. A referência é `domains/investor-portal/components/homepage/homepage-hero.tsx`, montada em `InstitutionalHomepage`. A referência usa Inertia 2, Framer Motion, tradução e navegação diferentes; o V2 usa Inertia 3 e pesquisa de oportunidades. Copiar essas dependências e `/pesquisa` ou `/aviso-legal` directamente criaria incompatibilidades e destinos inexistentes.
|
||||
|
||||
Plano executado:
|
||||
|
||||
1. Registar a apresentação anterior em computador e telemóvel, com os componentes reais e dados sintéticos.
|
||||
2. Copiar os dois originais efectivamente usados pela referência (`hero-1.png` e `hero-3.jpg`), conferir SHA-256 e gerar derivados WebP para computador e telemóvel. `hero-2.png` não faz parte da rotação da referência.
|
||||
3. Extrair `components/portal-hero.tsx` e aplicar fotografias em rotação, texto centrado e pesquisa branca com botão vermelho. Preservar os textos do V2, contagens da API, sectores, documentos, apoio, cabeçalho e rodapé.
|
||||
4. Manter a pesquisa de oportunidades em `/oportunidades?q=`, incluindo pesquisa vazia e caracteres acentuados. Conservar “Consultar oportunidades” e “Planear finanças”. A primeira iteração acrescentou o acesso jurídico `/termos-de-uso` ao hero; foi removido na revisão expressamente pedida pela Tempo abaixo.
|
||||
5. Verificar apresentação, teclado, imagens, rotação, pausa, movimento reduzido, falha da API, contagens zero, TypeScript, lint e compilação. Compilar primeiro o pacote QA e depois os assets locais em `public/build`.
|
||||
|
||||
Escolhas técnicas desta adaptação: React e transições CSS existentes, sem novas dependências; rotação de oito segundos, pausa por controlo explícito, foco, ponteiro ou separador oculto; apresentação estática quando o utilizador pede movimento reduzido. A primeira iteração apresentou o texto estático; a revisão abaixo introduz a escrita dinâmica pedida pela Tempo. O campo indica pesquisa de oportunidades e conserva exemplos que o catálogo suporta; esta alteração não implementa uma pesquisa global de legislação/licenciamento.
|
||||
|
||||
Evidência reproduzível: `tools/portal-hero-qa.mjs`, com Vite local e Playwright configurável por `HERO_QA_PLAYWRIGHT`/`HERO_QA_CHROMIUM`. Fixtures e capturas em `outputs/portal-hero-20261023/`; síntese persistente em `public-portal-hero-validation.json`. Os testes de interface interceptam a API e a navegação Inertia, sem acesso a Laravel, base de dados ou serviços externos. A compilação local aplica os assets desta pasta; não representa publicação noutro servidor.
|
||||
|
||||
Referências na matriz existente: `MASTER-efd550fd-0104` (composição da página), `MASTER-9dee08d0-0016` (contexto da pesquisa) e `MASTER-55050f60-0001` (entrada documental preservada). Esta prova cobre a adaptação visual e os percursos exercitados; não encerra os critérios funcionais mais amplos dessas linhas.
|
||||
|
||||
### Revisão: escrita dinâmica e remoção do acesso jurídico no hero
|
||||
|
||||
Decisão expressa da Tempo na continuação da sessão de 23/10/2026: animar “INVISTA EM MOÇAMBIQUE” e “Transforme informação em decisões e ideias em negócios reais.” como na página de referência, e remover “Termos de uso” do hero. A anterior apresentação estática e a inclusão desse link eram escolhas de implementação, agora substituídas pelo pedido do proprietário.
|
||||
|
||||
Análise e plano executado: reproduzir a sequência da referência (espera inicial de 250 ms, título a 50 ms por carácter, intervalo de 150 ms e frase a 28 ms por carácter); reiniciar ao trocar de imagem; acrescentar o cursor intermitente; reservar a dimensão final de ambas as linhas para manter a pesquisa imóvel; remover o link e o ícone jurídico do hero; verificar em computador e telemóvel e compilar o pacote local.
|
||||
|
||||
Implementação em `portal-hero.tsx`, com React existente e CSS do cursor em `app.css`, sem dependências novas. A pausa explícita suspende também a escrita, retomando do mesmo ponto. Quando o separador fica oculto a escrita pára; a preferência de movimento reduzido apresenta os dois textos completos. Leitores de ecrã recebem o texto completo uma única vez, independentemente do número de caracteres visíveis. Os temporizadores são limpos ao mudar de imagem ou desmontar o componente.
|
||||
|
||||
O script `tools/portal-hero-qa.mjs` demonstra a escrita progressiva, a ordem título→frase, o reinício manual/automático, a pausa/retoma, a posição estável da pesquisa, o modo sem movimento e a ausência de “Termos de uso” no hero. As capturas e resultados desta revisão ficam em `outputs/portal-hero-typing-20261023/`, preservando a evidência anterior. Síntese e compilação final registadas na revisão de `public-portal-hero-validation.json`; referência acrescentada à linha existente `MASTER-efd550fd-0104`, sem encerrar o seu critério mais amplo.
|
||||
|
||||
### Revisão: remoção dos controlos visíveis das imagens
|
||||
|
||||
Pedido expresso da Tempo na continuação de 23/10/2026: remover do hero o grupo com o botão de pausa e as duas bolinhas de selecção de imagem. Removidos o grupo, os ícones e o estado de pausa manual. A troca automática de imagens e a escrita dinâmica permanecem; conservam-se a pausa da rotação por foco/ponteiro, a suspensão em separador oculto e a preferência de movimento reduzido. Esta decisão substitui os controlos visíveis introduzidos como escolha de implementação nas revisões anteriores.
|
||||
|
||||
O script existente de interface foi adaptado para demonstrar a ausência dos controlos e a continuação da rotação automática, escrita e pesquisa, com capturas em `outputs/portal-hero-no-controls-20261023/`. Evidência e pacote local registados em `public-portal-hero-validation.json`, mantendo o histórico das revisões e os estados dos requisitos da matriz.
|
||||
|
||||
## Fotografias nas etapas de Como Investir — 23/10/2026
|
||||
|
||||
Origem: pedido expresso da Tempo para analisar e ilustrar exclusivamente os cartões “1. Explorar”, “2. Preparar” e “3. Acompanhar”. Aplicam-se as orientações permanentes de `AGENTS.md` de 12/10 e 22/10/2026; componentes e optimização de recursos são escolhas técnicas, sem novas regras operacionais.
|
||||
|
||||
Análise e plano: os cartões existentes são âncoras estáticas na home React/Inertia, numa grelha de três colunas a partir de 768 px. Explorar liga a `/oportunidades` e representa a identificação de sector, território e oportunidade; Preparar liga a `/como-investir` e representa percurso/documentação; Acompanhar liga a `/balcao-virtual` e representa envio/consulta de pedido e protocolo. Os recursos locais existentes ilustram outras secções; preparar três fotografias distintas para evitar repetição. Gerar cenas editoriais plausíveis com `imagegen`, rever composição, guardar originais e versões WebP locais, integrar imagens 16:9 com dimensões reservadas e carregamento diferido, manter textos/destinos/título vermelho, verificar comportamento responsivo, navegação e falhas de imagem, compilar. Âmbito exclusivamente visual, sem alterações em dados, autenticação, apoio ou rodapé. Referência documental na linha existente `MASTER-efd550fd-0104`, sem criar outra matriz nem encerrar o critério mais amplo.
|
||||
|
||||
Implementação: `components/investment-steps.tsx` reúne os três cartões, preservando títulos, descrições e âncoras. Fotografias individuais geradas com a ferramenta integrada `image_gen`, sem local ou projecto real atribuído: avaliação de oportunidade num parque empresarial genérico, preparação documental numa reunião e consulta de pedido no computador. Originais em `outputs/investment-steps-20261023/originals/`; recursos locais em `public/images/investment-steps/` com WebP 960×540, 480×270 e alternativa própria 240×135. Os cartões usam `srcset`, `sizes`, `loading="lazy"`, dimensões reservadas, corte 16:9, alt em português, foco visível e texto compacto com 16 px de preenchimento. Se também falhar a alternativa pequena, o cartão mantém a área reservada, descrição acessível e destino utilizável.
|
||||
|
||||
Verificação desta secção: componentes reais com API sintética e sem base de dados, seis larguras de 320 a 1920 px, duas densidades de ecrã, três imagens distintas, texto/destinos conservados, navegação por teclado, ausência de transbordo e deslocamento no carregamento, falhas dos recursos principais e de todos os recursos. Oito cenários sem erros JavaScript. Fotografias, prompts, hashes, capturas, relatório de browser e compilação documentados em `public-portal-steps-images.json` e `outputs/investment-steps-20261023/`. Ajuste restrito a esta secção; apoio, acesso à gestão e rodapé mantidos.
|
||||
|
||||
## Fotografias nos cartões de contexto do investimento — 23/10/2026
|
||||
|
||||
Origem: pedido expresso da Tempo para aplicar o anexo “Melhorar as imagens e a identidade visual do Portal do Investidor de Moçambique” exclusivamente à secção “Encontre o contexto do seu investimento”. O anexo define estética fotográfica editorial, imagens individuais contextualizadas, recursos locais, acessibilidade, desempenho e preservação dos cinco destinos e conteúdos. Aplicam-se as orientações permanentes de `AGENTS.md` de 12/10 e 22/10/2026; os pormenores de implementação abaixo são escolhas técnicas.
|
||||
|
||||
Análise prévia: Laravel/Inertia 3 e React, com Tailwind existente; os cinco cartões estavam embutidos em `pages/PortalInvestidor.tsx`. `EducationalArticleCard` destina-se a artigos com acções e metadados diferentes, pelo que não foi alterado. A secção foi extraída para `components/investment-context-cards.tsx`, mantendo o título, descrições, âncoras e grelha de uma/duas/quatro colunas. Não foram alteradas as páginas de destino, dados, APIs ou estilos globais.
|
||||
|
||||
Foram geradas cinco imagens distintas com a ferramenta integrada `image_gen`, segundo a skill `imagegen`: agro-indústria, terminal logístico, acompanhamento de instalação solar, formação técnica e análise documental. São imagens fotográficas ilustrativas geradas, sem localização ou instituição real atribuída, e não fotografias documentais de projectos cadastrados. A imagem do terminal foi revista para remover o camião com marca visível. Não foram usadas fotografias externas nem URLs de serviços de imagens. Originais seleccionados, prompts e hashes estão registados no manifesto `public-portal-context-images.json`; os originais foram preservados dentro do workspace.
|
||||
|
||||
Recursos finais em `public/images/investment-context/`: `clusters-agroindustria`, `corredores-logisticos-v2`, `projectos-ancora`, `educacao-formacao` e `documentos-formularios`, em WebP 960×540, com variantes 480×270 e alternativas 240×135. As imagens mantêm 16:9, `object-cover`, textos alternativos em português, dimensões reservadas, `srcset`, `sizes`, `loading="lazy"` e descodificação assíncrona. Cada cartão conserva o seu título e descrição; o foco usa o azul e os tons neutros pedidos no anexo.
|
||||
|
||||
Se um recurso principal falhar, o cartão usa a sua própria variante pequena, evitando repetir a fotografia de outra secção. Se também essa falhar, apresenta “Imagem indisponível” no mesmo espaço reservado, com descrição acessível e link operacional. Este é apenas um estado de erro; os cinco cartões usam imagens fotográficas na apresentação normal.
|
||||
|
||||
Verificação reproduzível: `tools/portal-context-cards-qa.mjs`, com o portal e cabeçalho reais, API sintética e acesso exclusivo aos recursos locais. Exercita seis larguras de ecrã, ecrãs de alta densidade, os cinco destinos por teclado, imagens carregadas e distintas, alinhamento, dimensões, ausência de deslocamento no carregamento e dois cenários de falha. Capturas e resultados em `outputs/investment-context-20261023/`; síntese, fontes/prompts e compilação em `public-portal-context-images.json`. Referências acrescentadas às linhas existentes `MASTER-efd550fd-0104` e `MASTER-55050f60-0001`, sem encerrar os respectivos critérios mais amplos.
|
||||
|
||||
Revisão expressa da Tempo de 23/10/2026: título da secção no vermelho da identidade existente (`#ed1c24`) e cartões mais compactos. Removidas as alturas mínimas de 56 e 72 px do título/descrição; reduzidos o preenchimento de 20 para 16 px, o intervalo de 8 para 6 px e as entrelinhas. As alturas passam a acompanhar o conteúdo, com alinhamento por linha da grelha, conservando imagens, textos e destinos. A reserva das dimensões das imagens mantém-se. Evidência desta revisão em `outputs/investment-context-compact-20261023/` e no campo `layoutRevision` do manifesto existente.
|
||||
|
||||
## Sectores da home e catálogo completo — 23/10/2026
|
||||
|
||||
Origem: pedido expresso da Tempo para adaptar a estrutura de “Áreas de Investimento” do projecto `D:/Projects/Tempo_Investidor/tempo-dashboard`, incluindo cartões, botão para mais sectores e respectiva página, e uniformizar a vermelho os títulos das secções da home. Aplicadas as orientações permanentes `AGENTS.md` de 12/10 e 22/10/2026: as decisões de produto são da Tempo; as escolhas técnicas abaixo não criam requisitos organizacionais.
|
||||
|
||||
Análise prévia: a referência usa `components/investor-portal/homepage/homepage-sectors.tsx` e `pages/investor-portal/sectors/index.tsx`. Mostra cinco cartões com fotografia e título sobreposto, resumo, “Saiba mais” e ligação central arredondada para o catálogo. O V2 já dispunha de `/sectores`, fichas `/sectores/{slug}`, especialização Fintech e dados de `SectorOrganization`. Foram reutilizados esses contratos e percursos, sem importar Framer Motion, traduções ou rotas exclusivas da referência.
|
||||
|
||||
Implementação: `components/public-sector-cards.tsx` partilha os cartões entre a home e o catálogo. A home mostra os primeiros cinco sectores, na ordem recebida da API, com “Ler mais” para a ficha existente e “Ver mais sectores” para `/sectores`. A página existente apresenta todos os sectores publicados, na mesma estrutura visual, e “Voltar ao início”. Mantêm-se nomes, descrições, contagens, valores zero, especializações e URLs canónicos recebidos. Descrições ausentes não são preenchidas com dados da referência. Fintech permanece ligado à ficha própria, sem âncoras HTML aninhadas.
|
||||
|
||||
Foram inventariados os onze JPG disponíveis em `storage/app/public/investor-portal/sectors` na referência, copiados os originais para evidência e preparados derivados locais WebP 960×540/480×270 em `public/images/sectors/`. O mapeamento visual por slug é de apresentação e não altera a taxonomia. Para Imobiliário e sectores sem imagem específica usa-se a vista empresarial genérica disponível na referência; para Bens de Consumo e Franquias usa-se a imagem de actividade industrial. A variante genérica também serve como alternativa em caso de erro; se ambas falharem, o título e as ligações continuam visíveis num fundo neutro. Não se atribui aos ficheiros uma origem oficial, licença externa ou localização documental não comprovada.
|
||||
|
||||
Os seis títulos de secção da home — contexto do investimento, sectores, Como Investir, oportunidades em destaque, documentos essenciais e apoio — usam `HOME_SECTION_HEADING` em `lib/portal-presentation.ts`: vermelho `#ed1c24`, 24 px, entrelinha de 32 px e peso 700. O cartão de Apoio passou a superfície clara para manter contraste com o título vermelho. O título principal animado do hero mantém a sua hierarquia própria. `PORTAL_MORE_LINK` centraliza o botão arredondado, contorno vermelho e estado de foco, seguindo a referência.
|
||||
|
||||
Verificação: `tools/portal-home-sectors-qa.mjs` carrega os componentes reais com dez sectores sintéticos, descrições nulas e especialização Fintech, sem base de dados. Exercita seis larguras, prévia de cinco/cartálogo de dez, estilos dos seis títulos, percurso por teclado, dez fichas, Fintech, regresso à home, catálogo vazio e dois cenários de indisponibilidade de imagens. Capturas, inventário, hashes e resultados em `outputs/home-sectors-20261023/`; resumo persistente em `public-portal-sectors-validation.json`. Referências na matriz existente: `MASTER-efd550fd-0104` e `MASTER-9f5719c8-0077`. Esta adaptação não encerra critérios mais amplos nem altera dados, permissões, regras operacionais ou publicação noutro servidor.
|
||||
|
||||
Pedido expresso da Tempo, 23/10/2026 — botão “Contactar Apoio ao Investidor”: em `layouts/portal-navigation.tsx`, a faixa de largura total foi substituída por um botão centrado, com largura ajustada ao texto, altura mínima de 44 px, preenchimento reduzido, sombra discreta e vermelho `#ed1c24`, conservando `/apoio` e o foco visível. Verificado a 320, 390 e 1440 px: cerca de 223×44 px, sem transbordo ou erros JavaScript. Evidência em `outputs/support-button-20261023/qa-results.json` e capturas; compilação local no mesmo directório. Ajuste de apresentação por decisão do proprietário, sem nova regra operacional nem alteração de dados.
|
||||
|
||||
Revisão anterior de 23/10/2026, superada pela correcção de âmbito abaixo: o Codex interpretou o pedido de abreviação como aplicável à zona inferior inteira e alterou indevidamente o botão de apoio e o rodapé partilhado. Essa interpretação não foi uma decisão da Tempo. A revisão removeu o bloco interno, abreviou o botão para “Apoio” e recolheu contactos/ligações em “Informações” num rodapé claro; estas alterações fora do bloco indicado foram revertidas após a clarificação expressa do proprietário. O registo desta revisão é histórico e não descreve a apresentação actual.
|
||||
|
||||
Evidência histórica da revisão superada: `outputs/footer-compact-20261023/qa-results.json`, capturas e `check.mjs`. Não constitui aceitação do âmbito pelo proprietário.
|
||||
|
||||
Correcção expressa da Tempo, 23/10/2026: o pedido abrange exclusivamente o bloco “INVESTE Moçambique / Portal do Investidor — conheça as oportunidades… / Acesso à gestão”. Em `pages/PortalInvestidor.tsx`, esse bloco fica abreviado num ícone de acesso à gestão, com nome acessível e descrição “Acesso à gestão”, destino `/acesso-gestao`, área de interacção de 44 px e fundo branco, sem o título e parágrafo anteriores. O botão de apoio foi restaurado exactamente à apresentação anterior à abreviação: “Contactar Apoio ao Investidor”, compacto, vermelho e sem ícone. `components/tempo-footer.tsx` e os estilos `.tempo-footer` foram integralmente restaurados ao estado anterior, incluindo contactos e ligações visíveis e o fundo original do rodapé geral. Não se altera autenticação nem dados. Esta correcção substitui a interpretação indevida acima e mantém as restantes alterações previamente pedidas.
|
||||
|
||||
Evidência da correcção: `outputs/footer-scope-correction-20261023/check.mjs`, `qa-results.json`, capturas e `build-local.log`; componentes reais com API sintética a 320, 390 e 1440 px, ícone de gestão focável, bloco específico claro sem parágrafo, botão de apoio completo/vermelho, rodapé geral restaurado, contactos e ligações legais visíveis, sem transbordo ou erros JavaScript. Lint dos componentes alterados e compilação local. Referência na linha existente `MASTER-efd550fd-0104`, sem encerramento do critério amplo nem alteração dos seus estados.
|
||||
|
||||
Continuação expressa da Tempo, 23/10/2026: acrescentado o título visível “Acesso à gestão” ao lado do ícone nesse mesmo bloco, para tornar o destino compreensível. O acesso mantém-se compacto, vermelho e com fundo claro, conservando `/acesso-gestao`. Ajuste restrito a essa ligação; apoio e rodapé geral conservam a apresentação restaurada. Referência: `MASTER-efd550fd-0104` e a evidência da revisão anterior acima. Verificado a 320, 390 e 1440 px, sem transbordo ou erros JavaScript; lint e compilação aprovados. Evidência em `outputs/management-label-20261023/qa-results.json`, capturas e `build-local.log`. No pedido seguinte, a Tempo determinou o alinhamento desse acesso à esquerda; aplicado `justify-start` apenas no seu contentor.
|
||||
|
||||
@@ -0,0 +1,748 @@
|
||||
{
|
||||
"date": "2026-10-23",
|
||||
"decision": "Pedido expresso da Tempo: adaptar Áreas de Investimento do tempo-dashboard aos sectores da home e ao catálogo completo, copiar a estrutura dos botões e uniformizar a vermelho os títulos de secção da home.",
|
||||
"reference": {
|
||||
"project": "D:/Projects/Tempo_Investidor/tempo-dashboard",
|
||||
"files": [
|
||||
"resources/js/components/investor-portal/homepage/homepage-sectors.tsx",
|
||||
"resources/js/pages/investor-portal/sectors/index.tsx"
|
||||
],
|
||||
"structure": "Cinco cartões com fotografia, título sobreposto, resumo e acção de leitura; botão central arredondado/contorno vermelho para o catálogo."
|
||||
},
|
||||
"analysisAndPlan": "docs/investmoz/public-portal-sections.md#sectores-da-home-e-catálogo-completo--23102026",
|
||||
"filesChanged": [
|
||||
"resources/js/components/public-sector-cards.tsx",
|
||||
"resources/js/lib/portal-presentation.ts",
|
||||
"resources/js/components/investment-context-cards.tsx",
|
||||
"resources/js/lib/portal-api.ts",
|
||||
"resources/js/pages/PortalInvestidor.tsx",
|
||||
"resources/js/pages/portal/services.tsx",
|
||||
"tools/portal-home-sectors-qa.mjs",
|
||||
"docs/investmoz/public-portal-sections.md",
|
||||
"docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv"
|
||||
],
|
||||
"behaviour": {
|
||||
"homePreview": 5,
|
||||
"previewOrder": "Ordem recebida da API, sem novos filtros de publicação.",
|
||||
"readMore": "Ler mais, para a URL recebida ou ficha existente /sectores/{slug}.",
|
||||
"moreSectors": "Ver mais sectores, botão arredondado para /sectores.",
|
||||
"catalogue": "Todos os sectores publicados devolvidos pelo cadastro existente; Voltar ao início para /.",
|
||||
"specializations": "Fintech conservado como ligação própria; sem âncoras aninhadas.",
|
||||
"countsAndDescriptions": "Preservados dados recebidos, incluindo zero e descrições ausentes; não importados textos ou registos do outro projecto.",
|
||||
"headings": [
|
||||
{
|
||||
"title": "Encontre o contexto do seu investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Sectores de investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Como Investir",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Oportunidades em destaque",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Documentos essenciais",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Apoio ao Investidor",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
}
|
||||
],
|
||||
"supportCard": "Superfície clara para contraste com o título vermelho."
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\agronegocio.jpg",
|
||||
"sourceBytes": 444057,
|
||||
"original": "outputs/home-sectors-20261023/originals/agronegocio.jpg",
|
||||
"sha256": "7337b689a6ad8b0b4c8d716ea356db14e22bd2982089825bb2382121ed3482f6",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/agronegocio.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 110310,
|
||||
"sha256": "3e9d3be22556300f880be39837f86e764e3d05433beff1da6694ef6cee1d5c34"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/agronegocio-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 31592,
|
||||
"sha256": "fd1f7624f7fd9224fc1c7bd11746806e419c645ba43c257b1edd9ba250cd47f1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\economia-azul.jpg",
|
||||
"sourceBytes": 456892,
|
||||
"original": "outputs/home-sectors-20261023/originals/economia-azul.jpg",
|
||||
"sha256": "1a2c04d0d9ed4a26129553daefd2ee18f6d820026ab1203acf3bcf91b0550f69",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/economia-azul.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 107626,
|
||||
"sha256": "bf3751e66839c1b3c585c91ffe65d32b9ee79ebd7d08e5e2c95c6cd581b04f8f"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/economia-azul-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 29350,
|
||||
"sha256": "aaf8ee66709d8e15c4304690dd03830b4368d14468a597d5617ccdb71d614331"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\energia.jpg",
|
||||
"sourceBytes": 386678,
|
||||
"original": "outputs/home-sectors-20261023/originals/energia.jpg",
|
||||
"sha256": "a164e2c8ef155a6a198ae400feca132e79ba75e022473893ec48ebfa0557f679",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/energia.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 81236,
|
||||
"sha256": "b5605d9bb8645c5042baa511aaf39fcb187080a48d8e067b130fc974c4c9196d"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/energia-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 21926,
|
||||
"sha256": "7e53977fc6ce11755455f8092ac3f8c7126f54d909a3935e522f8dae0ab834be"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\industria.jpg",
|
||||
"sourceBytes": 357131,
|
||||
"original": "outputs/home-sectors-20261023/originals/industria.jpg",
|
||||
"sha256": "d70c4257ac58a4b5e9f8b8b1a4cf6d98c0514043442c3ce46f5c98399038527f",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/industria.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 80422,
|
||||
"sha256": "ca8ddd2d65647f1ed7a9906271fa2ea3d027f310b89434fd7781a37a3d18322d"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/industria-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 25194,
|
||||
"sha256": "8bd695231209c0042b94e68b0e2095fa7202297708dabff0f78b337500b8c9b7"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\infraestruturas.jpg",
|
||||
"sourceBytes": 395383,
|
||||
"original": "outputs/home-sectors-20261023/originals/infraestruturas.jpg",
|
||||
"sha256": "ac6cde89507af68991faebee19540f955146724d3be901839b85a4098fe1674b",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/infraestruturas.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 86654,
|
||||
"sha256": "14754a59477e5c62a62c190be812c89d9616608b3c98acb6100ad70d94d34df0"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/infraestruturas-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 25772,
|
||||
"sha256": "eca58096087d05dae3eea2b4d063c103f9fc85b0ea20fc5051313cf9bfa1020f"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\logistica-transportes.jpg",
|
||||
"sourceBytes": 413702,
|
||||
"original": "outputs/home-sectors-20261023/originals/logistica-transportes.jpg",
|
||||
"sha256": "5976ead23da0d4e7d5debf875262f0229d389826aaeb7e7be4ebbac3f5129a9e",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/logistica-transportes.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 96254,
|
||||
"sha256": "0d235e7bbd160cccd3f97daa3337d6b27fde49db9782ad2ba0599c6a83022b6c"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/logistica-transportes-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 28816,
|
||||
"sha256": "32f5137e0a7f6a7894b186edb2ca38f12cdfdca04dda3588de3969fb3d2e333a"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\mineracao.jpg",
|
||||
"sourceBytes": 415161,
|
||||
"original": "outputs/home-sectors-20261023/originals/mineracao.jpg",
|
||||
"sha256": "7e6d1e5a17d434456ad937680a6c5501bd33bd624d2cc7ebf4b643185a9440aa",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/mineracao.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 96642,
|
||||
"sha256": "12c93df378f54108b13260c10d29789564af7844cc84bcd9281d2be917d83354"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/mineracao-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 26034,
|
||||
"sha256": "be3e7763fb8cd2e02f576204269821c40d93a7c155d8088a5084064b7269e4d6"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\outros-sectores.jpg",
|
||||
"sourceBytes": 328152,
|
||||
"original": "outputs/home-sectors-20261023/originals/outros-sectores.jpg",
|
||||
"sha256": "4aaac0869dc7e61c4c7a87e1af95beb108aa9be0da082702f6f70722e62b1e7a",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/outros-sectores.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 75414,
|
||||
"sha256": "f47b3d0da5b0cd5153d393c6f779484ba89043e50195e968338108e893a88077"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/outros-sectores-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 24936,
|
||||
"sha256": "a4fc00551d75c96a1648ad4f81d759d4d6ecd7e6fc5689689216a96934bd1ea2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\servicos-financeiros.jpg",
|
||||
"sourceBytes": 348386,
|
||||
"original": "outputs/home-sectors-20261023/originals/servicos-financeiros.jpg",
|
||||
"sha256": "f932a56248d5544bff92aa30f4af3a5a63044c311969d4370d06f157b097d570",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/servicos-financeiros.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 72144,
|
||||
"sha256": "15379485db7de7b2efebe8bdbd4cbd8e911eb34ab8a23b3dd114c849bb4ae65c"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/servicos-financeiros-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 20698,
|
||||
"sha256": "c21949201e35e63625d3d2e7520e10912fe65be1186954142b2784f6efca51ed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\tic-economia-digital.jpg",
|
||||
"sourceBytes": 417398,
|
||||
"original": "outputs/home-sectors-20261023/originals/tic-economia-digital.jpg",
|
||||
"sha256": "b09703bd079f55fb5a8ec3167ce96e1fdb1e5090813a81c817b9a33909427d94",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/tic-economia-digital.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 97892,
|
||||
"sha256": "b1d79c1a941085582d6a72799140bafed1bf38ec0201a4dfe5f6faf54ea55ae8"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/tic-economia-digital-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 28242,
|
||||
"sha256": "c4279a27ed402ca2feb62f11a8a954cbcae993912d3c8c27a2d9644fdf922114"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\turismo.jpg",
|
||||
"sourceBytes": 393339,
|
||||
"original": "outputs/home-sectors-20261023/originals/turismo.jpg",
|
||||
"sha256": "1a76db252bc8c1a9dc1f3c650bd95e55337c36a6f604753d61cad96c19b1875f",
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/sectors/turismo.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 93234,
|
||||
"sha256": "de46707dfdd8b98c48ec88d19f3df0b1d6c130adfcbd97d5a7c2e6fa2db62bef"
|
||||
},
|
||||
{
|
||||
"file": "public/images/sectors/turismo-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 27012,
|
||||
"sha256": "4d3d9a04b9426474dca9ba8895021b48fffbe7e4d4a4551272b8c832cf83488e"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"assetMapping": {
|
||||
"agronegocio": "agronegocio",
|
||||
"turismo": "turismo",
|
||||
"infraestruturas": "infraestruturas",
|
||||
"economia-azul": "economia-azul",
|
||||
"mercado-financeiro": "servicos-financeiros",
|
||||
"mineracao": "mineracao",
|
||||
"energia-especifica": "energia",
|
||||
"imobiliario": "outros-sectores",
|
||||
"bens-de-consumo-e-franquias": "industria",
|
||||
"tecnologias-de-informacao": "tic-economia-digital"
|
||||
},
|
||||
"assetSource": "JPG locais do projecto de referência indicado pelo proprietário, inventariados e copiados sem alteração; WebP derivados para apresentação. Sem alegação adicional de licença externa, origem oficial ou localização documental. Imagem genérica reutilizada como alternativa funcional e para sectores sem imagem específica.",
|
||||
"assetBytes960": 997828,
|
||||
"assetBytes480": 289572,
|
||||
"verification": {
|
||||
"browser": {
|
||||
"results": [
|
||||
{
|
||||
"width": 320,
|
||||
"homeSectors": 5,
|
||||
"catalogueSectors": 10,
|
||||
"homeColumns": 2,
|
||||
"headings": [
|
||||
{
|
||||
"title": "Encontre o contexto do seu investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Sectores de investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Como Investir",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Oportunidades em destaque",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Documentos essenciais",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Apoio ao Investidor",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"homeSectors": 5,
|
||||
"catalogueSectors": 10,
|
||||
"homeColumns": 2,
|
||||
"headings": [
|
||||
{
|
||||
"title": "Encontre o contexto do seu investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Sectores de investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Como Investir",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Oportunidades em destaque",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Documentos essenciais",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Apoio ao Investidor",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"homeSectors": 5,
|
||||
"catalogueSectors": 10,
|
||||
"homeColumns": 3,
|
||||
"headings": [
|
||||
{
|
||||
"title": "Encontre o contexto do seu investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Sectores de investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Como Investir",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Oportunidades em destaque",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Documentos essenciais",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Apoio ao Investidor",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"homeSectors": 5,
|
||||
"catalogueSectors": 10,
|
||||
"homeColumns": 5,
|
||||
"headings": [
|
||||
{
|
||||
"title": "Encontre o contexto do seu investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Sectores de investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Como Investir",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Oportunidades em destaque",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Documentos essenciais",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Apoio ao Investidor",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"homeSectors": 5,
|
||||
"catalogueSectors": 10,
|
||||
"homeColumns": 5,
|
||||
"headings": [
|
||||
{
|
||||
"title": "Encontre o contexto do seu investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Sectores de investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Como Investir",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Oportunidades em destaque",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Documentos essenciais",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Apoio ao Investidor",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"homeSectors": 5,
|
||||
"catalogueSectors": 10,
|
||||
"homeColumns": 5,
|
||||
"headings": [
|
||||
{
|
||||
"title": "Encontre o contexto do seu investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Sectores de investimento",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Como Investir",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Oportunidades em destaque",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Documentos essenciais",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
},
|
||||
{
|
||||
"title": "Apoio ao Investidor",
|
||||
"size": "24px",
|
||||
"weight": "700",
|
||||
"color": "rgb(237, 28, 36)",
|
||||
"lineHeight": "32px"
|
||||
}
|
||||
],
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"fail": "primary",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"fail": "all",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"emptyCatalogue": true,
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"navigation": [
|
||||
"/sectores",
|
||||
"/sectores",
|
||||
"/sectores",
|
||||
"/sectores",
|
||||
"/sectores",
|
||||
"/sectores/agronegocio",
|
||||
"/sectores",
|
||||
"/sectores/turismo",
|
||||
"/sectores",
|
||||
"/sectores/infraestruturas",
|
||||
"/sectores",
|
||||
"/sectores/economia-azul",
|
||||
"/sectores",
|
||||
"/sectores/mercado-financeiro",
|
||||
"/sectores",
|
||||
"/sectores/mineracao",
|
||||
"/sectores",
|
||||
"/sectores/energia-especifica",
|
||||
"/sectores",
|
||||
"/sectores/imobiliario",
|
||||
"/sectores",
|
||||
"/sectores/bens-de-consumo-e-franquias",
|
||||
"/sectores",
|
||||
"/sectores/tecnologias-de-informacao",
|
||||
"/sectores",
|
||||
"/sectores/mercado-financeiro/fintech",
|
||||
"/sectores",
|
||||
"/",
|
||||
"/sectores",
|
||||
"/sectores"
|
||||
],
|
||||
"databaseAccess": false
|
||||
},
|
||||
"script": "tools/portal-home-sectors-qa.mjs",
|
||||
"screenshots": [
|
||||
"outputs/home-sectors-20261023/catalogue-1024.png",
|
||||
"outputs/home-sectors-20261023/catalogue-1440.png",
|
||||
"outputs/home-sectors-20261023/catalogue-1920.png",
|
||||
"outputs/home-sectors-20261023/catalogue-320.png",
|
||||
"outputs/home-sectors-20261023/catalogue-390.png",
|
||||
"outputs/home-sectors-20261023/catalogue-768.png",
|
||||
"outputs/home-sectors-20261023/fallback-all.png",
|
||||
"outputs/home-sectors-20261023/fallback-primary.png",
|
||||
"outputs/home-sectors-20261023/home-sectors-1024.png",
|
||||
"outputs/home-sectors-20261023/home-sectors-1440.png",
|
||||
"outputs/home-sectors-20261023/home-sectors-1920.png",
|
||||
"outputs/home-sectors-20261023/home-sectors-320.png",
|
||||
"outputs/home-sectors-20261023/home-sectors-390.png",
|
||||
"outputs/home-sectors-20261023/home-sectors-768.png",
|
||||
"outputs/home-sectors-20261023/homepage-1440.png"
|
||||
],
|
||||
"existingFrontendTests": {
|
||||
"command": "node --test tests/frontend/portal-api.test.mjs tests/frontend/portal-navigation.test.mjs",
|
||||
"passed": 20
|
||||
},
|
||||
"types": {
|
||||
"command": "npm run types:check",
|
||||
"exitCode": 0
|
||||
},
|
||||
"lint": {
|
||||
"exitCode": 0
|
||||
},
|
||||
"build": {
|
||||
"command": "npm run build",
|
||||
"exitCode": 0,
|
||||
"assets": {
|
||||
"resources/js/pages/PortalInvestidor.tsx": "assets/PortalInvestidor-Q1OJxvfZ.js",
|
||||
"resources/js/pages/portal/services.tsx": "assets/services-DiuG4Zkv.js"
|
||||
},
|
||||
"log": "outputs/home-sectors-20261023/build-local.log"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"appliedLocally": true,
|
||||
"databaseChanged": false,
|
||||
"migrationsRun": false,
|
||||
"businessRulesChanged": false,
|
||||
"routesChanged": false,
|
||||
"sourceProjectChanged": false,
|
||||
"newDependencies": false,
|
||||
"externalDeployment": false
|
||||
},
|
||||
"limits": "Interface e navegação exercitadas com API e páginas públicas em fixtures; não equivale a demonstração integral dos workflows operacionais ou de critérios mais amplos da matriz.",
|
||||
"matrixReferences": [
|
||||
"MASTER-efd550fd-0104",
|
||||
"MASTER-9f5719c8-0077"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
{
|
||||
"date": "2026-10-23",
|
||||
"decision": "Pedido expresso da Tempo: analisar e acrescentar imagens adequadas exclusivamente aos três cartões de Como Investir.",
|
||||
"analysisAndPlan": "docs/investmoz/public-portal-sections.md#fotografias-nas-etapas-de-como-investir--23102026",
|
||||
"source": {
|
||||
"type": "Fotografias ilustrativas geradas por IA, sem atribuição a pessoas, instituições, locais ou projectos reais.",
|
||||
"tool": "image_gen, modo integrado",
|
||||
"skill": "C:/Users/jorge/.codex/skills/.system/imagegen/SKILL.md",
|
||||
"externalPhotographsUsed": false
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"key": "explorar",
|
||||
"prompt": "Use case: photorealistic-natural. Asset for a small navigation card on a Mozambique investor portal. Create one individual photorealistic editorial photograph, horizontal 16:9, professional restrained style, natural soft daylight, realistic skin and materials, neutral colors, documentary composition readable at 350px wide. Illustration of a plausible setting, no identifiable named project or official building. No text, lettering, logos, watermarks, flags, maps, diagrams, graphic overlays, montage, or advertisement styling. Keep main subject inside the central 80% for responsive crop. Stage 1 EXPLORE opportunities, sector and territory: two Mozambican business professionals, a woman and man in smart casual clothing, standing outdoors by a quiet access road at a modest business park, naturally discussing a tablet held by the woman. Generic unbranded low warehouse buildings and green productive land visible in background, credible southern African environment. Eye-level medium-wide candid composition, both people three-quarter view, looking at tablet and surroundings rather than camera. Business evaluation and discovery, no construction work or hard hats.",
|
||||
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-42eb9338-4350-40bf-9098-858c12994189.png",
|
||||
"original": "outputs/investment-steps-20261023/originals/explorar.png",
|
||||
"originalSha256": "a3c4ec1a0d2deaf87e0c8e765616c74ab70067eade9a862effce9e6befbbccb6",
|
||||
"originalDimensions": {
|
||||
"width": 1672,
|
||||
"height": 941
|
||||
},
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/investment-steps/explorar.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 52266,
|
||||
"sha256": "c3e151ae649ce3cb66f5babc39978fa5803d320cacc95a27e798db4ec2d45a4c"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-steps/explorar-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 19312,
|
||||
"sha256": "d3f383d9a58b23fada37472ea853f985aca8c3146123747d777b27704d32c7e0"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-steps/explorar-fallback.webp",
|
||||
"width": 240,
|
||||
"height": 135,
|
||||
"bytes": 5300,
|
||||
"sha256": "ba12ac4d2e81ac8a2c2e681667ccb08a3abfdba24de39e6e942d6378b700f2a9"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "preparar",
|
||||
"prompt": "Use case: photorealistic-natural. Asset for a small navigation card on a Mozambique investor portal. Create one individual photorealistic editorial photograph, horizontal 16:9, professional restrained style, natural soft daylight, realistic skin and materials, neutral colors, documentary composition readable at 350px wide. Illustration of a plausible setting, no identifiable named project or official building. No text, lettering, logos, watermarks, flags, maps, diagrams, graphic overlays, montage, or advertisement styling. Keep main subject inside the central 80% for responsive crop. Stage 2 PREPARE investment and organize documents: a Mozambican woman entrepreneur and a male business adviser seated at a light wood office desk, calmly reviewing and sorting a small tidy set of document folders and loose sheets beside a closed laptop. One points to a page while the other organizes a folder. No visible readable writing or personal data; papers at an angle. Bright modest professional meeting room, view from slightly above table height, hands and folders clear in foreground, natural candid interaction, realistic anatomy.",
|
||||
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-5f07618a-1e86-42d9-aaf8-3acd0abf2164.png",
|
||||
"original": "outputs/investment-steps-20261023/originals/preparar.png",
|
||||
"originalSha256": "679a6875bb3ec79a9b71c27c9d785334376746be2bb305f461db9e19b0a0b7ea",
|
||||
"originalDimensions": {
|
||||
"width": 1672,
|
||||
"height": 941
|
||||
},
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/investment-steps/preparar.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 57942,
|
||||
"sha256": "6de28042d3abce238a3680fb878779682f3672c3feb97b7e42b72e9a0a0ffbac"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-steps/preparar-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 19950,
|
||||
"sha256": "671ba0fc207f85833b91027d3f6dad91599e95702cfea439e558906f92a2bfac"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-steps/preparar-fallback.webp",
|
||||
"width": 240,
|
||||
"height": 135,
|
||||
"bytes": 6008,
|
||||
"sha256": "645b34cd60b31fdc8e6586dedb71061c08add9ff985ce90909333b0bd1e0aa3e"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "acompanhar",
|
||||
"prompt": "Use case: photorealistic-natural. Asset for a small navigation card on a Mozambique investor portal. Create one individual photorealistic editorial photograph, horizontal 16:9, professional restrained style, natural soft daylight, realistic skin and materials, neutral colors, documentary composition readable at 350px wide. Illustration of a plausible setting, no identifiable named project or official building. No text, lettering, logos, watermarks, flags, maps, diagrams, graphic overlays, montage, or advertisement styling. Keep main subject inside the central 80% for responsive crop. Stage 3 FOLLOW UP submitted request and protocol: a Mozambican male entrepreneur seated at a tidy desk using a laptop to check an online application, one hand naturally on trackpad, a small closed document folder nearby. Three-quarter side view showing focused face and laptop, warm daylight from side window, modest contemporary office with soft greenery out of focus. Laptop screen is oblique and softly out of focus with only indistinct neutral interface rows, no readable text, official seal, logo or invented dashboard details. Convey calm digital self-service and progress checking, not a call centre or signing documents.",
|
||||
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-f896c5f8-19a0-49b0-af8e-0d12bfe4c7aa.png",
|
||||
"original": "outputs/investment-steps-20261023/originals/acompanhar.png",
|
||||
"originalSha256": "17d453445f2afb4c7641909950d2ecfb8276c433a4e28eeee94d0a57d1bdad46",
|
||||
"originalDimensions": {
|
||||
"width": 1672,
|
||||
"height": 941
|
||||
},
|
||||
"derivatives": [
|
||||
{
|
||||
"file": "public/images/investment-steps/acompanhar.webp",
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"bytes": 43722,
|
||||
"sha256": "fe764c8161de36ee597f856face5341260670d1a28ab482bd01a0f5120ad0588"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-steps/acompanhar-480.webp",
|
||||
"width": 480,
|
||||
"height": 270,
|
||||
"bytes": 16176,
|
||||
"sha256": "79ceaecb0ef23c6729e43d04b7e91493d6784ea9f658d540dda3550702dc3623"
|
||||
},
|
||||
{
|
||||
"file": "public/images/investment-steps/acompanhar-fallback.webp",
|
||||
"width": 240,
|
||||
"height": 135,
|
||||
"bytes": 5298,
|
||||
"sha256": "cd4530cfef4f19f23f640ceae59cfd73d0ee269643118b458f2a4748f5845fc6"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"title": "1. Explorar",
|
||||
"description": "Identifique o sector, território e oportunidade.",
|
||||
"href": "/oportunidades",
|
||||
"image": "explorar"
|
||||
},
|
||||
{
|
||||
"title": "2. Preparar",
|
||||
"description": "Conheça o percurso e organize os documentos.",
|
||||
"href": "/como-investir",
|
||||
"image": "preparar"
|
||||
},
|
||||
{
|
||||
"title": "3. Acompanhar",
|
||||
"description": "Envie o pedido e acompanhe o protocolo.",
|
||||
"href": "/balcao-virtual",
|
||||
"image": "acompanhar"
|
||||
}
|
||||
],
|
||||
"presentation": {
|
||||
"component": "resources/js/components/investment-steps.tsx",
|
||||
"homeIntegration": "resources/js/pages/PortalInvestidor.tsx",
|
||||
"aspectRatio": "16:9",
|
||||
"responsiveImages": [
|
||||
960,
|
||||
480
|
||||
],
|
||||
"fallback": "Variante própria de 240 px; se também falhar, espaço reservado com descrição acessível e Imagem indisponível.",
|
||||
"columns": "Uma coluna abaixo de 768 px, três a partir de 768 px.",
|
||||
"compactText": "16 px de preenchimento, sem altura mínima de texto.",
|
||||
"accessibility": "Alt em português, nomes e descrições das âncoras, foco visível, dimensões reservadas e loading lazy.",
|
||||
"headings": "Título vermelho partilhado da home preservado."
|
||||
},
|
||||
"verification": {
|
||||
"browser": {
|
||||
"results": [
|
||||
{
|
||||
"width": 320,
|
||||
"dpr": 1,
|
||||
"fail": "none",
|
||||
"columns": 1,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-480.webp",
|
||||
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7820512820512822
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-480.webp",
|
||||
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7820512820512822
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-480.webp",
|
||||
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7820512820512822
|
||||
}
|
||||
],
|
||||
"layoutShift": false,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"dpr": 2,
|
||||
"fail": "none",
|
||||
"columns": 1,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/explorar.webp",
|
||||
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7755102040816326
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/preparar.webp",
|
||||
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7755102040816326
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar.webp",
|
||||
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7755102040816326
|
||||
}
|
||||
],
|
||||
"layoutShift": false,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"dpr": 1,
|
||||
"fail": "none",
|
||||
"columns": 3,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-480.webp",
|
||||
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7734375
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-480.webp",
|
||||
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7734375
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-480.webp",
|
||||
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7734375
|
||||
}
|
||||
],
|
||||
"layoutShift": false,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"dpr": 1,
|
||||
"fail": "none",
|
||||
"columns": 3,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-480.webp",
|
||||
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7784090909090908
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-480.webp",
|
||||
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7784090909090908
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-480.webp",
|
||||
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7784090909090908
|
||||
}
|
||||
],
|
||||
"layoutShift": false,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"dpr": 2,
|
||||
"fail": "none",
|
||||
"columns": 3,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/explorar.webp",
|
||||
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.775
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/preparar.webp",
|
||||
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.775
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar.webp",
|
||||
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.775
|
||||
}
|
||||
],
|
||||
"layoutShift": false,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"dpr": 1,
|
||||
"fail": "none",
|
||||
"columns": 3,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-480.webp",
|
||||
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.775
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-480.webp",
|
||||
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.775
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-480.webp",
|
||||
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.775
|
||||
}
|
||||
],
|
||||
"layoutShift": false,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"dpr": 2,
|
||||
"fail": "primary",
|
||||
"columns": 1,
|
||||
"photos": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-fallback.webp",
|
||||
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7755102040816326
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-fallback.webp",
|
||||
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7755102040816326
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-fallback.webp",
|
||||
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
|
||||
"lazy": "lazy",
|
||||
"ratio": 1.7755102040816326
|
||||
}
|
||||
],
|
||||
"layoutShift": false,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"dpr": 2,
|
||||
"fail": "all",
|
||||
"columns": 1,
|
||||
"photos": [],
|
||||
"layoutShift": false,
|
||||
"overflow": false
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"navigation": [
|
||||
"/oportunidades",
|
||||
"/como-investir",
|
||||
"/balcao-virtual"
|
||||
],
|
||||
"databaseAccess": false
|
||||
},
|
||||
"script": "outputs/investment-steps-20261023/check.mjs",
|
||||
"screenshots": [
|
||||
"outputs/investment-steps-20261023/cards-1024-none.png",
|
||||
"outputs/investment-steps-20261023/cards-1440-none.png",
|
||||
"outputs/investment-steps-20261023/cards-1920-none.png",
|
||||
"outputs/investment-steps-20261023/cards-320-none.png",
|
||||
"outputs/investment-steps-20261023/cards-390-all.png",
|
||||
"outputs/investment-steps-20261023/cards-390-none.png",
|
||||
"outputs/investment-steps-20261023/cards-390-primary.png",
|
||||
"outputs/investment-steps-20261023/cards-768-none.png"
|
||||
],
|
||||
"lint": {
|
||||
"exitCode": 0
|
||||
},
|
||||
"types": {
|
||||
"command": "npm run types:check",
|
||||
"exitCode": 0,
|
||||
"log": "outputs/investment-steps-20261023/types.log"
|
||||
},
|
||||
"build": {
|
||||
"command": "npm run build",
|
||||
"exitCode": 0,
|
||||
"homeAsset": "assets/PortalInvestidor-C1QTZ3-t.js",
|
||||
"log": "outputs/investment-steps-20261023/build-local.log"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"appliedLocally": true,
|
||||
"otherSectionsChanged": false,
|
||||
"supportAndFooterChanged": false,
|
||||
"databaseAccess": false,
|
||||
"businessRulesChanged": false,
|
||||
"routesChanged": false,
|
||||
"newDependencies": false,
|
||||
"externalDeployment": false
|
||||
},
|
||||
"matrixReferences": [
|
||||
"MASTER-efd550fd-0104"
|
||||
],
|
||||
"limits": "Componentes reais com API sintética e destinos interceptados para verificar a navegação; não demonstra os workflows operacionais completos nem encerra o critério mais amplo da matriz."
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,397 @@
|
||||
{
|
||||
"status": "installed_scoped_habitual_quarterly_ISPC_and_delta_verified",
|
||||
"authorization": "Tempo25/10:concluir instalação6af7092e peloGit combackup/restaurofresco,conferirdelta,preservarL3eprovasaceites;ISPCnãoéintegralmenteconcluído.",
|
||||
"candidate_installed": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"previous_installed": "fa32a2b9e127da9c4d764dd99ef58ee43b770aa6",
|
||||
"prepared_receipt": "ispc-l4-prepared-delivery-20261025.json",
|
||||
"authenticated_preflight": {
|
||||
"free_bytes": 7635865600,
|
||||
"existing_pipeline_minimum_bytes": 5368709120,
|
||||
"space_guard_satisfied": true,
|
||||
"installed": {
|
||||
"status": "installed",
|
||||
"commit": "fa32a2b9e127da9c4d764dd99ef58ee43b770aa6",
|
||||
"release": "/var/www/investmoz-homologacao/releases/fa32a2b9e127da9c4d764dd99ef58ee43b770aa6-ad33eeab",
|
||||
"previous_release": "/var/www/investmoz-homologacao/releases/7ad187f9f0745245268e3ed5317547a96b4700f3-066618f1",
|
||||
"database_migrations": 0,
|
||||
"content_imports": 0
|
||||
},
|
||||
"current_link": "/var/www/investmoz-homologacao/releases/fa32a2b9e127da9c4d764dd99ef58ee43b770aa6-ad33eeab",
|
||||
"release_revision": {
|
||||
"commit": "fa32a2b9e127da9c4d764dd99ef58ee43b770aa6",
|
||||
"branch": "main",
|
||||
"built_at": "2026-10-25T15:12:37.806036+00:00",
|
||||
"manifest_sha256": "cddac25dc66b3e555f2380cf3a9d4b2da6501cd03927c183c832a05b0ee5c993"
|
||||
},
|
||||
"service": "ExecMainStatus=0\nActiveState=inactive\nSubState=dead\n",
|
||||
"configuration_keys": [
|
||||
"repository",
|
||||
"branch",
|
||||
"target_url",
|
||||
"legacy_root",
|
||||
"state_root",
|
||||
"release_root",
|
||||
"current_link",
|
||||
"shared_assets",
|
||||
"php",
|
||||
"schema_contract",
|
||||
"ci_environment",
|
||||
"postgis_image",
|
||||
"test_database",
|
||||
"qa_port",
|
||||
"retention",
|
||||
"webhook_secret_file"
|
||||
],
|
||||
"safe_paths": {
|
||||
"repository": "nala/Investidor-V2",
|
||||
"release_root": "/var/www/investmoz-homologacao/releases",
|
||||
"postgis_image": "postgis/postgis@sha256:01a6a70e41e6c4467c8f55f6063555ed72db2d6662cd0d571040d42eadaeb6f6"
|
||||
},
|
||||
"pipeline_sha256": "adec1c17654f3a8658b83b134f1e75f95391b235738552721906c3236e388604"
|
||||
},
|
||||
"fresh_backup": {
|
||||
"stage": "/var/backups/investmoz-ispc-l4-delivery-20261025/backup",
|
||||
"created_at": "2026-10-25T15:25:16Z",
|
||||
"target": {
|
||||
"database": "teste_investidor",
|
||||
"port": 5432
|
||||
},
|
||||
"tables": 174,
|
||||
"files_verified": 369,
|
||||
"dump_sha256": "77ba24736ff574d9273845359db90d1a032ff0f39d7f9195efaeff842d5b5857",
|
||||
"files_sha256": "59917d3f0420dc8ede0562da70ae5f67877aa99f77313950f9e1d840b7505e3b",
|
||||
"restore_verified": false
|
||||
},
|
||||
"isolated_restore": {
|
||||
"restore_verified": true,
|
||||
"tables_verified": 174,
|
||||
"container_network": "none",
|
||||
"database": "ispc_l4_restore_20261025",
|
||||
"port": 55441,
|
||||
"source_dump_sha256": "77ba24736ff574d9273845359db90d1a032ff0f39d7f9195efaeff842d5b5857",
|
||||
"active_database_writes": 0
|
||||
},
|
||||
"installed_protected_delta": {
|
||||
"installed": {
|
||||
"status": "installed",
|
||||
"commit": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"release": "/var/www/investmoz-homologacao/releases/6af7092e83b9b3932712aa7a9f527393fcec4da8-647c8e2e",
|
||||
"previous_release": "/var/www/investmoz-homologacao/releases/fa32a2b9e127da9c4d764dd99ef58ee43b770aa6-ad33eeab",
|
||||
"database_migrations": 0,
|
||||
"content_imports": 0
|
||||
},
|
||||
"runtime_files_verified": 9,
|
||||
"L3_unchanged_runtime_files_verified": 11,
|
||||
"documentary_files_verified": 17,
|
||||
"persistent_files_and_env_verified": 369,
|
||||
"tables_compared": 174,
|
||||
"changed_tables": {
|
||||
"cache": {
|
||||
"before": {
|
||||
"rows": 106,
|
||||
"checksum": "81ca74112a72e3638ad335678ebfd0ef"
|
||||
},
|
||||
"after": {
|
||||
"rows": 106,
|
||||
"checksum": "e799b80b260c8c23326c72a5d7c52881"
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"before": {
|
||||
"rows": 156,
|
||||
"checksum": "5b29e80e2877c2e0bf0c81e046230987"
|
||||
},
|
||||
"after": {
|
||||
"rows": 157,
|
||||
"checksum": "e83a47d5ba184b55f1680f52ff7e0993"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preserved_counts": {
|
||||
"published_fiches": 29,
|
||||
"relations": 28,
|
||||
"geometries": 408
|
||||
},
|
||||
"migration_count": 0,
|
||||
"content_imports": 0,
|
||||
"comparison_role": "postgres_before_and_after;sameexportedsnapshotbaseline;noapplicationRLSvisibilityinference",
|
||||
"writes_by_verification": 0
|
||||
},
|
||||
"operational_delta": {
|
||||
"status": "operational_delta_reconciled",
|
||||
"comparison_identity": "postgres before/after;isolated restored baseline;no RLS visibility inference",
|
||||
"cache": "queue:restart value/expiry and request-throttle counter/timer pair;no deleted cache rows",
|
||||
"sessions": "anonymous sessions created by exact delivery verification user-agent",
|
||||
"delta": {
|
||||
"cache": {
|
||||
"before_rows": 106,
|
||||
"after_rows": 106,
|
||||
"changes": [
|
||||
{
|
||||
"key_sha256": "8d6ea77eb4b34d86d207362d583cfa8bf2880b907becd9b00398964482c6b4bd",
|
||||
"kind": "updated",
|
||||
"fields": [
|
||||
"expiration"
|
||||
],
|
||||
"key_pattern": "investemoz-cache-{hash}",
|
||||
"is_queue_restart_key": false,
|
||||
"expiration_unchanged": false,
|
||||
"throttle_timer_pair": true
|
||||
},
|
||||
{
|
||||
"key_sha256": "2f251473865d14b953236ab95b1521764754d76abd79b58ca7d38c96dfe2fab1",
|
||||
"kind": "updated",
|
||||
"fields": [
|
||||
"value",
|
||||
"expiration"
|
||||
],
|
||||
"key_pattern": "investemoz-cache-{hash}:timer",
|
||||
"is_queue_restart_key": false,
|
||||
"expiration_unchanged": false,
|
||||
"throttle_timer_pair": true
|
||||
},
|
||||
{
|
||||
"key_sha256": "7f7dd83f47f5bf404d55e6e9f1367948721579970b195680645e356031a070ff",
|
||||
"kind": "updated",
|
||||
"fields": [
|
||||
"value",
|
||||
"expiration"
|
||||
],
|
||||
"key_pattern": "investemoz-cache-illuminate:queue:restart",
|
||||
"is_queue_restart_key": true,
|
||||
"expiration_unchanged": false,
|
||||
"throttle_timer_pair": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessions": {
|
||||
"before_rows": 156,
|
||||
"after_rows": 159,
|
||||
"changes": [
|
||||
{
|
||||
"key_sha256": "5045187d392f685b58ae0ce1749c84eb5a30c5ab441aa0e65cfc6d905d0180fa",
|
||||
"kind": "added",
|
||||
"fields": [
|
||||
"id",
|
||||
"ip_address",
|
||||
"user_agent",
|
||||
"payload",
|
||||
"last_activity"
|
||||
],
|
||||
"anonymous_user": true,
|
||||
"delivery_user_agent": true
|
||||
},
|
||||
{
|
||||
"key_sha256": "3d45feddb6a0eafd7d40e3efe516866803e4432d922c6a6b6e839276d8461075",
|
||||
"kind": "added",
|
||||
"fields": [
|
||||
"id",
|
||||
"ip_address",
|
||||
"user_agent",
|
||||
"payload",
|
||||
"last_activity"
|
||||
],
|
||||
"anonymous_user": true,
|
||||
"delivery_user_agent": true
|
||||
},
|
||||
{
|
||||
"key_sha256": "712b82ddcfb6f6af6a4ea5937ae2d5195976cf4c808e6202a4b234eef1560210",
|
||||
"kind": "added",
|
||||
"fields": [
|
||||
"id",
|
||||
"ip_address",
|
||||
"user_agent",
|
||||
"payload",
|
||||
"last_activity"
|
||||
],
|
||||
"anonymous_user": true,
|
||||
"delivery_user_agent": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"business_tables_to_confirm_separately": 172,
|
||||
"active_data_writes_by_comparison": 0,
|
||||
"restore_network": "none"
|
||||
},
|
||||
"installed_public": {
|
||||
"status": "installed_delta_verified",
|
||||
"served_revision": {
|
||||
"commit": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"branch": "main",
|
||||
"built_at": "2026-10-25T15:29:39.962545+00:00",
|
||||
"manifest_sha256": "0761702f0aa0f54dd4e7dac14efadf290830c52a5762a97733d82f5332fb44a8"
|
||||
},
|
||||
"manifest_hash_matches": true,
|
||||
"compiled_delta_markers": {
|
||||
"ISPC — enquadramento e opção": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"ISPC — apuramento trimestral": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"ISPC — retenção em aquisições ocasionais": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Calcular e guardar apuramento trimestral ISPC": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Conferir e guardar retenção ISPC": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Repartição do IVA documental em MZN": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Reconciliação da dedução anterior": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Cálculos indeterminados e condições para resolver": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Preparação dos quadros L3 e formalidades": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Calcular e guardar restituição E": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Calcular e guardar acompanhamento D": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Calcular e guardar substituições C": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
]
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"file": "assets/project-dossier-document-DlW49dDv.js",
|
||||
"status": 200,
|
||||
"sha256": "7002e22485098102c9a49e6f637a8818a4ecb153cf58f9c9cfb377e1b8a3d5a0"
|
||||
},
|
||||
{
|
||||
"file": "assets/simulation-form-BqVD91Ek.js",
|
||||
"status": 200,
|
||||
"sha256": "85cbd533c5bc92af5a448db71da006d4346d2d0adb09dde825db0aa19ad5048c"
|
||||
},
|
||||
{
|
||||
"file": "assets/project-dossier-B9kwzlsI.js",
|
||||
"status": 200,
|
||||
"sha256": "c2b62654e0a286c0dfb16a44f3aac513064c8db305cccc827388de35cc6c720f"
|
||||
},
|
||||
{
|
||||
"file": "assets/services-tj9wihZS.js",
|
||||
"status": 200,
|
||||
"sha256": "9e88d806f7c8e7193dda407ba1efe8bf720e5eeb76ca40798f3e6c4914e57f4b"
|
||||
}
|
||||
],
|
||||
"simulator_status": 200,
|
||||
"simulator_entry": "assets/app-CkfkH6Xv.js",
|
||||
"existing_fiscal_availability": {
|
||||
"data": {
|
||||
"enabled": true,
|
||||
"coverage_start": "2026-01-01",
|
||||
"coverage_end": "2026-12-31",
|
||||
"message": "IVA geral sobre base líquida disponível; enquadramento declarado."
|
||||
}
|
||||
},
|
||||
"synthetic_private_writes": 0,
|
||||
"method": "Read-only installedrevision/manifest/newcompiledcatalogueandreaders/simulatorentry;acceptedQAproofsreused"
|
||||
},
|
||||
"pipeline_status": {
|
||||
"service": "ExecMainStatus=0\nActiveState=inactive\nSubState=dead\n",
|
||||
"installed": {
|
||||
"status": "installed",
|
||||
"commit": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"release": "/var/www/investmoz-homologacao/releases/6af7092e83b9b3932712aa7a9f527393fcec4da8-647c8e2e",
|
||||
"previous_release": "/var/www/investmoz-homologacao/releases/fa32a2b9e127da9c4d764dd99ef58ee43b770aa6-ad33eeab",
|
||||
"database_migrations": 0,
|
||||
"content_imports": 0
|
||||
},
|
||||
"latest_report": {
|
||||
"status": "installed",
|
||||
"commit": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"release": "/var/www/investmoz-homologacao/releases/6af7092e83b9b3932712aa7a9f527393fcec4da8-647c8e2e",
|
||||
"previous_release": "/var/www/investmoz-homologacao/releases/fa32a2b9e127da9c4d764dd99ef58ee43b770aa6-ad33eeab",
|
||||
"database_migrations": 0,
|
||||
"content_imports": 0,
|
||||
"requested_commits": [
|
||||
"6af7092e83b9b3932712aa7a9f527393fcec4da8"
|
||||
],
|
||||
"finished_at": "2026-10-25T15:29:42.957691+00:00"
|
||||
},
|
||||
"log_file": "1792941995601985924.log",
|
||||
"summaries": [
|
||||
"OK (1 test, 3106 assertions)",
|
||||
"built in 31.63s",
|
||||
"OK (59 tests, 317 assertions)",
|
||||
"# tests 782",
|
||||
"# pass 782",
|
||||
"# fail 0"
|
||||
],
|
||||
"log_bytes": 227445
|
||||
},
|
||||
"archived_old_build_dependencies": {
|
||||
"status": "dependencies_archived_and_verified",
|
||||
"versions": 8,
|
||||
"free_before": 5531414528,
|
||||
"free_after": 7635984384,
|
||||
"receipt": "/var/backups/investmoz-build-dependencies-ispc-l4-20261025/receipt.json",
|
||||
"protected": [
|
||||
"/var/www/investmoz-homologacao/releases/fa32a2b9e127da9c4d764dd99ef58ee43b770aa6-ad33eeab",
|
||||
"/var/www/investmoz-homologacao/releases/7ad187f9f0745245268e3ed5317547a96b4700f3-066618f1"
|
||||
],
|
||||
"active_or_previous_release_modified": false,
|
||||
"source_data_backups_deleted": 0
|
||||
},
|
||||
"accepted_QA_proofs_reused": {
|
||||
"prior_proofs": "L4-A12testes eL1/L2/L3/P001não repetidos;inventário eaceitações reutilizados",
|
||||
"new_frontend": "13distinctpassed:10quarter/3withholding;frontend-final.txt",
|
||||
"private_API": "1test/21assertions passed;loopback55441/simulator_fiscal_delta_20261021_test,DatabaseTransactions rollback;otheraccount404;api.txt",
|
||||
"UI": "Componentesreais:avaliar/opçãoguardada→calcular→guardarficheiroQA→recuperar→relatório;persistênciaprivadaconferidaemAPIseparadamente;ui-recovered-report.txt/png",
|
||||
"report_fix": "GrupoISPCmapeadoàsecçãofinanceira;testeactualdoexportadorincluiimposto/opção;ausênciaidentificadanademonstraçãoecorrigidaantesdeconcluir.",
|
||||
"types": "exit0;types-final.txt",
|
||||
"build": "exit0;build-final.txt",
|
||||
"inventory": "outputs/ispc-l4b-20261025/preservation-before-tests.json;reutiliza8143ficheirosinventariadoseL1fechados"
|
||||
},
|
||||
"manual_functional_tests_repeated": false,
|
||||
"pipeline_checks": "Existing Git process mandatory checks only;no new discretionary functional round.",
|
||||
"example": {
|
||||
"context": "opportunity:375;SYNTHETIC QA",
|
||||
"option": "documentada2026-01-01",
|
||||
"quarter": "2026/T2",
|
||||
"prior_turnover_MZN": "100000",
|
||||
"quarter_base_MZN": "200000",
|
||||
"rate_percent": 12,
|
||||
"tax_MZN": "24000",
|
||||
"payment_MZN": "24000",
|
||||
"declaration": "mantida",
|
||||
"other_fiscal_books": "exactamente preservados"
|
||||
},
|
||||
"remaining_L4_specific": [
|
||||
"Operações ocasionais próprias3(3–4)enquanto sujeito passivo,distintas da retenção do adquirente;mantidas indeterminadas no percurso específico.",
|
||||
"Aquisições acima limiteglobal2.500.000:correspondência do tratamento próprio;nãoliquidar5%porpresunção.",
|
||||
"Formulário/modelo,instruções eprocedimentos da regulamentação2026;relatório depreparação não édeclaraçãosubmetida. Aviso primário do Governo confirma aprovação de novo regulamento,mas não contém articulado/modelos: https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/ . Sem impor segunda validação jurídica ou repetir pesquisa sem nova evidência."
|
||||
],
|
||||
"L3": "Preserved;11runtimehashes,17originals and additive integrations verified. Specific residuals remain in existing matrix.",
|
||||
"preserved_L3_amounts": {
|
||||
"documentary_VAT_MZN": 16,
|
||||
"deduction_this_route_MZN": 0,
|
||||
"recognised_MZN": 12,
|
||||
"received_MZN": 7,
|
||||
"awaiting_receipt_MZN": 5
|
||||
},
|
||||
"residuals": "P001/P058awaitdefinedclarificationswithoutblocking;P084/P085closed;statistics/AIdeferred.",
|
||||
"schema_migrations": 0,
|
||||
"content_imports": 0,
|
||||
"ISPC_integrally_completed": false,
|
||||
"proof_hashes": {
|
||||
"outputs/ispc-l4-delivery-20261025/preflight.txt": "985e4cabc9f4f80ee55f89aa9a20580c1bc3b691225241165b01cc66ba4b5d19",
|
||||
"outputs/ispc-l4-delivery-20261025/backup.txt": "23f72466038eed5153c93408e5acd508b5c6a66fc0e4c3a9bd30a580367fe8ea",
|
||||
"outputs/ispc-l4-delivery-20261025/restore.txt": "d0100e1378c2e0e8a4be61de640e0e331695721080bae16d8a7a708cf4d4eb83",
|
||||
"outputs/ispc-l4-delivery-20261025/integration.txt": "d92b419bd0c57c7951d558039e1414acca4ec2abc47e5a9d2f2389b1401adf7c",
|
||||
"outputs/ispc-l4-delivery-20261025/status-final.txt": "638710d6205b634b8ab8f8cf625ee308fb76c4fe7c21608130d2b79afba60e0c",
|
||||
"outputs/ispc-l4-delivery-20261025/installed-protected.txt": "83231f0b6619828618188e7abb9337f6b5e92e4192f7e5a8bbf165f7d2b70a44",
|
||||
"outputs/ispc-l4-delivery-20261025/installed-public-delta.json": "7b944512bd87b5fa99e83e554247f1567c5629481b2dcdf668b411f118849aa2",
|
||||
"outputs/ispc-l4-delivery-20261025/operational-delta.txt": "e79fc1267ac6f06e0f2fe78a9ed00eb0cfc05b1ec1ab7bec92c61268e4acbd93",
|
||||
"outputs/ispc-l4-delivery-20261025/compact-dependencies.txt": "236ebb16564acf706dd434186b20013b9c035dd201f5f728ebcaf1e3813e2053"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
{
|
||||
"status": "prepared_for_Git_delivery_not_installed",
|
||||
"authorization": "Tempo25/10:preparar entrega do âmbito L4 trimestral habitual demonstrado em6af7092e;conferir apenas delta,preservarL3;sem declararL4integralmenteconcluído.",
|
||||
"installed_baseline": "fa32a2b9e127da9c4d764dd99ef58ee43b770aa6",
|
||||
"candidate": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"branch": "codex/ispc-l4-quarterly-qa-20261025",
|
||||
"changes": [
|
||||
"docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json",
|
||||
"docs/investmoz/simulations/ispc-l4-quarterly-qa-20261025.json",
|
||||
"docs/investmoz/simulations/ispc-l4a-eligibility-qa-20261025.json",
|
||||
"docs/investmoz/simulations/l3-additional-delivery-20261025.json",
|
||||
"docs/investmoz/simulations/l3-e-additional-coverage-qa-20261025.json",
|
||||
"docs/investmoz/unification/traceability-and-manual.md",
|
||||
"docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
|
||||
"resources/js/components/simulation-form.tsx",
|
||||
"resources/js/components/simulation-ispc-eligibility.tsx",
|
||||
"resources/js/components/simulation-ispc-quarter.tsx",
|
||||
"resources/js/components/simulation-ispc-withholding.tsx",
|
||||
"resources/js/lib/ispc-eligibility.ts",
|
||||
"resources/js/lib/ispc-quarter.ts",
|
||||
"resources/js/lib/ispc-withholding.ts",
|
||||
"resources/js/lib/project-dossier-report.ts",
|
||||
"resources/js/lib/project-dossier.ts",
|
||||
"tests/Feature/IspcQuarterDraftTest.php",
|
||||
"tests/frontend/fixtures/ispc-quarter-preserved.json",
|
||||
"tests/frontend/ispc-eligibility.test.mjs",
|
||||
"tests/frontend/ispc-quarter.test.mjs",
|
||||
"tests/frontend/ispc-withholding.test.mjs"
|
||||
],
|
||||
"runtime_delta": [
|
||||
{
|
||||
"path": "resources/js/components/simulation-form.tsx",
|
||||
"sha256_LF": "606e6e29f92323450433e8411b2064ba5f55b795a3376794ad1eb30729841c3c"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/simulation-ispc-eligibility.tsx",
|
||||
"sha256_LF": "f48dd4a8f87a994eda0447f81a463589905e5873b2e1044debac9d10825a880c"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/simulation-ispc-quarter.tsx",
|
||||
"sha256_LF": "0b950091e9204b2b20823694ea16dc94e4863914080542253966367a5580d62d"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/simulation-ispc-withholding.tsx",
|
||||
"sha256_LF": "e688a8d6a2e1e20c1e10a85d33152d58a5c14867672cf4ffb834980ec1732f08"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/ispc-eligibility.ts",
|
||||
"sha256_LF": "d8c3ab8b2687af905d3048db192b690fa6d49ab693ff58f2e8f57f76dfb4a1fb"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/ispc-quarter.ts",
|
||||
"sha256_LF": "f66bd52e9cdfe1ddaa6b00a931465862b3d890b9be216b7a056740f027f24129"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/ispc-withholding.ts",
|
||||
"sha256_LF": "6c31b240299edd3262efa28f7f3d5aee21123f94e30ef4ab90e26c35777cb6e4"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/project-dossier.ts",
|
||||
"sha256_LF": "a24e0484f558580c14c4a261de66db8d245ffccf9c6fe17b6d639e9e2cbbe3c2"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/project-dossier-report.ts",
|
||||
"sha256_LF": "0db9f5fcb2929fd8340894bd13b74ee7c400af25e483ddfe5651002fd7160f49"
|
||||
}
|
||||
],
|
||||
"existing_integrations_additions_only": {
|
||||
"resources/js/components/simulation-form.tsx": 6,
|
||||
"resources/js/lib/project-dossier.ts": 6,
|
||||
"resources/js/lib/project-dossier-report.ts": 1
|
||||
},
|
||||
"L3_runtime_preserved_unchanged": [
|
||||
"resources/js/components/acquisition-beverage-restitution.tsx",
|
||||
"resources/js/components/fiscal-calculation-gaps.tsx",
|
||||
"resources/js/components/fiscal-l3-preparation.tsx",
|
||||
"resources/js/components/simulation-acquisitions.tsx",
|
||||
"resources/js/components/simulation-operations.tsx",
|
||||
"resources/js/lib/iva-acquisition-bc.ts",
|
||||
"resources/js/lib/iva-beverage-allocation.ts",
|
||||
"resources/js/lib/iva-beverage-restitution.ts",
|
||||
"resources/js/lib/iva-calculation-gaps.ts",
|
||||
"resources/js/lib/iva-operations.ts",
|
||||
"resources/js/lib/l3-form-preparation.ts"
|
||||
],
|
||||
"L3_documentary_originals_preserved": 17,
|
||||
"documentary_delta": [
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/BR-16-I-Serie-2008-04-16.pdf",
|
||||
"sha256": "fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/BR-248-I-Suplemento-1-2025.pdf",
|
||||
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/BR-89-I-Serie-2017-06-08.pdf",
|
||||
"sha256": "11f0cab15eb6e51010952203ffa24fdc533a24fd642e19525fe4f46b17a1f5e5"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/BR-95-I-Suplemento-2025-05-21.pdf",
|
||||
"sha256": "e0e682e2c027c3cd9c1d46a73a3f48bc5f6bdbf05667fd97314d7a3f6a456358"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Decreto-22-2021-BR70.pdf",
|
||||
"sha256": "a8fac38dcfe08f9c7a8e46254cff10e1ec82fc4f7a8d88ad6e4fe7d91ffc21a5"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Decreto-27-2026-BR123.pdf",
|
||||
"sha256": "26249c3fa58f149c81de36e99d1aac2819a4771379d9e8833159660dabf539bb"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Decreto-29-1998-BR22-5-suplemento.pdf",
|
||||
"sha256": "6242cbe675faadf085f43f95ec8c7e8868aafe67c3b2ab2bb010992288f954f8"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Decreto-3-2011-BR-11-I-mineiros.pdf",
|
||||
"sha256": "5bca1133666ba9c69870b89823420175b7b5b8d0eccb667ecdf5c29716f3b6be"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Decreto-37-2023-BR124.pdf",
|
||||
"sha256": "7e680b191481faa0e2e9057fa5053587eb2a29d1d8c819e0eb9f02814255a04c"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf",
|
||||
"sha256": "b90dc7c14ecea2e378c4a62e8e5a47d54a5644c457baeb8b8053b92fd2a5710b"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Lei-14-1992-BR42.pdf",
|
||||
"sha256": "089fff7423e99a70a230a73ca379e4ea3a91d124703f91016e57ff82794b9e56"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Lei-17-2022-BR251-exemplar-MCNet5.pdf",
|
||||
"sha256": "14eb34f0af91a8f5cb1770c7b52d6ff839c6818302d1ed96d79afbfb9fe999d4"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Lei-17-2022-IPP-BR250-Parlamento.pdf",
|
||||
"sha256": "978cd22403ffd336a631704ecd7a5bbd814a427c815b9acdaca5515591dd9e2a"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Lei-2-2011-BR-1-4-suplemento-MCNet.pdf",
|
||||
"sha256": "a7543a68cc3287a46a83c39a04821b40f649dd06c4e9b5dc51ff0b47fba08899"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf",
|
||||
"sha256": "ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Lei-4-1994-BR37-3-suplemento.pdf",
|
||||
"sha256": "210df9db8980d0a5690cd187a6eeec7e37a900d743e39062c16f4e0d12215c7e"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Lei-7-1991-Partidos-IFES.pdf",
|
||||
"sha256": "6b783a0bb3330688ef10660a58060d9724aa2da6a873d234fe902f0eaaeda42d"
|
||||
}
|
||||
],
|
||||
"schema_runtime_PHP_configuration_dependencies_delta": false,
|
||||
"P001_experimental_changes_excluded": true,
|
||||
"accepted_QA_proofs_reused": {
|
||||
"prior_proofs": "L4-A12testes eL1/L2/L3/P001não repetidos;inventário eaceitações reutilizados",
|
||||
"new_frontend": "13distinctpassed:10quarter/3withholding;frontend-final.txt",
|
||||
"private_API": "1test/21assertions passed;loopback55441/simulator_fiscal_delta_20261021_test,DatabaseTransactions rollback;otheraccount404;api.txt",
|
||||
"UI": "Componentesreais:avaliar/opçãoguardada→calcular→guardarficheiroQA→recuperar→relatório;persistênciaprivadaconferidaemAPIseparadamente;ui-recovered-report.txt/png",
|
||||
"report_fix": "GrupoISPCmapeadoàsecçãofinanceira;testeactualdoexportadorincluiimposto/opção;ausênciaidentificadanademonstraçãoecorrigidaantesdeconcluir.",
|
||||
"types": "exit0;types-final.txt",
|
||||
"build": "exit0;build-final.txt",
|
||||
"inventory": "outputs/ispc-l4b-20261025/preservation-before-tests.json;reutiliza8143ficheirosinventariadoseL1fechados"
|
||||
},
|
||||
"accepted_proof_hashes_verified": {
|
||||
"outputs/ispc-l4b-20261025/frontend-final.txt": "2b0ec25bb83505ec9ce8ace02e0860faeeac4ce233083859f9a502069944f49f",
|
||||
"outputs/ispc-l4b-20261025/api.txt": "2a770d07561a1475973795d5bac01ce5d94ee8a91be1d57d9b0702c621b8284d",
|
||||
"outputs/ispc-l4b-20261025/ui-recovered-report.txt": "234bae0f17ef84dd9928197bfa38ad4a8a05c2dae18051e1ea9bd88852bb84fa",
|
||||
"outputs/ispc-l4b-20261025/build-final.txt": "9683e783fd6e457c89a747d4a1a6deb10ba00a034f7891cf69e4c1a0cb1f0073",
|
||||
"outputs/ispc-l4b-20261025/types-final.txt": "5346ac8a6566c1c38cb0bbda6a029579df58cd78af78e2c4845d9c82b5d1d76c",
|
||||
"tests/frontend/fixtures/ispc-quarter-preserved.json": "30266584de9cacb0e4d771a9cc9c38838b62a3472583792e13effcd094247c39"
|
||||
},
|
||||
"compatible_build_and_types": "Already passed on identical runtime hashes;proofs reused,no rerun.",
|
||||
"compiled_markers": {
|
||||
"ISPC — enquadramento e opção": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"ISPC — apuramento trimestral": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"ISPC — retenção em aquisições ocasionais": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Calcular e guardar apuramento trimestral ISPC": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Conferir e guardar retenção ISPC": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Repartição do IVA documental em MZN": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Reconciliação da dedução anterior": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Cálculos indeterminados e condições para resolver": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Preparação dos quadros L3 e formalidades": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Calcular e guardar restituição E": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Calcular e guardar acompanhamento D": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
],
|
||||
"Calcular e guardar substituições C": [
|
||||
"assets/services-tj9wihZS.js"
|
||||
]
|
||||
},
|
||||
"public_baseline_readonly": {
|
||||
"commit": "fa32a2b9e127da9c4d764dd99ef58ee43b770aa6",
|
||||
"branch": "main",
|
||||
"built_at": "2026-10-25T15:12:37.806036+00:00",
|
||||
"manifest_sha256": "cddac25dc66b3e555f2380cf3a9d4b2da6501cd03927c183c832a05b0ee5c993"
|
||||
},
|
||||
"examples": {
|
||||
"context": "opportunity:375;SYNTHETIC QA",
|
||||
"option": "documentada2026-01-01",
|
||||
"quarter": "2026/T2",
|
||||
"prior_turnover_MZN": "100000",
|
||||
"quarter_base_MZN": "200000",
|
||||
"rate_percent": 12,
|
||||
"tax_MZN": "24000",
|
||||
"payment_MZN": "24000",
|
||||
"declaration": "mantida",
|
||||
"other_fiscal_books": "exactamente preservados"
|
||||
},
|
||||
"remaining_L4_specific": [
|
||||
"Operações ocasionais próprias3(3–4)enquanto sujeito passivo,distintas da retenção do adquirente;mantidas indeterminadas no percurso específico.",
|
||||
"Aquisições acima limiteglobal2.500.000:correspondência do tratamento próprio;nãoliquidar5%porpresunção.",
|
||||
"Formulário/modelo,instruções eprocedimentos da regulamentação2026;relatório depreparação não édeclaraçãosubmetida. Aviso primário do Governo confirma aprovação de novo regulamento,mas não contém articulado/modelos: https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/ . Sem impor segunda validação jurídica ou repetir pesquisa sem nova evidência."
|
||||
],
|
||||
"preserved_amounts_L3": {
|
||||
"documentary_VAT_MZN": 16,
|
||||
"deduction_this_route_MZN": 0,
|
||||
"recognised_MZN": 12,
|
||||
"received_MZN": 7,
|
||||
"awaiting_receipt_MZN": 5
|
||||
},
|
||||
"residuals": "P001/P058awaitdefinedclarificationswithoutblocking;P084/P085closed;L3residualsremaininexistingmatrix;statistics/AIdeferred.",
|
||||
"execution_remaining": [
|
||||
"Fresh authenticated destination/schema/data/configuration/permissions and disk comparison.",
|
||||
"Fresh backup and verified isolated restore,without modifying active database.",
|
||||
"Fast-forward integration via existing Git process only after guards pass;verify servedrevision,manifest,compiled L4/L3 markers and protected delta with equivalent identities/context."
|
||||
],
|
||||
"backup_restore": "Not executed in preparation;prior L3 receipt preserved,not represented as fresh L4 backup.",
|
||||
"main_changed": false,
|
||||
"active_changes": false,
|
||||
"Git_preparation": {
|
||||
"feature_ref": "refs/heads/codex/ispc-l4-quarterly-qa-20261025",
|
||||
"remote_verified": {
|
||||
"refs/heads/codex/ispc-l4-quarterly-qa-20261025": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"refs/heads/main": "fa32a2b9e127da9c4d764dd99ef58ee43b770aa6"
|
||||
},
|
||||
"bundle": "outputs/ispc-l4-delivery-20261025/candidate.bundle",
|
||||
"bundle_sha256": "2d1b690ffbca0cdcc2365d2f5c8cf9f4d34a3e64f9cb5bd4fa2ce23835143c4d",
|
||||
"bundle_verified": true,
|
||||
"main_pushed": false
|
||||
},
|
||||
"execution_scripts": {
|
||||
"outputs/ispc-l4-delivery-20261025/backup.py": "f82ee71b83527e2e3b431f88aa24deb0433308f4c15a2873779068a21785c99d",
|
||||
"outputs/ispc-l4-delivery-20261025/bridge.py": "b12bb51e68c0c3270ac9d127d20aaf69c04745837fb06788e9e1d37a623e3487",
|
||||
"outputs/ispc-l4-delivery-20261025/installer-status.py": "47cf71d02bbbac76bdf8edf87ab9b69b88d6c610d4b1755e1da69060af8b02cf",
|
||||
"outputs/ispc-l4-delivery-20261025/integrate-after-guards.py": "b2146ae221af4fa5f26f3a16ba7de22eb3d35fed2ff9b606f49065b0a9524171",
|
||||
"outputs/ispc-l4-delivery-20261025/preflight.py": "d22ae1370f261cce91201830f39833d03019df5110be8aa5a9eccdfd639de55c",
|
||||
"outputs/ispc-l4-delivery-20261025/reconcile-operational-delta.py": "d2e2ab7093594ab667b03f76ef7620120d7fd580e22edeb335b3c5c208493cd0",
|
||||
"outputs/ispc-l4-delivery-20261025/restore.py": "51c03e0e590f3ad8d795334e67140f6de7c594fe52fb5ee9098b2da8bc508a3b",
|
||||
"outputs/ispc-l4-delivery-20261025/verify-protected.py": "5acdb275e6bd5de5237000f8a8a27234f9d00be4bf1cd1cf71ecf24bc0a7f415",
|
||||
"outputs/ispc-l4-delivery-20261025/verify-public.py": "e31052548421a2095d23435a716c34086542992fda3ede04f6ae3cd1d24f291b"
|
||||
},
|
||||
"execution_script_checks": "Python syntax checked;not executed against active target. Authenticated baseline,fresh backup/isolatedrestore andtimestamp guards prepared;disk comparison pending at execution.",
|
||||
"guard_origins": {
|
||||
"base_and_scope": "DecisãoTempo:preservarL3fa32 eentregar apenas âmbitoQA6af.",
|
||||
"disk_5_GiB": "Requisito técnico do processo de entrega já existente;conferido no código emexecuçãoL3,sem criar regra organizacional.",
|
||||
"backup_freshness_30_minutes": "Escolha técnica do script de integração;expiração exige actualizar preflight/backup-restauro,sem nova aprovação organizacional."
|
||||
},
|
||||
"superseded_by": "ispc-l4-delivery-20261025.json",
|
||||
"installation_followup": "6af7092e83b9b3932712aa7a9f527393fcec4da8"
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"status": "installed_scoped_habitual_quarterly_ISPC",
|
||||
"matrix_anchor": "AUD-SIM-INT02",
|
||||
"scope": "Actividade habitual ISPC2026/MZN, enquadramento/opção, cálculo trimestral e retenção12(3–4) delimitada. Não é encerramento de operações ocasionais próprias ou formalidades sem correspondência de modelo.",
|
||||
"source": {
|
||||
"file": "docs/investmoz/simulations/source-archive/BR-248-I-Suplemento-1-2025.pdf",
|
||||
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
|
||||
"citation": "Lei9/2025,republicaçãoISPC,arts8–12;PDF11/p2180(11),enquadramento2–5PDF10",
|
||||
"application": "8(1)a selecciona taxa3/4/5 pelo volume anual2026 declarado e9 aplica-a à base trimestral; não repartir limites anuais por4 nem copiar acesso2025. Serviços12/15; cumulação usa taxa maior8(2).20%somenteparcela acima limite anual4.000.000 com acumulado anterior; cálculos racionais exactos."
|
||||
},
|
||||
"new_implementation": [
|
||||
"Livro trimestral por contexto; receitas de cada trimestre e suporte explícitos,volume global do sujeito passivo confirmado; previsão anual distinta de ano encerrado reconciliado.",
|
||||
"Elegibilidade actual e opção documentada reutilizadas; preparação apenas simula; opção posterior ao trimestre não retroage automaticamente.",
|
||||
"Imposto/prestação/obrigação declarativa separados; inferior500 dispensa pagamento, não declaração;igual500 mantém pagamento. Não acumular dispensas nem deduzir custos/IVA.",
|
||||
"Retenção5%separada para aquisições ocasionais a pessoa singular não inscrita3(3–4),ligadasàactividade,limiteglobalanual2.500.000;acima limiteindeterminado,semtruncar. Sem crédito contraimpostopróprio,mínimo500não aplicadoàretenção.",
|
||||
"Gravação/recuperação mantém outros livros e históricos;alterações conservam resultado congelado até nova versão;relatório financeiro inclui grupoISPC."
|
||||
],
|
||||
"verification": {
|
||||
"prior_proofs": "L4-A12testes eL1/L2/L3/P001não repetidos;inventário eaceitações reutilizados",
|
||||
"new_frontend": "13distinctpassed:10quarter/3withholding;frontend-final.txt",
|
||||
"private_API": "1test/21assertions passed;loopback55441/simulator_fiscal_delta_20261021_test,DatabaseTransactions rollback;otheraccount404;api.txt",
|
||||
"UI": "Componentesreais:avaliar/opçãoguardada→calcular→guardarficheiroQA→recuperar→relatório;persistênciaprivadaconferidaemAPIseparadamente;ui-recovered-report.txt/png",
|
||||
"report_fix": "GrupoISPCmapeadoàsecçãofinanceira;testeactualdoexportadorincluiimposto/opção;ausênciaidentificadanademonstraçãoecorrigidaantesdeconcluir.",
|
||||
"types": "exit0;types-final.txt",
|
||||
"build": "exit0;build-final.txt",
|
||||
"inventory": "outputs/ispc-l4b-20261025/preservation-before-tests.json;reutiliza8143ficheirosinventariadoseL1fechados"
|
||||
},
|
||||
"example": {
|
||||
"context": "opportunity:375;SYNTHETIC QA",
|
||||
"option": "documentada2026-01-01",
|
||||
"quarter": "2026/T2",
|
||||
"prior_turnover_MZN": "100000",
|
||||
"quarter_base_MZN": "200000",
|
||||
"rate_percent": 12,
|
||||
"tax_MZN": "24000",
|
||||
"payment_MZN": "24000",
|
||||
"declaration": "mantida",
|
||||
"other_fiscal_books": "exactamente preservados"
|
||||
},
|
||||
"remaining_specific": [
|
||||
"Operações ocasionais próprias3(3–4)enquanto sujeito passivo,distintas da retenção do adquirente;mantidas indeterminadas no percurso específico.",
|
||||
"Aquisições acima limiteglobal2.500.000:correspondência do tratamento próprio;nãoliquidar5%porpresunção.",
|
||||
"Formulário/modelo,instruções eprocedimentos da regulamentação2026;relatório depreparação não édeclaraçãosubmetida. Aviso primário do Governo confirma aprovação de novo regulamento,mas não contém articulado/modelos: https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/ . Sem impor segunda validação jurídica ou repetir pesquisa sem nova evidência."
|
||||
],
|
||||
"active_changes": false,
|
||||
"schema_changes": false,
|
||||
"rules_published_or_activated": 0,
|
||||
"residuals": "P001/P058nãointerrompem;P084/P085fechados;L3residuaisapenasbloqueiamcasosdependentes;estatísticas/IAadiadas.",
|
||||
"runtime_hashes": {
|
||||
"resources/js/lib/ispc-quarter.ts": "f66bd52e9cdfe1ddaa6b00a931465862b3d890b9be216b7a056740f027f24129",
|
||||
"resources/js/lib/ispc-withholding.ts": "6c31b240299edd3262efa28f7f3d5aee21123f94e30ef4ab90e26c35777cb6e4",
|
||||
"resources/js/components/simulation-ispc-quarter.tsx": "0b950091e9204b2b20823694ea16dc94e4863914080542253966367a5580d62d",
|
||||
"resources/js/components/simulation-ispc-withholding.tsx": "e688a8d6a2e1e20c1e10a85d33152d58a5c14867672cf4ffb834980ec1732f08",
|
||||
"resources/js/lib/project-dossier-report.ts": "0db9f5fcb2929fd8340894bd13b74ee7c400af25e483ddfe5651002fd7160f49"
|
||||
},
|
||||
"proof_hashes": {
|
||||
"outputs/ispc-l4b-20261025/frontend-final.txt": "2b0ec25bb83505ec9ce8ace02e0860faeeac4ce233083859f9a502069944f49f",
|
||||
"outputs/ispc-l4b-20261025/api.txt": "2a770d07561a1475973795d5bac01ce5d94ee8a91be1d57d9b0702c621b8284d",
|
||||
"outputs/ispc-l4b-20261025/ui-recovered-report.txt": "234bae0f17ef84dd9928197bfa38ad4a8a05c2dae18051e1ea9bd88852bb84fa",
|
||||
"outputs/ispc-l4b-20261025/build-final.txt": "9683e783fd6e457c89a747d4a1a6deb10ba00a034f7891cf69e4c1a0cb1f0073",
|
||||
"outputs/ispc-l4b-20261025/types-final.txt": "5346ac8a6566c1c38cb0bbda6a029579df58cd78af78e2c4845d9c82b5d1d76c",
|
||||
"tests/frontend/fixtures/ispc-quarter-preserved.json": "30266584de9cacb0e4d771a9cc9c38838b62a3472583792e13effcd094247c39"
|
||||
},
|
||||
"compatible_QA_preparation": {
|
||||
"branch": "codex/ispc-l4-quarterly-qa-20261025",
|
||||
"path": "C:\\laragon\\www\\portal-investidor-ispc-qa-20261025",
|
||||
"base": "fa32a2b9",
|
||||
"runtime_files": [
|
||||
"resources/js/components/simulation-form.tsx",
|
||||
"resources/js/components/simulation-ispc-eligibility.tsx",
|
||||
"resources/js/components/simulation-ispc-quarter.tsx",
|
||||
"resources/js/components/simulation-ispc-withholding.tsx",
|
||||
"resources/js/lib/ispc-eligibility.ts",
|
||||
"resources/js/lib/ispc-quarter.ts",
|
||||
"resources/js/lib/ispc-withholding.ts",
|
||||
"resources/js/lib/project-dossier.ts",
|
||||
"resources/js/lib/project-dossier-report.ts"
|
||||
],
|
||||
"runtime_hashes_match_accepted_QA": true,
|
||||
"P001_runtime": "fa32a2b9 unchanged; experimental d1551dab runtime excluded",
|
||||
"compatible_build": "exit0;outputs/ispc-l4b-20261025/compatible-build.txt",
|
||||
"compatible_types": "exit0;outputs/ispc-l4b-20261025/compatible-types.txt",
|
||||
"functional_proofs": "13frontend/API1-21/UIreport reused; no new functional round",
|
||||
"installed": true,
|
||||
"functional_source_commits": [
|
||||
"479b6681",
|
||||
"1b7ccfee"
|
||||
],
|
||||
"candidate_commit": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"functional_candidate_commit": "49b448eb47df9ded3f906996e36c1d5ebd855741",
|
||||
"installed_at_preparation": false,
|
||||
"installed_commit": "6af7092e83b9b3932712aa7a9f527393fcec4da8"
|
||||
},
|
||||
"QA_status_before_delivery": "QA_demonstrated_eligibility_option_quarter_calculation_private_recovery_and_report_not_installed",
|
||||
"installation_followup_20261025": {
|
||||
"status": "installed_scoped_habitual_quarterly",
|
||||
"commit": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"receipt": "ispc-l4-delivery-20261025.json",
|
||||
"QA_proofs": "preserved;no manual repetition",
|
||||
"ISPC_integrally_completed": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"status": "QA_partial_eligibility_and_separate_option_not_installed",
|
||||
"matrix_anchor": "AUD-SIM-INT02",
|
||||
"authorization": "Tempo25/10: preparar entrega compatível L3, reconciliar saldo e avançar L4 ISPC; P001 residual não bloqueia trabalho independente.",
|
||||
"source": {
|
||||
"path": "docs/investmoz/simulations/source-archive/BR-248-I-Suplemento-1-2025.pdf",
|
||||
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
|
||||
"law": "Lei9/2025, republicação ISPC; arts2–5,9-A/13; PDF10–11; vigência2026art6/PDF9",
|
||||
"acceptance": "Fonte oficial aceite pela Tempo; sem segunda validação de proveniência ou aprovação genérica Fiscalista."
|
||||
},
|
||||
"scope": [
|
||||
"Condições preliminares de acesso habitual em2026/MZN; pessoa,actividade nacional,volume anterior2025 ou previsão de início2026,contabilidade obrigatória e exclusões3(2).",
|
||||
"Limite4.000.000 inclusivo; desconhecido permanece indeterminado e não vira zero; operação ocasional tem percurso próprio ainda não implementado.",
|
||||
"Serviços liberais8(1)c:183dias versus184 à mesma entidade; não aplicar esta exclusão aos serviços8(1)b.",
|
||||
"Opção separada de elegibilidade; seleccionarISPC não adopta regime nem alteraIVA/IRPS/IRPC.",
|
||||
"Reutilização de perfil com diferenças e confirmação; nenhuma inferência de categoria,volume anual anterior ou opção a partir de vendas correntes.",
|
||||
"Livro por contexto no snapshot existente; preserva mapas fiscais,histórico e resultados congelados; relatório do dossier inclui enquadramento e opção."
|
||||
],
|
||||
"verification": {
|
||||
"inventory_before_tests": "outputs/ispc-l4a-20261025/preservation-before-tests.json",
|
||||
"new_frontend_tests": 12,
|
||||
"frontend_result": "12 passed/0failed; outputs/ispc-l4a-20261025/frontend.txt",
|
||||
"types": "exit0;outputs/ispc-l4a-20261025/types.txt",
|
||||
"build": "exit0;outputs/ispc-l4a-20261025/build.txt",
|
||||
"persistence": "serialização/recuperação e preservação dos livros/históricos cobertas nos novos testes; transporte privado existente reutilizado,sem nova rondaAPI",
|
||||
"UI": "componente integrado;sem demonstração visual nova nesta passagem",
|
||||
"synthetic_fixture": "outputs/ispc-l4a-20261025/calculated.json; não importada no activo"
|
||||
},
|
||||
"remaining_L4": [
|
||||
"Apuramento trimestral e aplicação dos escalões3/4/5%,serviços12%,liberal15%,excesso20% e actividades mistas8; identificar correspondência exacta anual/trimestral antes do cálculo.",
|
||||
"Mínimo trimestral500 e obrigação declarativa; retenção5% nas aquisições ocasionais próprias,limite2.500.000 e modelo; sem reutilizar taxa deIVA.",
|
||||
"Percurso ocasional3(3–4),formalidades,modelos e prazos do regulamento aplicável; conferir apenas correspondências concretas necessárias,sem nova validação genérica.",
|
||||
"Demonstração visual integrada e gravação privada deste incremento antes de propor entregaL4."
|
||||
],
|
||||
"preserved": {
|
||||
"L1": "131entregues;P001residual;P058parcial com exclusões",
|
||||
"P084_P085": "fechados no âmbito operacional Tempo;sem pesquisa",
|
||||
"L3": "candidata0f4df8a4 preparada,não instalada;saldo próprio não encerraL3",
|
||||
"E_original": "16MZN documental/zero dedução nesta via/12 reconhecidos/7recebidos/5porreceber",
|
||||
"statistics_AI": "adiadas"
|
||||
},
|
||||
"active_changes": false,
|
||||
"schema_changes": false,
|
||||
"runtime_hashes": {
|
||||
"resources/js/lib/ispc-eligibility.ts": "1828cb2d696a6bd852078fc3f1cf90a6649c1f3a0df5bec0a72ac1d1f175b2eb",
|
||||
"resources/js/components/simulation-ispc-eligibility.tsx": "38ebe2b2fd340e269ba6fdeec96f0d39d0594892b9cb968ec094b2c165346b42",
|
||||
"resources/js/components/simulation-form.tsx": "5090175a3fee965795fb47ac7f673c5aafc3641c25d323be0be631178afae775",
|
||||
"resources/js/lib/project-dossier.ts": "de70672560162ef94f1e5429f70c2466a8347e73b2d800da013c7c50e480091c"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"status": "QA_grouped_compatible_delta_not_installed",
|
||||
"matrix_anchor": "AUD-SIM-INT02",
|
||||
"base_installed": "6af7092e83b9b3932712aa7a9f527393fcec4da8",
|
||||
"branch": "codex/ispc-residuals-qa-20261025",
|
||||
"scope": "Operações ocasionais próprias3(3–4),aquisições acima do limite12(3)comcorrespondênciapositivaIRPS57(2k),preparação/transporte de camposB–Geral e histórico externo;semfechointegralISPC.",
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/AT-Comunicado-9-2026-ISPC.pdf",
|
||||
"bytes": 308401,
|
||||
"sha256": "106f8a9ce9255c03549c70bf07fe999d32e6e55302d009b509862aa74ccb4973",
|
||||
"availability": "private source archive;not automatically published/imported into active library",
|
||||
"scan": "not claimed"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/AT-Modelo-B-Geral.pdf",
|
||||
"bytes": 319577,
|
||||
"sha256": "bd977c7c59d4d98befd92e2c566b1bf168cb1343bb519f51b41a3807f431fc57",
|
||||
"availability": "private source archive;not automatically published/imported into active library",
|
||||
"scan": "not claimed"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/AT-Modelo-30-ISPC.pdf",
|
||||
"bytes": 159748,
|
||||
"sha256": "6802edd06b6ff638a0b851a3ec7ecf69af8f07840faa47446becad67d35d445c",
|
||||
"availability": "private source archive;not automatically published/imported into active library",
|
||||
"scan": "not claimed"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/simulations/source-archive/Lei-19-2017-IRPS-BR-OCAM.pdf",
|
||||
"bytes": 1894997,
|
||||
"sha256": "c3a0186cb9ad090d7a9533006295dc9bb6a62ad0d6150173cbd49dba4ee4b52d",
|
||||
"availability": "private source archive;not automatically published/imported into active library",
|
||||
"scan": "not claimed"
|
||||
}
|
||||
],
|
||||
"official_source_inventory": {
|
||||
"local_reused": "outputs/ispc-l4a-20261025/preservation-before-tests.json",
|
||||
"AT_forms": "39documents;at-forms.json",
|
||||
"AT_legislation": "189documents;at-legislation.json",
|
||||
"AT_communications": "4documents;at-communications.xml",
|
||||
"provenance": "AT public document APIs reached through links/configuration of the official website;BR2017 republication hostedOCAM;no generic second authenticity validation",
|
||||
"AT_comunicado": {
|
||||
"url": "https://www.at.gov.mz/api/webdav?path=%2FINFORMA%C3%87%C3%95ES%2FCOMUNICADO%20DE%20IMPRENSA%2F07-10-2026%20-%20COMUNICADO%20N%C2%BA9_2026%20-%20CUMPRIMENTO%20DAS%20OBRIGA%C3%87%C3%95ES%20DECLARATIVAS%20E%20DE%20PAGAMENTO%20DO%20ISPC.pdf",
|
||||
"listing": "https://www.at.gov.mz/informacoes/comunicado-de-imprensa",
|
||||
"file": "C:\\laragon\\www\\portal-investidor-ispc-residuals-qa-20261025\\docs\\investmoz\\simulations\\source-archive\\AT-Comunicado-9-2026-ISPC.pdf",
|
||||
"sha256": "106f8a9ce9255c03549c70bf07fe999d32e6e55302d009b509862aa74ccb4973",
|
||||
"bytes": 308401,
|
||||
"origin": "AT official website; public document linked from communications page",
|
||||
"automatic_rule_activation": false
|
||||
},
|
||||
"AT_forms_selected": [
|
||||
{
|
||||
"id": "ed263a2ecdd81846af4cdc8f75fc76c1",
|
||||
"title": "Modelo 30 ISPC IMPOSTO SIMPLIFICADO PARA PEQUENOS CONTRIBUINTES",
|
||||
"description": "Documentação oficial sincronizada em tempo real.",
|
||||
"size": "156 KB",
|
||||
"date": "06/01/2026",
|
||||
"url": "/api/webdav?path=%2FFORMULARIOS%2FIMPOSTOS%20FISCAIS%2FIRPC%2FModelo%2030%20ISPC%20IMPOSTO%20SIMPLIFICADO%20PARA%20PEQUENOS%20CONTRIBUINTES.pdf",
|
||||
"downloadUrl": "/api/webdav?path=%2FFORMULARIOS%2FIMPOSTOS%20FISCAIS%2FIRPC%2FModelo%2030%20ISPC%20IMPOSTO%20SIMPLIFICADO%20PARA%20PEQUENOS%20CONTRIBUINTES.pdf",
|
||||
"viewUrl": "/api/webdav?path=%2FFORMULARIOS%2FIMPOSTOS%20FISCAIS%2FIRPC%2FModelo%2030%20ISPC%20IMPOSTO%20SIMPLIFICADO%20PARA%20PEQUENOS%20CONTRIBUINTES.pdf",
|
||||
"file": "C:\\laragon\\www\\portal-investidor-ispc-residuals-qa-20261025\\docs\\investmoz\\simulations\\source-archive\\AT-Modelo-30-ISPC.pdf",
|
||||
"origin": "AT official forms directory",
|
||||
"sha256": "6802edd06b6ff638a0b851a3ec7ecf69af8f07840faa47446becad67d35d445c",
|
||||
"bytes": 159748
|
||||
},
|
||||
{
|
||||
"id": "c37f0377df612928865f37cfbfc9dabd",
|
||||
"title": "MODELO B GERAL",
|
||||
"description": "Documentação oficial sincronizada em tempo real.",
|
||||
"size": "312 KB",
|
||||
"date": "28/10/2025",
|
||||
"url": "/api/webdav?path=%2FFORMULARIOS%2FIMPOSTOS%20FISCAIS%2FIVA%2FMODELO%20B%20GERAL.pdf",
|
||||
"downloadUrl": "/api/webdav?path=%2FFORMULARIOS%2FIMPOSTOS%20FISCAIS%2FIVA%2FMODELO%20B%20GERAL.pdf",
|
||||
"viewUrl": "/api/webdav?path=%2FFORMULARIOS%2FIMPOSTOS%20FISCAIS%2FIVA%2FMODELO%20B%20GERAL.pdf",
|
||||
"file": "C:\\laragon\\www\\portal-investidor-ispc-residuals-qa-20261025\\docs\\investmoz\\simulations\\source-archive\\AT-Modelo-B-Geral.pdf",
|
||||
"origin": "AT official forms directory",
|
||||
"sha256": "bd977c7c59d4d98befd92e2c566b1bf168cb1343bb519f51b41a3807f431fc57",
|
||||
"bytes": 319577
|
||||
}
|
||||
],
|
||||
"IRPS_Bulletin_URL": "https://ocam.org.mz/wp-content/uploads/2024/06/Lei-n-19_2017-de-28-de-Dezembro-IRPS-2-1.pdf"
|
||||
},
|
||||
"implemented_calculations": [
|
||||
{
|
||||
"case": "Operação ocasional própria",
|
||||
"positive": "Pessoa singular,semactividadehabitual,actoimprevisível/nãoreiterado,nacional,nasfamílias3(4);IRPS8(3h,4)/9da republicação19/2017;ISPC4/5/8/9/10-A. Cenário100000MZN/3%=3000;opção/factosefectivosdistintosdeprevisão.",
|
||||
"anti_duplication": "Retenção liberatória5%documentadareconciliadacomabase não gera segunda liquidação própria;divergência ficaindeterminada,nãohácréditopresumido."
|
||||
},
|
||||
{
|
||||
"case": "Aquisição que atravessa2.500.000MZN",
|
||||
"positive": "Acumulado2400000+aquisição200000:parcela100000atélimite/retencão5000;excedente100000separado. SóactoIRPSqualificado57(2k),semopçãoISPCaplicável,comrendimentoilíquidoidentificado permite20000noexcedente;total25000de retenções,não imposto próprio do adquirente.",
|
||||
"limits": "Sem enquadramento/base/documentos,parcela excedente e total permanecem indeterminados. Fornecedor com opçãoISPC não recebeIRPS20automaticamente;factura/liquidação própria são âmbito separado. Não se usa20%do excesso de vendasISPC para compras."
|
||||
}
|
||||
],
|
||||
"implemented_formal_preparation": [
|
||||
"ATcomunicado9/2026visualizado:ModeloB–Geraltransitório,enquanto sistemaéajustado;mesesJaneiro/Abril/Julho/Outubro;nenhumdiadomêsinventado.",
|
||||
"OriginalB–Geral2pconferido:quadros1/2/3/5/6/8/10;linha1base01/montante05,linha2base02/montante06,subtotal09;numeração própria doquadro10nãosemisturacomquadro8.",
|
||||
"HTMLdepreparação,relatóriorecuperado efontes/originaispreservados;semrecriarformuláriooficial,assinatura,recibosouacçãodafuncionário. RetençõesISPC5/IRPS20descritas separadamente.",
|
||||
"Modelo30oficialdisponívelaindaexibe75.000/3%antigos:preservadoparaconsulta,nãotransportadopara2026.",
|
||||
"Estadosdepreparação/submissão/recepção/pagamentodeclaradoscomreferência/dataehistórico;sem efectuaressasacçõesoucertificarAT."
|
||||
],
|
||||
"remaining_calculations": [
|
||||
{
|
||||
"item": "Juros compensatórios",
|
||||
"state": "montante documentado pode ser conservado;fórmula ainda não implementada",
|
||||
"needed": "regra/taxa/base/período/dias efectivamente aplicáveis,no documento/instrução correspondente;sem zero ou taxa presumidos"
|
||||
},
|
||||
{
|
||||
"item": "Outros enquadramentos da parcela excedente",
|
||||
"state": "sem fórmula genérica;casos diferentes do acto isolado qualificado não recebem taxa automática",
|
||||
"needed": "natureza,regime do fornecedor,opção aplicável,base e suporte concretos;encaminhar para o cálculo próprio quando for outro regime,não bloquear o caso positivo implementado"
|
||||
}
|
||||
],
|
||||
"external_formal_dependencies": [
|
||||
{
|
||||
"item": "Quadro2doB–Geral",
|
||||
"needed": "correspondência concreta do mês/ano a inscrever para o trimestre;comunicado fornece meses de pagamento,que não são copiados automaticamente para período"
|
||||
},
|
||||
{
|
||||
"item": "Dispensa inferior500MZN noB–Geral",
|
||||
"needed": "Representação declarativa do imposto apurado com prestação dispensada10-A;sem preencher montanteapurado comozero porpresunção. O cálculo imposto/prestação jáestá implementado."
|
||||
},
|
||||
{
|
||||
"item": "Entrega de retenções",
|
||||
"needed": "modelo/código/instrução próprios12(3)ISPCe57(2k)IRPS;B–Geraldeapoio nãoéapresentadocomocorrespondênciacompletadestasretenções"
|
||||
},
|
||||
{
|
||||
"item": "Regulamentação2026/instruções de envio de facturas",
|
||||
"needed": "texto/exemplar/instrução concreta quando disponibilizado;comunicado transitório já implementado,não se mantém pesquisa genérica como aprovação adicional"
|
||||
},
|
||||
{
|
||||
"item": "Submissão e pagamento",
|
||||
"needed": "actos externos e comprovativos reais;nenhuma operaçãoATfoiexecutadaemQA"
|
||||
}
|
||||
],
|
||||
"operation_data_missing_in_synthetic_preparation": "Identidade/NUIT/área fiscal/domicílio,referência de período ejuros são dados para formalizar a operação,não nova condição de acesso ao cálculo.",
|
||||
"verification": {
|
||||
"preservation_before_tests": "outputs/ispc-residuals-20261025/preservation-before-tests.json",
|
||||
"new_frontend_distinct": 19,
|
||||
"frontend_proofs": [
|
||||
"frontend-final.txt:14passed",
|
||||
"directed-final.txt:1additionalpass",
|
||||
"directed-retention-form.txt:1additionalpass",
|
||||
"directed-known-exclusions.txt:1additionalpass",
|
||||
"directed-dispensation-form.txt:1additionalpass",
|
||||
"directed-liberatory-history.txt:1additionalpass"
|
||||
],
|
||||
"API": "1test/22assertionspassed;QAloopback55441/simulator_fiscal_delta_20261021_test;DatabaseTransactionsrollback;APP_BASE_PATHcandidateexplicit",
|
||||
"UI": "Vendedor376/adquirente375separados;3000/5000/20000/25000;guardar→recuperar→relatório;B–Geralpreparadodescarregado;visibilidadeapenasdascondiçõesafectadasverificada",
|
||||
"UI_proofs": [
|
||||
"ui-own-report.txt",
|
||||
"ui-acquisition-report.txt",
|
||||
"ui-occasional-applicability.txt",
|
||||
"ui-regular-applicability.txt",
|
||||
"preparation.html"
|
||||
],
|
||||
"types": "exit0;types-final.txt",
|
||||
"build": "exit0;build-final.txt",
|
||||
"habitual_re_evaluation": "not repeated;arithmetic engines unchanged;24000snapshotpreservedexactly;onlyconditionalvisibilitydeltaexamined"
|
||||
},
|
||||
"runtime_delta": [
|
||||
{
|
||||
"path": "resources/js/lib/ispc-residuals.ts",
|
||||
"sha256_LF": "40d5751c06c7c155ab4b7a17ee2d1653da2fbbf075807fbb8561cdbe19820955"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/simulation-ispc-residuals.tsx",
|
||||
"sha256_LF": "a9219d6f41ccbd0ce45659d4f6aa0fa1c169a685840f5cb1e18daeca377d5e18"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/simulation-form.tsx",
|
||||
"sha256_LF": "07f7d0d73993ffc2924f1b9b2d21cbc53fb7c95aab959c0820fd7dbc35a1fffa"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/project-dossier.ts",
|
||||
"sha256_LF": "beca4eebd3959afa520061f231de25d064723537bd98deb541712ab448eba369"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/ispc-eligibility.ts",
|
||||
"sha256_LF": "44675c5efacdc1ae19505667611ba3b522d3043cebacc8754597c3a68ca89d25"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/simulation-ispc-quarter.tsx",
|
||||
"sha256_LF": "2c2555309b32d4261f14e7164f39314c9188129c2990357fc54090a2468dd2b4"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/simulation-ispc-withholding.tsx",
|
||||
"sha256_LF": "e0445d015b72859e48726959aa52686f79b1cf5c2ade29751efa83257068b878"
|
||||
}
|
||||
],
|
||||
"compiled_markers": {
|
||||
"Calcular e guardar operações ocasionais": [
|
||||
"assets/services-Cvwv29Mo.js"
|
||||
],
|
||||
"Calcular e guardar parcela excedente": [
|
||||
"assets/services-Cvwv29Mo.js"
|
||||
],
|
||||
"Preparar e guardar campos B–Geral": [
|
||||
"assets/services-Cvwv29Mo.js"
|
||||
],
|
||||
"Descarregar preparação B–Geral": [
|
||||
"assets/services-Cvwv29Mo.js"
|
||||
],
|
||||
"Calcular e guardar apuramento trimestral ISPC": [
|
||||
"assets/services-Cvwv29Mo.js"
|
||||
],
|
||||
"Calcular e guardar restituição E": [
|
||||
"assets/services-Cvwv29Mo.js"
|
||||
],
|
||||
"Calcular e guardar acompanhamento D": [
|
||||
"assets/services-Cvwv29Mo.js"
|
||||
],
|
||||
"Calcular e guardar substituições C": [
|
||||
"assets/services-Cvwv29Mo.js"
|
||||
]
|
||||
},
|
||||
"schema_backend_configuration_dependency_changes": false,
|
||||
"active_changes": false,
|
||||
"originals_previous_preserved": 17,
|
||||
"existing_books_preserved": "L3,ISPC habitual andacceptedhistories;no reset or overwrite of deliberateedits",
|
||||
"P001_P058": "unchanged;notblocking",
|
||||
"P084_P085": "closed",
|
||||
"statistics_AI": "deferred",
|
||||
"proof_hashes": {
|
||||
"outputs/ispc-residuals-20261025/frontend-final.txt": "bb0db1bde933e5fe08356537df1ba783003629f7fcc21abdcf18e119bcb0ab8d",
|
||||
"outputs/ispc-residuals-20261025/directed-final.txt": "9a2de706bca568599efd9dbfff7cb2c55ba037d64139e0b7d269271b9d84582b",
|
||||
"outputs/ispc-residuals-20261025/directed-retention-form.txt": "44061cb0f2661fb27b6c1e0acca437710d6569f482c0ad521bec95aced7216f5",
|
||||
"outputs/ispc-residuals-20261025/directed-known-exclusions.txt": "9c2e039af72cd3b46624979d1d07cae31ffc7f19572dfec1c279272dfdaee0dc",
|
||||
"outputs/ispc-residuals-20261025/directed-dispensation-form.txt": "ff6ab503c476af55e950937d99a40eb6591d7d479cfed60dcddc681dcf7421fb",
|
||||
"outputs/ispc-residuals-20261025/api.txt": "12ac172c5898f91ed9ac1bd0216265c33683e6984e817c5d6cb8759c1448b184",
|
||||
"outputs/ispc-residuals-20261025/types-package.txt": "5346ac8a6566c1c38cb0bbda6a029579df58cd78af78e2c4845d9c82b5d1d76c",
|
||||
"outputs/ispc-residuals-20261025/build-package.txt": "d3b9271730c3e0e6dd87f770a7d7badeb7a5e25e76aac44877b4484078e037b4",
|
||||
"outputs/ispc-residuals-20261025/ui-own-report.txt": "c770078829e65c3830116c192d819c1f8327b8024806b841d88436219bb26571",
|
||||
"outputs/ispc-residuals-20261025/ui-acquisition-report.txt": "ce976d2042e8ab7f13c42c42b8bd13a32ad67a2eb64ccf408ddf7d20688f1534",
|
||||
"outputs/ispc-residuals-20261025/ui-occasional-applicability.txt": "72a3eee5b24c5b18faef1ab00076fb9e4d550b84daac41690dfc4488201311b8",
|
||||
"outputs/ispc-residuals-20261025/ui-regular-applicability.txt": "2fb06f6e9e845022a464c9ffcc1fdee66b3ee2566bfce811c3e63b02cef4ab0a",
|
||||
"outputs/ispc-residuals-20261025/preparation.html": "bd8dabebcc27b1e27df345648cf6b8fd5ce780f37d422d23f3689ef941f52cf1",
|
||||
"outputs/ispc-residuals-20261025/active-baseline.json": "ed1e4d9c09215f9695fad1e02e6efec4c5a7097c5c35f0425e3a5142e98f2bc1",
|
||||
"outputs/ispc-residuals-20261025/directed-liberatory-history.txt": "ec7836ece4ea47a5e5b039958f18ed0d00cd87af219e1ce3cad2316c7f36977d",
|
||||
"outputs/ispc-residuals-20261025/types-final.txt": "5346ac8a6566c1c38cb0bbda6a029579df58cd78af78e2c4845d9c82b5d1d76c",
|
||||
"outputs/ispc-residuals-20261025/build-final.txt": "939ba2172a3b5360d909d36199887f34a9a654f2f4e57a19a12b9c763dd9ca92"
|
||||
},
|
||||
"preservation_audit": {
|
||||
"matrix_only_changed": "AUD-SIM-INT02",
|
||||
"L1_closed_preserved": 131,
|
||||
"original_PDFs_preserved": 17,
|
||||
"habitual_arithmetic_engines": "ispc-quarter.ts/ispc-withholding.ts byte-identical to6af7092e",
|
||||
"eligibility_regular_path": "byte-identical except message for occasional route",
|
||||
"source_scope_choices": "Separate books,concrete factinputs and exact cap intervals are implementation choices,not new organisational approvals;no generic legal/provenance revalidation."
|
||||
},
|
||||
"liberatory_history_guard": "Same operation reference/date/quarter/amount with documented prior liberatory withholding cannot generate another own payment without explicit correction reference; history preserved."
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"implementation_commit": "7e6e462e12a50aab6ba0043740ffeecb414cbedf",
|
||||
"status": "delivered_P131_P132_partial_P104_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"AUD-SIM-L1-P131": [
|
||||
"goods_available",
|
||||
"goods_transport",
|
||||
"goods_installation",
|
||||
"service_completed",
|
||||
"service_early_charge",
|
||||
"import_clearance",
|
||||
"import_auction",
|
||||
"continuous_period",
|
||||
"contract_effects"
|
||||
],
|
||||
"AUD-SIM-L1-P132": [
|
||||
"own_goods",
|
||||
"own_services",
|
||||
"commission_delivery",
|
||||
"consignment_deadline",
|
||||
"advance_payment"
|
||||
]
|
||||
},
|
||||
"coverage_partial": {
|
||||
"AUD-SIM-L1-P104": "Preparação e simulação cambial condicionada,gravação/recuperação/histórico/relatório;moeda original,taxa,direcção,data,fonte e pressuposto explícitos. Não fechaP104 e não confirma base fiscal. Falta obter/cotejar disposição de conversão da Lei2/2006 referida em15(8),seu momento/regra e correspondência em2026."
|
||||
},
|
||||
"branch_count_complete": 2,
|
||||
"event_treatment_count": 14,
|
||||
"conditional_scenario_count": 1,
|
||||
"source": "CIVA Lei13/2016:7(1)–(7),PDF8/p1426(266);8,PDF9/p1426(267);15(8),PDF14/p1426(272),páginas visualmente conferidas.Lei22/2022 eLei10/2025 não alteram7/8;15(8)mantido.RIVADecreto7/2008,21,PDF4:regra histórica5dias úteis e excepções;prazo actual não automatizado,exige disposição/calendário/âmbito identificados.",
|
||||
"meaning": {
|
||||
"P131": "Eventos7(1)a/b/c,7(2),7(3),7(7):disponibilização,início transporte,instalação/montagem concluída,serviço realizado/cobrado ou debitado antecipadamente pelo montante respectivo,desembaraço/arrematação/venda na importação,termo do período sucessivo,efeitos translativos com excepções3(3)a/b. Facto gerador separado de exigibilidade8;factura comercial não desloca evento aduaneiro.",
|
||||
"P132": "Eventos7(4)/(5)/(6) e8c:afectações/transmissões assimiladas3(3)e/f,serviços4(2),disponibilização pelo comissário,termo180dias de mercadorias à consignação3(3)d e pagamentos anteriores à factura. Corrigida remissão a consignação,não embalagensP102.8a/b/c separam parcela recebida e restante,sem dupla contagem."
|
||||
},
|
||||
"scope": "2026contexto;facto gerador7 e exigibilidade8 por data/âmbito;uma parcela de recebimento e restante por linha,ou parcelas de âmbitos distintos,sem agregação de totais entre linhas. Prazo de emissão é parâmetro legal identificado,não calendário presumido;importação segue7c. P104parcial não aplica câmbio fiscal.Sem nova base/IVA,taxa,dedução,campo11,regularização ou totaisModeloA;P133regimes suspensivos/viagens mantém âmbito separado.",
|
||||
"documentary_research_delimited": {
|
||||
"P104": {
|
||||
"finding": "Remissão15(8) para Lei2/2006 identificada;artigo de conversão e parâmetros ainda não cotejados em exemplar íntegro.",
|
||||
"attempts": [
|
||||
"Acervo local seisPDF semLei2/2006",
|
||||
"Pesquisa Lei2/2006/CIP identificaBR12-I22/03/2006",
|
||||
"PDF primário platformchinaplp devolveu404;DLA recusou/timeout;archiveBR2006 devolveu403"
|
||||
],
|
||||
"next_action": "Obter exemplar íntegroLei2/2006 e identificar disposição de conversão/momento/regra;reutilizar rascunhos,sem validação jurídica genérica."
|
||||
},
|
||||
"RIVA21": {
|
||||
"primary_notice": "https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/",
|
||||
"finding": "Anúncio governamental inclui alteração21;anúncio não substitui texto normativo/publicação. Prazo não presumido5dias úteis por defeito;campo exige termo/disposição/calendário aplicáveis.",
|
||||
"next_action": "Localizar texto publicado da alteração21 para eventual automatização do prazo;motor8já opera com parâmetro concreto identificado."
|
||||
}
|
||||
},
|
||||
"technical_choices": "currencyEvents poroportunidade nosnapshot/API/dossier existentes. Exporta/reutiliza convertCents já existente,no mesmo algoritmo,sem alteração dos cálculos financeiros anteriores;BigInt6casas/taxa8casas,arredondamento de cenário à6. DatasUTC/histórico existentes;nenhum relógioactual/taxacorrente ou feriado por defeito. Alterações de percurso conservam campos ocultos e exigem rever confirmações.",
|
||||
"verification": {
|
||||
"frontend": {
|
||||
"tests": 19,
|
||||
"passed": 19,
|
||||
"failed": 0,
|
||||
"coverage": "15percursos/exemplos:14eventos+1câmbiocondicionado;factura pontual/tardia/ausente,pagamento parcial/integral,termo prazo,restantependente,eventofuturo/pagamento anterior,importação7c,consignação180dias,moedainversa/arredondamento/parametros;persistência/histórico/relatório/preservação"
|
||||
},
|
||||
"privateAPI": {
|
||||
"tests": 1,
|
||||
"assertions": 65,
|
||||
"target": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"xml": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-currency-events-20261025\\phpunit.xml"
|
||||
},
|
||||
"types": "passed",
|
||||
"build": "passed;existingplugin timingwarnings",
|
||||
"UI": "Edge/CUA:referência prazo vazia conserva facto gerador e deixa exigibilidade indeterminada;restauro egravação/recuperação/relatório.Fixturefinal regenerada após separarimportação7c;15linhas/1versãoanterior,P104conditional64500MZN/P132pagamento200+restante800,importação2026-12-10.Dadossintéticos,nãoaceitaçãomanualTempo.",
|
||||
"UI_screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-currency-events-20261025\\qa-recovered-report.png"
|
||||
},
|
||||
"preservation": {
|
||||
"base": "7a3ac43224843f0f39900645bc901475275b18be",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P048",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P050",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P052",
|
||||
"AUD-SIM-L1-P053",
|
||||
"AUD-SIM-L1-P054",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P056",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P060",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P080",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086",
|
||||
"AUD-SIM-L1-P087",
|
||||
"AUD-SIM-L1-P088",
|
||||
"AUD-SIM-L1-P089",
|
||||
"AUD-SIM-L1-P090",
|
||||
"AUD-SIM-L1-P091",
|
||||
"AUD-SIM-L1-P092",
|
||||
"AUD-SIM-L1-P093",
|
||||
"AUD-SIM-L1-P094",
|
||||
"AUD-SIM-L1-P095",
|
||||
"AUD-SIM-L1-P096",
|
||||
"AUD-SIM-L1-P097",
|
||||
"AUD-SIM-L1-P098",
|
||||
"AUD-SIM-L1-P099",
|
||||
"AUD-SIM-L1-P100",
|
||||
"AUD-SIM-L1-P101",
|
||||
"AUD-SIM-L1-P102",
|
||||
"AUD-SIM-L1-P103",
|
||||
"AUD-SIM-L1-P105",
|
||||
"AUD-SIM-L1-P106",
|
||||
"AUD-SIM-L1-P107",
|
||||
"AUD-SIM-L1-P108",
|
||||
"AUD-SIM-L1-P109",
|
||||
"AUD-SIM-L1-P110",
|
||||
"AUD-SIM-L1-P111",
|
||||
"AUD-SIM-L1-P112",
|
||||
"AUD-SIM-L1-P113",
|
||||
"AUD-SIM-L1-P114",
|
||||
"AUD-SIM-L1-P115",
|
||||
"AUD-SIM-L1-P116",
|
||||
"AUD-SIM-L1-P117",
|
||||
"AUD-SIM-L1-P118",
|
||||
"AUD-SIM-L1-P119",
|
||||
"AUD-SIM-L1-P120",
|
||||
"AUD-SIM-L1-P122",
|
||||
"AUD-SIM-L1-P123",
|
||||
"AUD-SIM-L1-P124",
|
||||
"AUD-SIM-L1-P125",
|
||||
"AUD-SIM-L1-P126",
|
||||
"AUD-SIM-L1-P127",
|
||||
"AUD-SIM-L1-P128"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_delivery": "92eaa935/7a3ac432;174tables/369files;29published/28relations/408geometries;code/schema/data/config/permissions compared",
|
||||
"frozen": "tests\\frontend\\fixtures\\iva-currency-events-preserved.json",
|
||||
"sha256": "79972e699d32cea2be74f36c244825e47c63d20e6d1d3c14052ba564c4a63c93",
|
||||
"scope": "P104partial/P131/P132QA;allpriorbooks/history immutable;noactivewrites"
|
||||
},
|
||||
"runtime_delta": {
|
||||
"resources/js/lib/iva-currency-events.ts": "7133d1df2cf4171495bd145f5b8350938f0a3b4230af824c223d2b03cf0d9bb1",
|
||||
"resources/js/components/simulation-currency-events.tsx": "d103234361d57d0461d19a0f1fb9cbc5a714dbd566000478fad9aced585f32fa",
|
||||
"resources/js/components/simulation-monthly-iva.tsx": "44901ef5fc9def8ebd6c251d4af65f36a1cb330f5ed7d44db4a52d98753f18ff",
|
||||
"resources/js/lib/project-dossier.ts": "1712be726c868d182a2ddf405331dcc1f55bf0c3ce72ad32ab758c15afef2f4a",
|
||||
"resources/js/lib/business-financial-plan.ts": "c20274722d1ffcd0360f041a6cacb5d868fe0621cb27a1aca6e06826612c3ebb"
|
||||
},
|
||||
"counts_current": {
|
||||
"delivered": 118,
|
||||
"QA": 2,
|
||||
"functional_pending": 12,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"homologation_unchanged": "92eaa935",
|
||||
"remaining": "Entrega conjuntaP131/P132;P104incluídoapenascomopreparação/simulaçãocondicionada,seconstarnomanifesto,nãofecho. RestamP002/P046/P047/P058/P059/P084/P085/P104/P121/P129/P130/P133comimpedimentosnamesmamatriz.",
|
||||
"limits": "L1aberto;P00105/06;bebidasL2/L3;Estatísticas/IAadiadas",
|
||||
"sources_inventory": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-currency-events-20261025\\source-archive-inventory.json",
|
||||
"qa_status_preserved": "QA_complete_P131_P132_partial_P104_not_delivered",
|
||||
"delivery_followup": {
|
||||
"served_commit": "e8b5ae43add95be3a7fd437e4488befce76c0a97",
|
||||
"receipt": "l1-currency-events-delivery-20261025.json",
|
||||
"record": "iva_l1_currency_events_P131_P132_partial_P104_homologation_20261025",
|
||||
"counts": {
|
||||
"delivered": 120,
|
||||
"QA": 0,
|
||||
"functional_pending": 12,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"P104_closure": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"implementation_commit": "5f088e341125124077bc420a0b1c8ef0e8c28173",
|
||||
"status": "delivered_demonstrated_scope_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"AUD-SIM-L1-P121": [
|
||||
"web",
|
||||
"software_service",
|
||||
"information",
|
||||
"media",
|
||||
"teaching",
|
||||
"analogous"
|
||||
],
|
||||
"AUD-SIM-L1-P130": [
|
||||
"software_asset",
|
||||
"content_asset",
|
||||
"data_asset",
|
||||
"crypto_asset",
|
||||
"ebook_asset",
|
||||
"profile_asset",
|
||||
"access_asset",
|
||||
"other_asset"
|
||||
]
|
||||
},
|
||||
"branch_count_complete": 2,
|
||||
"treatment_count": 14,
|
||||
"standard_examples": 42,
|
||||
"additional_examples": 4,
|
||||
"source": "Lei10/2025, localPDF12–14/p2180(12–14),3(2)/(7),6(7)k/(8)/(9),23(2),25(7),26(4),32(1)c,33(2); Lei13/2016 localPDF8/p1426(266),6(7)k i–v, ePDF17/p1426(275),26(1–3). Páginas visualmente conferidas. Inventário de sete exemplares com hash preservado.",
|
||||
"source_link": "https://miramar.co.mz/wp-content/uploads/2026/01/Tax_Update_Key_Amendments_to_Mozambican_Tax_Legislation_1767376440.pdf",
|
||||
"scope": "Natureza digital, localização e responsável; gravação, recuperação, histórico e relatório. Não calcula taxa/base/IVA/dedução/campo11 ou totais; não envia declaração nem inicia pagamento. Software/acesso/licenciamento não decidem natureza apenas pelo título.",
|
||||
"rules": {
|
||||
"inbound": "Fornecedor estrangeiro e adquirente nacional: localização6(7)k.26(4) atribui obrigações ao adquirente sujeito passivo nacional identificado; qualidade desconhecida não apaga localização.25(7) declaração mensal/mapa;32(1)c referência até dia10 do mês seguinte.",
|
||||
"outbound": "Adquirente estabelecido/domiciliado no estrangeiro:6(8), sem isenção ou IVAzero presumidos.",
|
||||
"domestic": "Serviços6(3); bens3(2)/(7) equiparados a corpóreos,6(1) exige local na colocação à disposição demonstrado, sem inferência por servidor/IP/domicílio.",
|
||||
"other_nonresident": "26(1)/(2): representante residente/poderes/comunicação anterior;26(3): falta de representante e actividade comercial/industrial/profissional comprovadas. Não atribui retenção automaticamente a consumidor.33(2)26(1–3) separado de26(4)."
|
||||
},
|
||||
"documentary_limits": {
|
||||
"procedure": "Modelos/formalidades de25(7)/26(4) não cotejados neste incremento. A pesquisa oficial delimitada encontrou anúncio governamental de30/09/2026, não o texto normativo de execução. Isto não declara inexistência de regulamentação nem bloqueia classificação e preparação. Nenhum modelo, taxa de retenção, aprovador ou formulário inventado.",
|
||||
"official_notice": "https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/",
|
||||
"next_action": "Cotejar texto publicado e modelos aplicáveis antes de automatizar formalidades/submissão; manter preparação, localização e atribuição legal demonstrada.",
|
||||
"P104": "Mantém pendência de escolha da taxa/data; conversão condicionada preservada, sem dependência efectiva destes percursos.",
|
||||
"P084_P085": "Preparação/simulação condicionada preservadas; benefícios não encerrados."
|
||||
},
|
||||
"verification": {
|
||||
"frontend": {
|
||||
"tests": 57,
|
||||
"passed": 57,
|
||||
"failed": 0,
|
||||
"digital_tests": 49,
|
||||
"P002_adapter_compatibility_tests": 8
|
||||
},
|
||||
"privateAPI": {
|
||||
"tests": 1,
|
||||
"assertions": 209,
|
||||
"database": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction": "rollback",
|
||||
"examples": 46
|
||||
},
|
||||
"types": "passed",
|
||||
"build": "passed",
|
||||
"browser": "Edge: qualidade do adquirente Sim→Por esclarecer mantém localização Moçambique e responsável indeterminado; reposta, classificada, gravada, recuperada e relatório aberto. Resultado versão4, três versões históricas.",
|
||||
"screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-digital-20261025\\qa-recovered-report.png",
|
||||
"preservation": "Comparação integral dos livros e históricos congelados:120entregues e P002QA preservados; sem perdas confirmadas ou escrita activa."
|
||||
},
|
||||
"integration_preparation": {
|
||||
"branches": [
|
||||
"P002",
|
||||
"P121",
|
||||
"P130"
|
||||
],
|
||||
"accepted_P002_candidate": "e4c490b3",
|
||||
"ancestor_confirmed": true,
|
||||
"local_main": "7ccf62adec52e574fdcc75ea2e3852833e3dba10",
|
||||
"local_main_ancestor": true,
|
||||
"merge_tree": "76a7356e225e3e9a857f3aa744f151a3a692c8f4",
|
||||
"conflicts": false,
|
||||
"strategy": "Uma entrega compatível pelo processo Git existente; reutilizar provas por ramo. Revalidar main/destino fresco e conferir somente delta instalado na entrega autorizada. Nenhuma integração no activo ou push neste incremento.",
|
||||
"homologation_baseline": "e8b5ae43",
|
||||
"runtime_delta_from_baseline": [
|
||||
{
|
||||
"file": "resources/js/lib/iva-digital.ts",
|
||||
"sha256_lf": "623165a4e36e161776fea115ee7f8b9b61b4e5766831daa404badf8397eae271"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/components/simulation-digital.tsx",
|
||||
"sha256_lf": "7c6cc9a4a79e385f7554ac43a0dd6310c8b890b162fd3a224378c55127b3844c"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/lib/iva-foreign-operations.ts",
|
||||
"sha256_lf": "b2735b9071aac41f9ddf0ae0c81d3943c870adccfa315ec01432388fec4a7de8"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/components/simulation-foreign-operations.tsx",
|
||||
"sha256_lf": "5276d81afd97cd40df63973fcfdca27d80903464af078f67deb3e45c7bb90948"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/components/simulation-monthly-iva.tsx",
|
||||
"sha256_lf": "19269934dc83f2cb3c36b1677f0bcdf066dc0ffd2af889730bd92acb54532587"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/lib/project-dossier.ts",
|
||||
"sha256_lf": "f0428b411146e030bbf952428faffab6ac26178d09aa3a6437daa9363bc75a46"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/lib/iva-currency-events.ts",
|
||||
"sha256_lf": "fa99e217e96bc6601ebfe0ac48a8ad3395ce85d57218c23888e6eece5a7405fa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"balance": {
|
||||
"delivered": 120,
|
||||
"QA": 3,
|
||||
"functional_pending": 9,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"pending_by_dependency": {
|
||||
"documentary_specific": [
|
||||
"P104",
|
||||
"P084",
|
||||
"P085",
|
||||
"P001"
|
||||
],
|
||||
"travel_events": [
|
||||
"P129",
|
||||
"P133"
|
||||
],
|
||||
"imports_benefits": [
|
||||
"P046",
|
||||
"P047",
|
||||
"P058",
|
||||
"P059"
|
||||
]
|
||||
},
|
||||
"homologation": "e8b5ae43",
|
||||
"preservation_before_tests": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-digital-20261025\\preservation-before-tests.json",
|
||||
"source_inventory": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-digital-20261025\\source-archive-inventory.json",
|
||||
"qa_status_preserved": "QA_complete_P121_P130_not_delivered",
|
||||
"delivery_followup": {
|
||||
"served_commit": "df43aeb1e583e65af854ac71f46ddaa1f49b0b7c",
|
||||
"receipt": "l1-foreign-digital-delivery-20261025.json",
|
||||
"record": "iva_l1_foreign_digital_P002_P121_P130_homologation_20261025",
|
||||
"counts": {
|
||||
"delivered": 123,
|
||||
"QA": 0,
|
||||
"functional_pending": 9,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"digital_coverage_integral": false,
|
||||
"P104_closure": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"implementation_commit": "86970cd507795acb8ff7d942923d84e54e2d3331",
|
||||
"status": "delivered_demonstrated_scope_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"AUD-SIM-L1-P002": [
|
||||
"goods",
|
||||
"services",
|
||||
"listed_services",
|
||||
"foreign_transport_portion"
|
||||
]
|
||||
},
|
||||
"branch_count_complete": 1,
|
||||
"examples": 7,
|
||||
"meaning": "Localização estrangeira reutilizada; enquadramento potencial CIVA19(1)bii. Não concede nem calcula dedução/campo11, IVA, base ou totais. Hipótese interna e exclusões19(2) explícitas; localização nacional não equivale a isenção. Digital mantém âmbito próprio pendente.",
|
||||
"coverage_partial": {
|
||||
"AUD-SIM-L1-P104": "Exemplar integral Lei 2/2006 obtido: BR 12-I de 22/03/2006. Artigo 106(5), PDF18/página impressa66, conferido visualmente: facturação em moeda nacional. Não identifiquei no exemplar a regra de escolha da taxa/data cambial. P104 continua conversão condicionada, sem confirmação de base fiscal."
|
||||
},
|
||||
"documentary_research": {
|
||||
"P104": {
|
||||
"finding": "Exemplar integral Lei 2/2006 obtido: BR 12-I de 22/03/2006. Artigo 106(5), PDF18/página impressa66, conferido visualmente: facturação em moeda nacional. Não identifiquei no exemplar a regra de escolha da taxa/data cambial. P104 continua conversão condicionada, sem confirmação de base fiscal.",
|
||||
"next_action": "Localizar a disposição aplicável que concretiza a remissão do CIVA 15(8), especificando taxa e momento de conversão; conservar moeda, taxa, direcção, data e fonte. Sem taxa corrente por defeito nem nova validação genérica.",
|
||||
"source": {
|
||||
"file": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-foreign-operations-20261025\\Lei-2-2006-BR-12-I.pdf",
|
||||
"sha256": "efe00dbb9d9712d719f11d38a55be028c4d82315960ddfcc7786fdf9bdf133cb",
|
||||
"pages": 36,
|
||||
"origin": "https://www.dlapiperafrica.com/export/sites/africa/mozambique/insights/legislation-series/tax/downloads/Lei-n_2.2006.pdf_2063068952.pdf",
|
||||
"official_identity": "BR12-I22/03/2006;Lei2/2006",
|
||||
"publication": "Localresearchonly;notautomaticallypublishedorusedasFXrule"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verification": {
|
||||
"frontend": {
|
||||
"tests": 8,
|
||||
"passed": 8,
|
||||
"failed": 0
|
||||
},
|
||||
"privateAPI": {
|
||||
"tests": 1,
|
||||
"assertions": 47,
|
||||
"database": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction": "rollback"
|
||||
},
|
||||
"types": "passed",
|
||||
"build": "passed",
|
||||
"browser": "Edge: alteração para hipótese interna desconhecida conserva território e produz indeterminação; reposição, gravação, recuperação e relatório; 7 linhas, versão4, 3 versões históricas",
|
||||
"preservation": "120 fechos inventariados antes dos testes. Comparação integral dos livros anteriores congelados, excluindo apenas novos contextos sintéticos e novo livro P002. Nenhuma perda confirmada.",
|
||||
"screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-foreign-operations-20261025\\qa-recovered-report.png"
|
||||
},
|
||||
"runtime_delta": [
|
||||
{
|
||||
"file": "resources/js/lib/iva-foreign-operations.ts",
|
||||
"sha256_lf": "33ac9467a0c667d80cb5f66bad43ec567cd96207702579d57a9cbb15cc30d5f8"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/components/simulation-foreign-operations.tsx",
|
||||
"sha256_lf": "5e7eddbd4bd7c9388fc55956baee90c74f673fef6f0d4555cf6f5331e98f8d62"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/components/simulation-monthly-iva.tsx",
|
||||
"sha256_lf": "3072660e5f91668fc69761ddeead00ac9f5e3e752c72537c637cf07993b3ffd3"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/lib/project-dossier.ts",
|
||||
"sha256_lf": "3edb5682d7c8ca6e61e504eab67ca169f18ddcb07050c62fb58f57d3206b9829"
|
||||
},
|
||||
{
|
||||
"file": "resources/js/lib/iva-currency-events.ts",
|
||||
"sha256_lf": "fa99e217e96bc6601ebfe0ac48a8ad3395ce85d57218c23888e6eece5a7405fa"
|
||||
}
|
||||
],
|
||||
"scope": "QA isolada; sem escrita no activo, publicação de documentos, migração ou instalação. iva-currency-events.ts altera apenas explicação documental de P104.",
|
||||
"balance": {
|
||||
"delivered": 120,
|
||||
"QA": 1,
|
||||
"functional_pending": 11,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"homologation": "e8b5ae43",
|
||||
"pending": [
|
||||
"P104",
|
||||
"P121",
|
||||
"P130",
|
||||
"P129",
|
||||
"P133",
|
||||
"P046",
|
||||
"P047",
|
||||
"P058",
|
||||
"P059",
|
||||
"P084",
|
||||
"P085"
|
||||
],
|
||||
"preservation_receipt": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-foreign-operations-20261025\\preservation-before-tests.json",
|
||||
"qa_status_preserved": "QA_complete_P002_P104_pending_cotejo_documented_not_delivered",
|
||||
"delivery_followup": {
|
||||
"served_commit": "df43aeb1e583e65af854ac71f46ddaa1f49b0b7c",
|
||||
"receipt": "l1-foreign-digital-delivery-20261025.json",
|
||||
"record": "iva_l1_foreign_digital_P002_P121_P130_homologation_20261025",
|
||||
"counts": {
|
||||
"delivered": 123,
|
||||
"QA": 0,
|
||||
"functional_pending": 9,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"digital_coverage_integral": false,
|
||||
"P104_closure": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"implementation_commit": "2b592ded669f400e2322c183947a045bb3790c73",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"AUD-SIM-L1-P122": [
|
||||
"goods_sale_commission",
|
||||
"goods_purchase_commission"
|
||||
],
|
||||
"AUD-SIM-L1-P128": [
|
||||
"service_sale_mandate",
|
||||
"service_purchase_mandate"
|
||||
]
|
||||
},
|
||||
"branch_count": 2,
|
||||
"treatment_count": 4,
|
||||
"source": "CIVA3(3)c,Lei13/2016PDF6/p1426(264);4(3),PDF7/p1426(265). Lei22/2022não altera3/4;Lei10/2025PDF12mantém3(3)c e não altera4. Base15(2)e/P091já entregue permanece independente;sem nova aplicação da base neste incremento.",
|
||||
"meaning": {
|
||||
"P122": "Transferências de bens em execução de contrato de comissão,incluindo consignação;na venda comissário considerado comprador,na compra comitente considerado comprador. Venda:comitente→comissário→terceiro;compra:terceiro→comissário→comitente. Contraprestações e referências das duas relações separadas.",
|
||||
"P128": "Serviço efectuado por intervenção de mandatário emnome próprio:mandatário considerado adquirente eprestador sucessivamente4(3). Venda/prestação:mandante→mandatário→terceiro;compra/aquisição:terceiro→mandatário→mandante. Não é mera intermediação emnome de outrem ou base15(2)e de bens."
|
||||
},
|
||||
"formula": "Classifytwo successive relations bykind/direction;keep eachoriginalconsideration separate;no monetaryaggregation or taxcalculation.",
|
||||
"scope": "2026,circuitos emnome próprio,partes/contratos/objectos econtraprestações originais das duas relações. Sem soma/compensação automática,base tributável nova,localização,isenção,taxa,IVA,dedução/campo11 ou totaisModeloA. Sem conversão cambial;moeda original identificada e coerente com contexto. P091preservado,sem repetir aceitação/cálculo.",
|
||||
"technical_choices": "NewownNames opportunity-scoped book in existingmodelo_a snapshot/API/dossier,no schema/permissions. Switchretainsallhiddenrefs/values andclearsownconfirmations,keepscommon. Originaldecimalstrings up to15integer/6fractiondigits retained;no rounding/conversion. References or explicitassumptions reused,no certificates/approvers invented. OptionalexistingBaseRef notprerequisite;doesnotmodifyP091.",
|
||||
"verification": {
|
||||
"frontend": {
|
||||
"tests": 6,
|
||||
"passed": 6,
|
||||
"failed": 0,
|
||||
"by_treatment": "Four circuits:ownconditions unknown/no,direction/parties,separate originals,dates/refs/currency,zero vsunknown",
|
||||
"additional": "Switchretainsvalues/refs andclearsownconfirmations;nonmonetarytaxfields remainnull;allpriorbooks includingP091 unchanged,history/report/currentkey/corruptwriteguards"
|
||||
},
|
||||
"privateAPI": {
|
||||
"tests": 1,
|
||||
"assertions": 37,
|
||||
"target": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"xml": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g10-own-name-20261025\\api-tests.xml"
|
||||
},
|
||||
"types": "passed",
|
||||
"build": "passed,existing plugin timing warnings",
|
||||
"UI": "Edge/CUArealcomponent:ownNameunknown makescircuitindeterminate;restoredconfirmation andsave/recover/report4circuitswith2relations each and1000.25/1100.75MZN keptseparate,no totals,newbase/VAT/ratenull,3previousversions. SyntheticQA,notmanualTempoacceptance.",
|
||||
"UI_screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g10-own-name-20261025\\qa-recovered-report.png"
|
||||
},
|
||||
"preservation": {
|
||||
"base": "5837f34a2cc64a94f91f038ffcb0c62f5fb5ba6a",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P048",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P050",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P052",
|
||||
"AUD-SIM-L1-P053",
|
||||
"AUD-SIM-L1-P054",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P056",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P060",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P080",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086",
|
||||
"AUD-SIM-L1-P089",
|
||||
"AUD-SIM-L1-P090",
|
||||
"AUD-SIM-L1-P091",
|
||||
"AUD-SIM-L1-P094",
|
||||
"AUD-SIM-L1-P095",
|
||||
"AUD-SIM-L1-P096",
|
||||
"AUD-SIM-L1-P097",
|
||||
"AUD-SIM-L1-P098",
|
||||
"AUD-SIM-L1-P099",
|
||||
"AUD-SIM-L1-P100",
|
||||
"AUD-SIM-L1-P101",
|
||||
"AUD-SIM-L1-P102",
|
||||
"AUD-SIM-L1-P103",
|
||||
"AUD-SIM-L1-P105",
|
||||
"AUD-SIM-L1-P106",
|
||||
"AUD-SIM-L1-P107",
|
||||
"AUD-SIM-L1-P108",
|
||||
"AUD-SIM-L1-P109",
|
||||
"AUD-SIM-L1-P110",
|
||||
"AUD-SIM-L1-P111",
|
||||
"AUD-SIM-L1-P112",
|
||||
"AUD-SIM-L1-P113",
|
||||
"AUD-SIM-L1-P114",
|
||||
"AUD-SIM-L1-P115",
|
||||
"AUD-SIM-L1-P116",
|
||||
"AUD-SIM-L1-P117",
|
||||
"AUD-SIM-L1-P118",
|
||||
"AUD-SIM-L1-P119",
|
||||
"AUD-SIM-L1-P120"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_delivery": "2fdca863/5837f34a;174tables/369files;29published/28relations/408geometries;code/schema/data/config/permissions compared",
|
||||
"frozen": "tests\\frontend\\fixtures\\iva-g10-own-name-preserved.json",
|
||||
"sha256": "03defa4a680af33e1bcd05770d619343704d448d295142263b21c40e516b2a5d",
|
||||
"scope": "P122/P128QA;all priorbooks/history includingP105–110 andpartialP084/P085 immutable;no activewrites"
|
||||
},
|
||||
"runtime_delta": {
|
||||
"resources/js/lib/iva-own-name.ts": "c36509de24d31d747d91630a31ce7d72baa749e9928b718a944556348fc54f85",
|
||||
"resources/js/components/simulation-own-name.tsx": "6e142242aa29ae07e36b4d8d0be7d78f770d701e0a92e1a6e9832fcb794c17cf",
|
||||
"resources/js/components/simulation-monthly-iva.tsx": "9718dc440aa98497011775d8bd03049d7857fa3badf27f4467d6d434f642defd",
|
||||
"resources/js/lib/project-dossier.ts": "099dc307a86c763a9509516f6d59cc5c454b1f81189cc04b919d8d6045baf9dc"
|
||||
},
|
||||
"counts_current": {
|
||||
"delivered": 107,
|
||||
"QA": 2,
|
||||
"functional_pending": 23,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"homologation_unchanged": "2fdca863",
|
||||
"remaining": "OnejointGit delivery forP122/P128,freshbackup/realisolatedrestore,acceptedQA reused,installeddeltaonly. No individualbranch deployment.",
|
||||
"limits": "L1open;deductionoutsideincrement;P00105/06;P121digital/P084/P085pending;beveragesL2/L3;Statistics/AIdeferred",
|
||||
"sources_inventory": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g10-own-name-20261025\\source-archive-inventory.json",
|
||||
"qa_status_preserved": "QA_complete_not_delivered",
|
||||
"delivery_followup": {
|
||||
"served_commit": "63cc336787cade92fb7ee5b2b2d407308a0cf66b",
|
||||
"receipt": "l1-g10-own-name-delivery-20261025.json",
|
||||
"record": "iva_l1_g10_own_name_P122_P128_homologation_20261025",
|
||||
"counts": {
|
||||
"delivered": 109,
|
||||
"QA": 0,
|
||||
"functional_pending": 23,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"key": "iva_l1_g4e_beneficiaries_code_API_QA_visual_pending_20261024",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"base": "3be371c8",
|
||||
"group": "G4E",
|
||||
"branches": {
|
||||
"AUD-SIM-L1-P061": "export_nonresident_buyer",
|
||||
"AUD-SIM-L1-P069": "export_diplomatic_mission",
|
||||
"AUD-SIM-L1-P070": "export_international_organisation",
|
||||
"AUD-SIM-L1-P071": "export_humanitarian_organisation"
|
||||
},
|
||||
"branch_count": 4,
|
||||
"treatment_count": 4,
|
||||
"source": "CIVA13(1)(b/j/k/l),PDF12/p1426(270),19(1)(b)(i),PDF15/p1426(273); alterações2022/2025 reutilizadas",
|
||||
"preservation": {
|
||||
"initial_worktree": "clean at3be371c8",
|
||||
"accepted_delivered": 58,
|
||||
"prior_books": 18,
|
||||
"prior_books_identical_current": true,
|
||||
"confirmed_losses": [],
|
||||
"active": "No remote changes; accepted G4D inventory reused, not freshly re-read"
|
||||
},
|
||||
"tests": {
|
||||
"frontend_new": 3,
|
||||
"frontend_delta_with_G4D": 7,
|
||||
"API_tests": 1,
|
||||
"API_assertions": 39,
|
||||
"database": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"rollback": true,
|
||||
"types": "passed",
|
||||
"production": "built successfully; plugin timings warning; PowerShell classified native stderr as nonzero",
|
||||
"visual": false,
|
||||
"report_visual": false,
|
||||
"Word_PDF": "accepted exporter proofs reused; no pagination claim",
|
||||
"QA_build": "built successfully; existing future Vite config compatibility warning",
|
||||
"QA_storage": "save/recover/reject passed in temporary directory",
|
||||
"QA_HTTP": "200 HTML and module entrypoints; no visual claim"
|
||||
},
|
||||
"limits": "2026/MZN; projecção sob condições/documento ou pressuposto expresso, sem certificar exportação, acordo, reconhecimento ou despacho. Sem isenção geral do destinatário, dedução automática de compras, apuramento integral IVA ou submissãoAT. Bebidas permanecemL2/L3; P001só05/06; Estatísticas/IAadiadas.",
|
||||
"counts": {
|
||||
"delivered": 62,
|
||||
"QA_ready_for_delivery": 0,
|
||||
"functional_pending": 70,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"candidate": "38faeac6",
|
||||
"manual_visual_completion": {
|
||||
"status": "demonstrated_QA_not_installed",
|
||||
"candidate": "38faeac6",
|
||||
"prior_code_record": "d03101bb",
|
||||
"group": "G4E",
|
||||
"branches": [
|
||||
"P061",
|
||||
"P069",
|
||||
"P070",
|
||||
"P071"
|
||||
],
|
||||
"visual": {
|
||||
"actor": "Tempo",
|
||||
"method": "manual",
|
||||
"url": "http://127.0.0.1:18639/g4e.html",
|
||||
"request": "Quatro tratamentos; condição em falta sem apagar preço; Guardar → Recuperar → Relatório",
|
||||
"reply_exact": "ja efectuado",
|
||||
"tool_visual_observation": false,
|
||||
"no_repeat_of_accepted_proofs": true
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 58,
|
||||
"QA": 4,
|
||||
"functional_pending": 70,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"homologation": "686328a0/3be371c8",
|
||||
"L1_complete": false,
|
||||
"P001": "05/06 only",
|
||||
"beverages": "L2/L3; not closed",
|
||||
"statistics_AI": "deferred"
|
||||
},
|
||||
"visual_pending_historical": {
|
||||
"url": "http://127.0.0.1:18639/g4e.html",
|
||||
"reason": "Browser inventory empty; Edge reports Browser is not available. No security setting changed or workaround.",
|
||||
"request": "Quatro tratamentos; retirar condição de acordo/reconhecimento torna apuramento indeterminado sem apagar preço; Guardar → Recuperar → Relatório; comparar G4D preservado."
|
||||
},
|
||||
"delivery_followup": {
|
||||
"served_commit": "2e21ca45002b76adcef0347135783c706d37dd42",
|
||||
"record": "iva_l1_g4e_homologation_20261024",
|
||||
"receipt": "l1-g4e-delivery-20261024.json"
|
||||
},
|
||||
"qa_acceptance_counts": {
|
||||
"delivered": 58,
|
||||
"QA_ready_for_delivery": 4,
|
||||
"functional_pending": 70,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"qa_acceptance_status": "demonstrated_QA_not_installed"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"key": "iva_l1_g5a_special_regimes_QA_demonstrated_20261024",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"candidate": "1f7fc13872bd706f583a6f4d29024cd3c22ec265",
|
||||
"base": "0eb2c485",
|
||||
"homologation": "2e21ca45/0eb2c485",
|
||||
"group": "G5A",
|
||||
"branches": {
|
||||
"AUD-SIM-L1-P081": [
|
||||
"regime_dispatch_goods",
|
||||
"regime_dispatch_transport"
|
||||
],
|
||||
"AUD-SIM-L1-P082": [
|
||||
"regime_internal_goods",
|
||||
"regime_internal_services"
|
||||
],
|
||||
"AUD-SIM-L1-P083": [
|
||||
"regime_transit_goods",
|
||||
"regime_transit_transport"
|
||||
]
|
||||
},
|
||||
"branch_count": 3,
|
||||
"treatment_count": 6,
|
||||
"source": "CIVA14(1)(b/c/d),PDF13/p1426(271),19(1)(b)(iv),PDF15/p1426(273);Lei22/2022art1;Lei10/2025art1/PDF12–13; fontes aceites, texto/remissões conferidos",
|
||||
"new_field": "specialRegime opcional nos livros anteriores,obrigatório nos seis tratamentos; selecção apenas do regime compatível com a alínea; escolha não comprova as condições",
|
||||
"preservation": {
|
||||
"delivered": 62,
|
||||
"prior_books": 19,
|
||||
"prior_books_identical_current_without_recalculation": true,
|
||||
"UI_prior_books_identical": true,
|
||||
"confirmed_losses": [],
|
||||
"active_inventory": "Accepted G4E receipt reused; no fresh active query/mutation claimed"
|
||||
},
|
||||
"tests": {
|
||||
"frontend_new": 4,
|
||||
"API_tests": 1,
|
||||
"API_assertions": 44,
|
||||
"QA_database": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"types": "passed",
|
||||
"production_build": "passed; existing plugin timing warnings",
|
||||
"QA_build": "passed; existing future config/chunk size warnings",
|
||||
"QA_storage": "save/recover/reject passed in temporary directory",
|
||||
"UI": {
|
||||
"actor": "Codex",
|
||||
"method": "Edge extension/CUA observed actions",
|
||||
"URL": "http://127.0.0.1:18640/g5a.html",
|
||||
"observed": [
|
||||
"six distinct treatments initially visible",
|
||||
"no-final-use condition unchecked -> indeterminate; price100MZN retained",
|
||||
"unknown regime -> indeterminate; price/currency/period retained",
|
||||
"restore regime and explicitly recheck conditions ->600MZN/05",
|
||||
"Guardar -> Recuperar -> Relatório with context/regime/source and totals",
|
||||
"three previous results in history;19priorbooks unchanged"
|
||||
],
|
||||
"manual_Tempo_reply": null,
|
||||
"screenshot": "outputs/iva-l1-g5-regimes-20261024/qa-recovered-report.png",
|
||||
"screenshot_sha256": "ec68a1287ade198f34544fed0e5b1f49a6c15c2e6855fcaf13dc34ceb45c8772"
|
||||
},
|
||||
"Word_PDF": "accepted exporter/numeric proofs reused; no new pagination claim"
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 65,
|
||||
"QA": 0,
|
||||
"functional_pending": 67,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"G5": {
|
||||
"delivered": 3,
|
||||
"QA": 0,
|
||||
"functional_pending": 4
|
||||
},
|
||||
"limits": "2026/MZN; condição inicial14(1),regime/destino/permanência e ligação directa próprios; preço distinto dos bens/serviços. Sem isenção pelo mapa, certificação de regime, cálculo da própria importação/saída, aquisições14(2),dedução automática ou submissãoAT. P080/P084/P085/P086 continuam pendentes. L1aberto;P001só05/06;bebidasL2/L3;Estatísticas/IAadiadas.",
|
||||
"G4E_summary_reused": {
|
||||
"P061": "export_nonresident_buyer",
|
||||
"P069": "export_diplomatic_mission",
|
||||
"P070": "export_international_organisation",
|
||||
"P071": "export_humanitarian_organisation"
|
||||
},
|
||||
"qa_acceptance_status": "demonstrated_QA_not_installed",
|
||||
"qa_acceptance_counts": {
|
||||
"delivered": 62,
|
||||
"QA": 3,
|
||||
"functional_pending": 67,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"delivery_followup": {
|
||||
"served_commit": "69f2e365bf4ebd509ec2e080dd0bf1b7717d7a75",
|
||||
"record": "iva_l1_g5a_homologation_20261024",
|
||||
"receipt": "l1-g5a-delivery-20261024.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"key": "iva_l1_g5b_P086_acquisition_continuity_QA_demonstrated_20261024",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"candidate": "689a4c93c33cd737c6fc90823533b34b5219b5d3",
|
||||
"base": "d3e9f373",
|
||||
"homologation": "69f2e365/d3e9f373",
|
||||
"group": "G5B",
|
||||
"branches": {
|
||||
"AUD-SIM-L1-P086": "former_exploration_exemption -> explicit normal acquisition continuation"
|
||||
},
|
||||
"branch_count": 1,
|
||||
"scope": "nonapplication and continuation;not a newly active exemption",
|
||||
"source": {
|
||||
"rectification": {
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-89-I-Serie-2017-06-08.pdf",
|
||||
"bytes": 9456358,
|
||||
"sha256": "11f0cab15eb6e51010952203ffa24fdc533a24fd642e19525fe4f46b17a1f5e5",
|
||||
"pages": 16
|
||||
},
|
||||
"page": "PDF1/p569,visual inspected",
|
||||
"CIVA_original": "14(2)(c),PDF13/p1426(271)",
|
||||
"2022_2025": "art1 lists checked;no reintroduction14(2)(c)",
|
||||
"acceptance": "Tempo08/10/2026;no generic provenance/legal revalidation added"
|
||||
},
|
||||
"inventory": {
|
||||
"archive_files": [
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-16-I-Serie-2008-04-16.pdf",
|
||||
"bytes": 2267865,
|
||||
"sha256": "fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee",
|
||||
"pages": 28
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-248-I-Suplemento-1-2025.pdf",
|
||||
"bytes": 440018,
|
||||
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
|
||||
"pages": 21
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-89-I-Serie-2017-06-08.pdf",
|
||||
"bytes": 9456358,
|
||||
"sha256": "11f0cab15eb6e51010952203ffa24fdc533a24fd642e19525fe4f46b17a1f5e5",
|
||||
"pages": 16
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-95-I-Suplemento-2025-05-21.pdf",
|
||||
"bytes": 329070,
|
||||
"sha256": "e0e682e2c027c3cd9c1d46a73a3f48bc5f6bdbf05667fd97314d7a3f6a456358",
|
||||
"pages": 2
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-13-2016-CIVA-BR-156-10-suplemento.pdf",
|
||||
"bytes": 2127554,
|
||||
"sha256": "b90dc7c14ecea2e378c4a62e8e5a47d54a5644c457baeb8b8053b92fd2a5710b",
|
||||
"pages": 32
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf",
|
||||
"bytes": 1834284,
|
||||
"sha256": "ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53",
|
||||
"pages": 18
|
||||
}
|
||||
],
|
||||
"existing_corpus_references": [
|
||||
{
|
||||
"path": "docs/investmoz/document-corpus-inventory-2026-10-06.csv",
|
||||
"sha256": "3a24442558ad2b9d4ac9b58cc6d281bd0db83b4c93d4696e30a6f8981d9eaa7b",
|
||||
"usage": "existing corpus/source inventory reused; no claim of newly reviewing every source"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/fiscal-source-references-2026-10-06.csv",
|
||||
"sha256": "f2fde2257fd481f984e59bdfc62ce209c849b350961111616d2c2927c1421d6f",
|
||||
"usage": "existing corpus/source inventory reused; no claim of newly reviewing every source"
|
||||
}
|
||||
]
|
||||
},
|
||||
"preservation": {
|
||||
"delivered": 65,
|
||||
"sales_books": 20,
|
||||
"sales_books_identical_current_without_recalculation": true,
|
||||
"purchase_books_in_frozen_fixture": 0,
|
||||
"UI_previous_sales_identical": true,
|
||||
"new_purchase_context": "opportunity:329",
|
||||
"UI_history_count": 3,
|
||||
"confirmed_losses": [],
|
||||
"active": "Accepted G5A receipt reused;no fresh active query/mutation"
|
||||
},
|
||||
"implementation": {
|
||||
"optional_fields": [
|
||||
"acquisitionTreatment",
|
||||
"acquisitionTreatmentOrigin",
|
||||
"AcquisitionBook.history"
|
||||
],
|
||||
"legacy_fields": "No new mandatory keys on previous rows/books;scoped continuity version only on new chosen treatment",
|
||||
"manual_periods": "calculate only explicit purchase-book months;budget reuse still verifies existing source;no sales month required for direct entries",
|
||||
"period_change_impact": "Report includes declared purchase months rather than implicit financial-plan months;previous accepted4-month scenarios pass;no frozen result recalculated",
|
||||
"history": "Previous result snapshots retained when explicitly recalculating;old reference/document/values/answers conserved",
|
||||
"not_applicable_UI": "Normal deduction controls hidden while former eliminated reference selected;no deleted data"
|
||||
},
|
||||
"tests": {
|
||||
"frontend_new": 4,
|
||||
"existing_changed_acquisition_suite": 8,
|
||||
"frontend_total": 12,
|
||||
"latest_delta": 4,
|
||||
"API_tests": 1,
|
||||
"API_assertions": 45,
|
||||
"QA_database": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"types": "passed",
|
||||
"production_build": "passed;existing plugin timing warnings",
|
||||
"QA_build": "passed;existing config/chunk warnings",
|
||||
"QA_storage": "save/recover/reject passed in temporary directory",
|
||||
"UI": {
|
||||
"actor": "Codex",
|
||||
"method": "Edge/CUA observed",
|
||||
"URL": "http://127.0.0.1:18641/g5b.html",
|
||||
"observed": [
|
||||
"former14(2)(c) selected -> eliminated notice;deduction indeterminate,100/16/116MZN retained",
|
||||
"normal deduction controls hidden while nonapplicable",
|
||||
"explicit continue ->normal controls/facts restored;calculate ->16MZN field10 under normal declared conditions",
|
||||
"Guardar -> Recuperar -> Relatório with document,source,period/context;17/18 remain indeterminate",
|
||||
"three results in history and20salesbooks unchanged in saved file"
|
||||
],
|
||||
"manual_Tempo_reply": null,
|
||||
"screenshot": "outputs/iva-l1-g5b-acquisitions-20261024/qa-recovered-report.png",
|
||||
"screenshot_sha256": "e29df1a8daf66f43ab7aaab9f78f472cf331272860aaac0edcde6ff40fd3a9d5"
|
||||
},
|
||||
"Word_PDF": "Existing exporter/numeric proofs reused;no new pagination claim"
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 66,
|
||||
"QA": 0,
|
||||
"functional_pending": 66,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"G5": {
|
||||
"delivered": 4,
|
||||
"QA": 0,
|
||||
"functional_pending": 3
|
||||
},
|
||||
"limits": "2026/MZN;P086 é não aplicação da antiga14(2)(c),eliminada pelo Aviso2017,e continuidade deliberada na aquisição normal; não é isenção nova/activa nem cálculo retroactivo. Sem taxa presumida,dedução automática,importação,apuramento integral ou submissãoAT. P080/P084/P085 pendentes. L1aberto;P001só05/06;bebidasL2/L3;Estatísticas/IAadiadas.",
|
||||
"qa_acceptance_status": "demonstrated_QA_not_installed",
|
||||
"qa_acceptance_counts": {
|
||||
"delivered": 65,
|
||||
"QA": 1,
|
||||
"functional_pending": 66,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"delivery_followup": {
|
||||
"served_commit": "6f056359df4231f9e1802922782b21263a8dae0b",
|
||||
"record": "iva_l1_g5b_homologation_20261024",
|
||||
"receipt": "l1-g5b-delivery-20261024.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"candidate": "9842dcbb0e570bc9af6d4f0d20b76f67bee30178",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"P080": [
|
||||
"special_economic_zone_import",
|
||||
"free_zone_import",
|
||||
"free_warehouse_import",
|
||||
"general_free_warehouse_import",
|
||||
"customs_warehouse_import",
|
||||
"duty_free_shop_import"
|
||||
]
|
||||
},
|
||||
"source": "Lei13/2016,CIVA14(1)(a),initial condition14(1),PDF13/p1426(271);no14change in consulted2022/2025 amendments",
|
||||
"source_reused": "Existing six-file source archive and general corpus/reference inventories;official source acceptanceTempo08/10;not reauthenticated",
|
||||
"legal_interpretation_limits": "P080 own import,not sale;not mere geography;all six regimes,customs control,applicable provisions,permanence,no final use/consumption;no automatic field11/duties/normalVAT",
|
||||
"tests": {
|
||||
"frontend": {
|
||||
"passed": 20,
|
||||
"failed": 0,
|
||||
"new": 3,
|
||||
"record": "node:test tool output this session;no separately saved stdout claimed"
|
||||
},
|
||||
"API": {
|
||||
"tests": 1,
|
||||
"assertions": 26,
|
||||
"xml": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g5c-review-20261024\\api-tests.xml",
|
||||
"target": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transactions": "rollback"
|
||||
},
|
||||
"types": "passed",
|
||||
"production_build": "passed;existing plugin/chunk warnings",
|
||||
"UI": "Codex Edge/CUA,real component;unknown->indeterminate,no->nonapplication,yes->exempt;save/recover/report;not manualTempo acceptance",
|
||||
"synthetic_QA_storage": "1 test passed;UTF8 multibyte buffer decoding preserved"
|
||||
},
|
||||
"preservation": {
|
||||
"base": "e384418ecc76ed77fe34a30814c4737a96887b00",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P053",
|
||||
"AUD-SIM-L1-P054",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"preservation": "Reuse accepted G6C installed receipt d46b794a/2ed62165; no active code/schema/data/config/permissions changes in this QA task",
|
||||
"frozen": "tests/frontend/fixtures/iva-g6d-inputs.json",
|
||||
"frozen_sha256": "26f9b75b4b436bfd2e21487b8eed12e8db62e9ebfc46e3ace9c92271994fc80b",
|
||||
"books": {
|
||||
"sales": 20,
|
||||
"acquisitions": 1,
|
||||
"imports": 4
|
||||
},
|
||||
"counts_before": {
|
||||
"delivered": 72,
|
||||
"QA": 1,
|
||||
"pending": 59,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"G6D": "P048 remains QA, not installed"
|
||||
},
|
||||
"UI_book": {
|
||||
"rows": 6,
|
||||
"history": 5,
|
||||
"opportunity": "334",
|
||||
"province": "Manica",
|
||||
"period": "2026-12",
|
||||
"document_date": "2026-11-14"
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 72,
|
||||
"QA": 2,
|
||||
"functional_pending": 58,
|
||||
"documentary_P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"active": "unchanged d46b794a/2ed62165;no push,installation,migration or active data change",
|
||||
"other_QA": "P048 G6D candidate0eb37ef5 preserved,not delivered",
|
||||
"pending": {
|
||||
"P084": {
|
||||
"source": "14(2)(a),14(3),samePDF13",
|
||||
"treatment": "Acquisition/import of goods for gifts to national public-interest/relevant-social institutions;goods wholly suitable and used in evident public-interest activities;no services",
|
||||
"documentary": "Application procedure referred to in14(3) not located in consulted sources;not declared nonexistent",
|
||||
"development": "Specific acquisition/import flow absent;common persistence/report reused",
|
||||
"verification": "Not performed for this branch",
|
||||
"action": "Locate applicable implementing provision and amendments/scope,then implement only express conditions and verify save/recover/report",
|
||||
"estimate": "6–10h preliminary development/verification after documentary delimitation;unmeasured;document retrieval ETA unknown"
|
||||
},
|
||||
"P085": {
|
||||
"source": "14(2)(b),14(3),samePDF13",
|
||||
"treatment": "Acquisition of goods for gifts mitigating effects of natural disasters;no automatic extension to import/services",
|
||||
"documentary": "Application procedure referred to in14(3) not located in consulted sources;not declared nonexistent",
|
||||
"development": "Specific acquisition/goods/disaster-purpose flow absent",
|
||||
"verification": "Not performed for this branch",
|
||||
"action": "Locate applicable implementation procedure;reuse P084 common fields while keeping own scope;verify complete flow",
|
||||
"estimate": "4–8h preliminary after documentary delimitation,reusingP084;unmeasured;retrieval ETA unknown"
|
||||
}
|
||||
},
|
||||
"limits": "72delivered remainclosed;L1open;P001only05/06;beveragesL2/L3;Statistics/AIdeferred;L1closure doesnot requireL2/L3/alltaxes",
|
||||
"qa_status_preserved": "P080 demonstrated QA,not delivered;P084/P085 individually diagnosed and still pending",
|
||||
"delivery_followup": {
|
||||
"served_commit": "2d889a211ba303db31dcba815e439456dfba9f36",
|
||||
"record": "iva_l1_P048_P080_joint_homologation_20261024",
|
||||
"receipt": "l1-g6d-g5c-delivery-20261024.json",
|
||||
"counts": {
|
||||
"delivered": 74,
|
||||
"QA": 0,
|
||||
"functional_pending": 58,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"key": "iva_l1_g6a_P049_P051_import_exemptions_QA_demonstrated_20261024",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"candidate": "965da2642627ad1e484d52587e07b4a60cae6af2",
|
||||
"base": "e99ea1d0",
|
||||
"homologation": "6f056359/e99ea1d0",
|
||||
"group": "G6A",
|
||||
"branches": {
|
||||
"AUD-SIM-L1-P049": "reimport_same_state",
|
||||
"AUD-SIM-L1-P051": "central_bank_gold"
|
||||
},
|
||||
"branch_count": 2,
|
||||
"source": {
|
||||
"reference": "CIVA12(1)(d/f),Lei13/2016PDF11/p1426(269),visual inspected;2022PDF2/p2252(26) and2025PDF12–13/p2180(12–13) retain d/f",
|
||||
"acceptance": "Tempo08/10/2026;no new generic authenticity/legal approval",
|
||||
"archive": [
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-16-I-Serie-2008-04-16.pdf",
|
||||
"sha256": "fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee",
|
||||
"pages": 28
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-248-I-Suplemento-1-2025.pdf",
|
||||
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
|
||||
"pages": 21
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-89-I-Serie-2017-06-08.pdf",
|
||||
"sha256": "11f0cab15eb6e51010952203ffa24fdc533a24fd642e19525fe4f46b17a1f5e5",
|
||||
"pages": 16
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-95-I-Suplemento-2025-05-21.pdf",
|
||||
"sha256": "e0e682e2c027c3cd9c1d46a73a3f48bc5f6bdbf05667fd97314d7a3f6a456358",
|
||||
"pages": 2
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-13-2016-CIVA-BR-156-10-suplemento.pdf",
|
||||
"sha256": "b90dc7c14ecea2e378c4a62e8e5a47d54a5644c457baeb8b8053b92fd2a5710b",
|
||||
"pages": 32
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf",
|
||||
"sha256": "ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53",
|
||||
"pages": 18
|
||||
}
|
||||
],
|
||||
"existing_corpus_references": [
|
||||
"docs/investmoz/document-corpus-inventory-2026-10-06.csv",
|
||||
"docs/investmoz/fiscal-source-references-2026-10-06.csv"
|
||||
]
|
||||
},
|
||||
"preservation": {
|
||||
"base": "e99ea1d0137543ca3fc37cb2c79732f3bb7c3edf",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_active_receipt": "l1-g5b-delivery-20261024.json;6f056359/e99ea1d0;174tables/369files/29published/28relations/408geometries;no schema/content/permissions changes",
|
||||
"frozen_fixture_sha256": "96f239cd9edd6d522447ce16e7b6dc32610f836a9d25dc8ea8789fb3042cf6f2",
|
||||
"sales_books": 20,
|
||||
"purchase_books": 1,
|
||||
"preserved_open": [
|
||||
"P080",
|
||||
"P084",
|
||||
"P085"
|
||||
],
|
||||
"counts": {
|
||||
"delivered": 66,
|
||||
"QA": 0,
|
||||
"functional_pending": 66,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"frontend_new": 5,
|
||||
"affected_report_tests": 14,
|
||||
"frontend_total": 19,
|
||||
"API_tests": 1,
|
||||
"API_assertions": 20,
|
||||
"QA_database": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"types": "passed",
|
||||
"production_build": "passed;existing plugin warnings",
|
||||
"QA_build": "passed;existing chunk warning",
|
||||
"QA_storage": "save/recover/reject passed,temporary file only",
|
||||
"UI": {
|
||||
"actor": "Codex",
|
||||
"method": "Edge/CUA observed",
|
||||
"URL": "http://127.0.0.1:18642/g6a.html",
|
||||
"observed": [
|
||||
"unknown condition ->indeterminate",
|
||||
"failed condition ->nonapplication,normal tax unknown",
|
||||
"affirmative conditions ->exempt import VAT0.00",
|
||||
"switch treatment ->only applicable questions;answers,document,1000MZN retained",
|
||||
"save/recover/report ->P049/P051,Manica,2026-12,documents,sources",
|
||||
"three previous UI results;20salesbooks and1purchasebook identical"
|
||||
],
|
||||
"manual_Tempo_reply": null,
|
||||
"screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6a-imports-20261024\\qa-recovered-report.png",
|
||||
"screenshot_sha256": "e6914778f6513e2ea22850d1b30a79ff3592e0c8085cb09c38f1ffb23541cf9c"
|
||||
},
|
||||
"Word_PDF": "Existing exporter proofs reused;no new pagination claim"
|
||||
},
|
||||
"implementation": {
|
||||
"optional_snapshot_key": "importExemptions",
|
||||
"no_schema_change": true,
|
||||
"legacy_books": "no additional required fields;20sales and1purchase unchanged/current",
|
||||
"data_conditions": "per branch;unknown != false;no ordinary rate inferred",
|
||||
"value": "declared/previsional value preserved,optional;no invented customs taxable base",
|
||||
"period": "explicit import period independent from supporting document date and sales calendar",
|
||||
"history": "explicit classification archives previous result;edits keep historical state;reports show only applicable conditions",
|
||||
"roll_back": "l1-g6a-readonly-rollback-20261024.patch checked,not applied;readers/history/reports retained"
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 68,
|
||||
"QA": 0,
|
||||
"functional_pending": 64,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"G6": {
|
||||
"delivered": 2,
|
||||
"QA": 0,
|
||||
"functional_pending": 13
|
||||
},
|
||||
"limits": "2026/MZN;classificação L1 de12(1)(d/f),sem direitosaduaneirosL13,dedução/campo11L2,baseaduaneira,taxa normal presumida,apuramento integral ou submissãoAT. P080/P084/P085 explicitamente abertos;L1aberto;P001só05/06;bebidasL2/L3;Estatísticas/IAadiadas.",
|
||||
"qa_acceptance_status": "demonstrated_QA_not_installed",
|
||||
"qa_acceptance_counts": {
|
||||
"delivered": 66,
|
||||
"QA": 2,
|
||||
"functional_pending": 64,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"delivery_followup": {
|
||||
"served_commit": "e3601c124d50fa2b52ea9d9fb86fbddf1d224d51",
|
||||
"record": "iva_l1_g6a_homologation_20261024",
|
||||
"receipt": "l1-g6a-delivery-20261024.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"key": "iva_l1_g6b_P055_P057_import_exemptions_QA_demonstrated_20261024",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"candidate": "f75f36f0e36a5d9981f631b6e22f3c9e723ee454",
|
||||
"base": "a23f1dcb",
|
||||
"homologation": "e3601c12/a23f1dcb",
|
||||
"group": "G6B",
|
||||
"branches": {
|
||||
"AUD-SIM-L1-P055": "author_art_import",
|
||||
"AUD-SIM-L1-P057": "fire_brigade_vehicle"
|
||||
},
|
||||
"branch_count": 2,
|
||||
"source": {
|
||||
"reference": "CIVA12(1)(j/l),Lei13/2016PDF11/p1426(269),visual inspected;2022PDF2/p2252(26) and2025PDF12–13/p2180(12–13) retain j/l",
|
||||
"acceptance": "Tempo08/10/2026;no new generic authenticity/legal approval",
|
||||
"archive": [
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-16-I-Serie-2008-04-16.pdf",
|
||||
"sha256": "fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee",
|
||||
"pages": 28
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-248-I-Suplemento-1-2025.pdf",
|
||||
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
|
||||
"pages": 21
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-89-I-Serie-2017-06-08.pdf",
|
||||
"sha256": "11f0cab15eb6e51010952203ffa24fdc533a24fd642e19525fe4f46b17a1f5e5",
|
||||
"pages": 16
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-95-I-Suplemento-2025-05-21.pdf",
|
||||
"sha256": "e0e682e2c027c3cd9c1d46a73a3f48bc5f6bdbf05667fd97314d7a3f6a456358",
|
||||
"pages": 2
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-13-2016-CIVA-BR-156-10-suplemento.pdf",
|
||||
"sha256": "b90dc7c14ecea2e378c4a62e8e5a47d54a5644c457baeb8b8053b92fd2a5710b",
|
||||
"pages": 32
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf",
|
||||
"sha256": "ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53",
|
||||
"pages": 18
|
||||
}
|
||||
],
|
||||
"existing_corpus_references": [
|
||||
"docs/investmoz/document-corpus-inventory-2026-10-06.csv",
|
||||
"docs/investmoz/fiscal-source-references-2026-10-06.csv"
|
||||
]
|
||||
},
|
||||
"preservation": {
|
||||
"base": "a23f1dcb6e886a6de75917f17f2da4b9ab94599e",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_active_receipt": "l1-g6a-delivery-20261024.json;e3601c12/a23f1dcb;174tables/369files/29published/28relations/408geometries;no schema/content/permissions changes",
|
||||
"frozen_fixture_sha256": "6765354faf917282ea03bed31cee071d1a44cf008a7560171048adb8ffa6debf",
|
||||
"sales_books": 20,
|
||||
"purchase_books": 1,
|
||||
"import_books": 1,
|
||||
"import_history": 3,
|
||||
"preserved_open": [
|
||||
"P080",
|
||||
"P084",
|
||||
"P085"
|
||||
],
|
||||
"counts": {
|
||||
"delivered": 68,
|
||||
"QA": 0,
|
||||
"functional_pending": 64,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"frontend_new": 4,
|
||||
"existing_import_suite": 5,
|
||||
"frontend_total": 9,
|
||||
"API_tests": 1,
|
||||
"API_assertions": 23,
|
||||
"QA_database": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"types": "passed",
|
||||
"production_build": "passed;existing plugin warnings",
|
||||
"QA_build": "passed;existing chunk warning",
|
||||
"QA_storage": "save/recover/reject and UTF8 split-multibyte boundary passed;Buffer concatenation before decoding;temporary file only",
|
||||
"UI": {
|
||||
"actor": "Codex",
|
||||
"method": "Edge/CUA observed",
|
||||
"URL": "http://127.0.0.1:18643/g6b.html",
|
||||
"observed": [
|
||||
"unknown condition ->indeterminate",
|
||||
"failed condition ->nonapplication,normal tax unknown",
|
||||
"affirmative conditions ->exempt import VAT0.00",
|
||||
"switch treatment ->only applicable questions;answers,document,2500MZN retained",
|
||||
"save/recover/report ->P055/P057,Manica,2026-12,documents,sources",
|
||||
"four previous UI results;20salesbooks,1purchase and1importbook identical"
|
||||
],
|
||||
"manual_Tempo_reply": null,
|
||||
"screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6b-imports-20261024\\qa-recovered-report.png",
|
||||
"screenshot_sha256": "7f6ef7c5a3ee5aca026ad29279510b2ab9fa63d76d13b878007a5c58f4163a7e"
|
||||
},
|
||||
"Word_PDF": "Existing exporter proofs reused;no new pagination claim"
|
||||
},
|
||||
"implementation": {
|
||||
"existing_snapshot_key": "importExemptions",
|
||||
"scoped_version": "import-beneficiaries/1 only when new treatment selected;legacy keys/source preserved",
|
||||
"no_schema_change": true,
|
||||
"legacy_books": "no additional required fields;20sales,1purchase and1importbook unchanged/current",
|
||||
"data_conditions": "per branch;unknown != false;no ordinary rate inferred",
|
||||
"value": "declared/previsional value preserved,optional;no invented customs taxable base",
|
||||
"period": "explicit import period independent from supporting document date and sales calendar",
|
||||
"history": "explicit classification archives previous result;edits keep historical state;reports show only applicable conditions",
|
||||
"roll_back": "l1-g6b-readonly-rollback-20261024.patch checked,not applied;readers/history/reports retained"
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 70,
|
||||
"QA": 0,
|
||||
"functional_pending": 62,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"G6": {
|
||||
"delivered": 4,
|
||||
"QA": 0,
|
||||
"functional_pending": 11
|
||||
},
|
||||
"limits": "2026/MZN;classificação L1 de12(1)(j/l),sem direitosaduaneirosL13,dedução/campo11L2,baseaduaneira,taxa normal presumida,apuramento integral ou submissãoAT. P080/P084/P085 explicitamente abertos;L1aberto;P001só05/06;bebidasL2/L3;Estatísticas/IAadiadas.",
|
||||
"QA_auxiliary_encoding_correction": {
|
||||
"cause": "Demo middleware decoded each incoming byte chunk separately;one split UTF8 character changed in a prior-history key",
|
||||
"scope": "local synthetic QA file only;private API and accepted active data unchanged",
|
||||
"before": "ui-saved-before-utf8-fix.json preserved",
|
||||
"fix": "Concatenate buffers before UTF8 decoding;boundary regression test passed",
|
||||
"verification": "Reset from exact private API recovery via explicit QA button;save/recover/report repeated;all20sales+1purchase+1importbooks identical to frozen baseline",
|
||||
"helper_sha256": "3cbaac752269c91c6675a10e95646292cc8191949c23700b37805ed46a49672f"
|
||||
},
|
||||
"qa_acceptance_status": "demonstrated_QA_not_installed",
|
||||
"qa_acceptance_counts": {
|
||||
"delivered": 68,
|
||||
"QA": 2,
|
||||
"functional_pending": 62,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"delivery_followup": {
|
||||
"served_commit": "4cebd617651ca877bd4715c661973873e69f05e1",
|
||||
"record": "iva_l1_g6b_homologation_20261024",
|
||||
"receipt": "l1-g6b-delivery-20261024.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"key": "iva_l1_g6c_P053_P054_import_assets_QA_demonstrated_20261024",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"candidate": "8e5565d5e0501ecd2a40ae684f4948b867308c54",
|
||||
"base": "72e56f7a",
|
||||
"homologation": "4cebd617/72e56f7a",
|
||||
"group": "G6C",
|
||||
"branches": {
|
||||
"AUD-SIM-L1-P053": [
|
||||
"eligible_ship_import",
|
||||
"ship_operation_object_import"
|
||||
],
|
||||
"AUD-SIM-L1-P054": [
|
||||
"eligible_aircraft_import",
|
||||
"aircraft_operation_object_import"
|
||||
]
|
||||
},
|
||||
"branch_count": 2,
|
||||
"treatment_count": 4,
|
||||
"source": {
|
||||
"reference": "CIVA12(1)(h/i),Lei13/2016PDF11/p1426(269),by reference13(1)(f/g) and13(1)(c/d),PDF12/p1426(270);full-page visual proofs G6A/G4B reused;2022PDF2/p2252(26) and2025PDF13/p2180(13) retain h/i and do not change13",
|
||||
"acceptance": "Tempo08/10/2026;no generic authenticity/legal revalidation",
|
||||
"archive": [
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-16-I-Serie-2008-04-16.pdf",
|
||||
"sha256": "fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee",
|
||||
"pages": 28
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-248-I-Suplemento-1-2025.pdf",
|
||||
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
|
||||
"pages": 21
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-89-I-Serie-2017-06-08.pdf",
|
||||
"sha256": "11f0cab15eb6e51010952203ffa24fdc533a24fd642e19525fe4f46b17a1f5e5",
|
||||
"pages": 16
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-95-I-Suplemento-2025-05-21.pdf",
|
||||
"sha256": "e0e682e2c027c3cd9c1d46a73a3f48bc5f6bdbf05667fd97314d7a3f6a456358",
|
||||
"pages": 2
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-13-2016-CIVA-BR-156-10-suplemento.pdf",
|
||||
"sha256": "b90dc7c14ecea2e378c4a62e8e5a47d54a5644c457baeb8b8053b92fd2a5710b",
|
||||
"pages": 32
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf",
|
||||
"sha256": "ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53",
|
||||
"pages": 18
|
||||
}
|
||||
],
|
||||
"existing_corpus_references": [
|
||||
"docs/investmoz/document-corpus-inventory-2026-10-06.csv",
|
||||
"docs/investmoz/fiscal-source-references-2026-10-06.csv"
|
||||
]
|
||||
},
|
||||
"preservation": {
|
||||
"base": "72e56f7ae5b3c5d8c0f22b61778ec291b2bca2f6",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_active_receipt": "l1-g6b-delivery-20261024.json;4cebd617/72e56f7a;174tables/369files/29published/28relations/408geometries;no schema/content/permissions changes",
|
||||
"frozen_fixture_sha256": "acbd2187c64bf4952b90117d247e5d8440b71cf094aad36ffe147e2098ef9d17",
|
||||
"sales_books": 20,
|
||||
"purchase_books": 1,
|
||||
"import_books": 2,
|
||||
"import_histories": {
|
||||
"opportunity:330": 3,
|
||||
"opportunity:331": 4
|
||||
},
|
||||
"preserved_open": [
|
||||
"P080",
|
||||
"P084",
|
||||
"P085"
|
||||
],
|
||||
"counts": {
|
||||
"delivered": 70,
|
||||
"QA": 0,
|
||||
"functional_pending": 62,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"frontend_new": 4,
|
||||
"existing_import_suites": 9,
|
||||
"frontend_total": 13,
|
||||
"API_tests": 1,
|
||||
"API_assertions": 24,
|
||||
"QA_database": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"types": "passed",
|
||||
"production_build": "passed;existing plugin timing warnings",
|
||||
"QA_build": "passed;existing chunk warning",
|
||||
"QA_storage": "save/recover/reject and split-multibyte UTF8 boundary passed;inherited Buffer concatenation preserved",
|
||||
"UI": {
|
||||
"actor": "Codex",
|
||||
"method": "Edge/CUA observed",
|
||||
"URL": "http://127.0.0.1:18644/g6c.html",
|
||||
"observed": [
|
||||
"four treatments mapped to two IDs",
|
||||
"object link unknown ->indeterminate;negative ->nonapplication;affirmative ->exempt0.00",
|
||||
"switch object to principal aircraft ->own condition unknown,object response retained;return ->response restored",
|
||||
"4000MZN/document/2026-12 conserved",
|
||||
"save/recover/report ->P053/P054/four treatments,Manica,opportunity332,source12h/i and13f/g",
|
||||
"four previous new-book UI results;all23acceptedbooks identical"
|
||||
],
|
||||
"manual_Tempo_reply": null,
|
||||
"screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6c-imports-20261024\\qa-recovered-report.png",
|
||||
"screenshot_sha256": "d09a98c6a6c73a526c8803358eef6448cdc634a8a430c6ab8fe691ecdbd32bf4"
|
||||
},
|
||||
"Word_PDF": "Existing exporter proofs reused;no new pagination claim"
|
||||
},
|
||||
"implementation": {
|
||||
"existing_snapshot_key": "importExemptions",
|
||||
"no_schema_change": true,
|
||||
"scoped_version": "import-assets/1 only for selected asset treatments;G6A/G6B keys/source remain unchanged",
|
||||
"shared_definitions": "internationalShipEligibility/internationalAircraftEligibility export existing G4B strings without changing sale treatments",
|
||||
"legacy_books": "20sales,1purchase and2importbooks identical/current without recalculation",
|
||||
"history": "explicit classification archives previous result;edits keep earlier version",
|
||||
"not_applicable_UI": "only conditions for selected principal asset/object;hidden answers retained",
|
||||
"field11": "no deduction or ModeloA totals populated by classification",
|
||||
"rollback": "l1-g6c-readonly-rollback-20261024.patch checked,not applied;readers/history/reports retained"
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 72,
|
||||
"QA": 0,
|
||||
"functional_pending": 60,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"G6": {
|
||||
"delivered": 6,
|
||||
"QA": 0,
|
||||
"functional_pending": 9
|
||||
},
|
||||
"limits": "2026/MZN;classificação12(1)(h/i),bem principal ou objecto da exploração identificado,sem baseaduaneira,direitosL13,dedução/campo11L2,taxa normal presumida,apuramento integral ou submissãoAT. Não encerraP052/abastecimentos. P080/P084/P085 explicitamente abertos;L1aberto;P001só05/06;bebidasL2/L3;Estatísticas/IAadiadas.",
|
||||
"qa_acceptance_status": "demonstrated_QA_not_installed",
|
||||
"qa_acceptance_counts": {
|
||||
"delivered": 70,
|
||||
"QA": 2,
|
||||
"functional_pending": 60,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"delivery_followup": {
|
||||
"served_commit": "d46b794ab2c6113738f76c69097ae3dcddcf56dd",
|
||||
"record": "iva_l1_g6c_homologation_20261024",
|
||||
"receipt": "l1-g6c-delivery-20261024.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"key": "iva_l1_g6d_P048_import_customs_QA_demonstrated_20261024",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"candidate": "0eb37ef59b9b65837e6791518b378fe4e64ca112",
|
||||
"base": "2ed62165",
|
||||
"homologation": "d46b794a/2ed62165",
|
||||
"group": "G6D",
|
||||
"branches": {
|
||||
"AUD-SIM-L1-P048": [
|
||||
"customs_transit_import",
|
||||
"customs_temporary_import",
|
||||
"customs_drawback_import"
|
||||
]
|
||||
},
|
||||
"branch_count": 1,
|
||||
"treatment_count": 3,
|
||||
"source": {
|
||||
"reference": "CIVA12(1)(c),Lei13/2016PDF11/p1426(269),full-page visual proof G6A reused;2022PDF2/p2252(26) and2025PDF13/p2180(13) retain c by reference;no new expanded wording used",
|
||||
"acceptance": "Tempo08/10/2026;no generic authenticity/legal revalidation",
|
||||
"archive": [
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-16-I-Serie-2008-04-16.pdf",
|
||||
"sha256": "fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee",
|
||||
"pages": 28
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-248-I-Suplemento-1-2025.pdf",
|
||||
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
|
||||
"pages": 21
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-89-I-Serie-2017-06-08.pdf",
|
||||
"sha256": "11f0cab15eb6e51010952203ffa24fdc533a24fd642e19525fe4f46b17a1f5e5",
|
||||
"pages": 16
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\BR-95-I-Suplemento-2025-05-21.pdf",
|
||||
"sha256": "e0e682e2c027c3cd9c1d46a73a3f48bc5f6bdbf05667fd97314d7a3f6a456358",
|
||||
"pages": 2
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-13-2016-CIVA-BR-156-10-suplemento.pdf",
|
||||
"sha256": "b90dc7c14ecea2e378c4a62e8e5a47d54a5644c457baeb8b8053b92fd2a5710b",
|
||||
"pages": 32
|
||||
},
|
||||
{
|
||||
"file": "docs\\investmoz\\simulations\\source-archive\\Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf",
|
||||
"sha256": "ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53",
|
||||
"pages": 18
|
||||
}
|
||||
],
|
||||
"existing_corpus_references": [
|
||||
"docs/investmoz/document-corpus-inventory-2026-10-06.csv",
|
||||
"docs/investmoz/fiscal-source-references-2026-10-06.csv"
|
||||
],
|
||||
"scope_lookup_2020": {
|
||||
"URL": "https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-suplemento-n%C2%BA-1-de-231220-pag-2414-1-20-br-n%C2%BA-246-boletim-da",
|
||||
"scope": "Official INM index identifies Lei16/2020 as altering9(13),not12(1)(c)",
|
||||
"retrieval": "Index search text read;direct page timeout and two official PDF download attempts failed;no fullPDF obtained or claimed",
|
||||
"use": "scope lookup for an existing reference;not a second authenticity approval and not the operative text of12(1)(c)"
|
||||
}
|
||||
},
|
||||
"preservation": {
|
||||
"base": "2ed62165b7a5b7e0a836db350fb39ace6453fed5",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P053",
|
||||
"AUD-SIM-L1-P054",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_active_receipt": "l1-g6c-delivery-20261024.json;d46b794a/2ed62165;174tables/369files/29published/28relations/408geometries;no schema/content/permissions changes",
|
||||
"frozen_fixture_sha256": "ac5f1b23de6b3e13c9b0e116b435c389d8147d5f4145b19dc234bf79596c5db6",
|
||||
"sales_books": 20,
|
||||
"purchase_books": 1,
|
||||
"import_books": 3,
|
||||
"import_histories": {
|
||||
"opportunity:330": 3,
|
||||
"opportunity:331": 4,
|
||||
"opportunity:332": 4
|
||||
},
|
||||
"preserved_open": [
|
||||
"P080",
|
||||
"P084",
|
||||
"P085"
|
||||
],
|
||||
"counts": {
|
||||
"delivered": 72,
|
||||
"QA": 0,
|
||||
"functional_pending": 60,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"frontend_new": 4,
|
||||
"existing_import_suites": 13,
|
||||
"frontend_total": 17,
|
||||
"API_tests": 1,
|
||||
"API_assertions": 25,
|
||||
"QA_database": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"types": "passed",
|
||||
"production_build": "passed;existing plugin timing warnings",
|
||||
"QA_build": "passed;existing chunk warning",
|
||||
"QA_storage": "save/recover/reject and split-multibyte UTF8 boundary passed;inherited Buffer concatenation preserved",
|
||||
"UI": {
|
||||
"actor": "Codex",
|
||||
"method": "Edge/CUA observed",
|
||||
"URL": "http://127.0.0.1:18645/g6d.html",
|
||||
"observed": [
|
||||
"three treatments mapped to one ID",
|
||||
"full customs duty exemption unknown ->indeterminate;negative ->nonapplication;affirmative with own regime ->exempt0.00",
|
||||
"switch transit to temporary import ->own regime condition unknown,previous answers retained;return ->transit response restored",
|
||||
"5000MZN/document/2026-12 conserved",
|
||||
"save/recover/report ->P048/three treatments,Manica,opportunity333,source12(1)(c)",
|
||||
"four previous new-book UI results;all24acceptedbooks identical"
|
||||
],
|
||||
"manual_Tempo_reply": null,
|
||||
"screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6d-imports-20261024\\qa-recovered-report.png",
|
||||
"screenshot_sha256": "fadcff5be7c2a7a768a87531fefab15d189f87c9181e60fc2c794b4a5a01ae8c"
|
||||
},
|
||||
"Word_PDF": "Existing exporter proofs reused;no new pagination claim"
|
||||
},
|
||||
"implementation": {
|
||||
"existing_snapshot_key": "importExemptions",
|
||||
"no_schema_change": true,
|
||||
"scoped_version": "import-customs-regimes/1 only for selected customs import treatments;G6A/G6B/G6C keys/source remain unchanged",
|
||||
"regime_confirmations": "transit/temporary/drawback have distinct keys;fullCustomsExemption differs from P049 customsExempt;partial relief or suspension cannot satisfy total exemption",
|
||||
"legacy_books": "20sales,1purchase and3importbooks identical/current without recalculation",
|
||||
"history": "explicit classification archives previous result;edits keep earlier version",
|
||||
"not_applicable_UI": "only conditions for selected customs regime;hidden answers retained",
|
||||
"field11": "no deduction or ModeloA totals populated by classification",
|
||||
"rollback": "l1-g6d-readonly-rollback-20261024.patch checked,not applied;readers/history/reports retained"
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 72,
|
||||
"QA": 1,
|
||||
"functional_pending": 59,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"G6": {
|
||||
"delivered": 6,
|
||||
"QA": 1,
|
||||
"functional_pending": 8
|
||||
},
|
||||
"limits": "2026/MZN;P048/12(1)(c),trânsito/importação temporária/draubaque com isenção total de direitos,sem baseaduaneira,cálculo de direitosL13,saída de regime,dedução/campo11L2,taxa normal presumida,apuramento integral ou submissãoAT. Não confundir com vendas em regimeG5/P083. P080/P084/P085 explicitamente abertos;L1aberto;P001só05/06;bebidasL2/L3;Estatísticas/IAadiadas.",
|
||||
"qa_status_preserved": "demonstrated_QA_not_installed",
|
||||
"delivery_followup": {
|
||||
"served_commit": "2d889a211ba303db31dcba815e439456dfba9f36",
|
||||
"record": "iva_l1_P048_P080_joint_homologation_20261024",
|
||||
"receipt": "l1-g6d-g5c-delivery-20261024.json",
|
||||
"counts": {
|
||||
"delivered": 74,
|
||||
"QA": 0,
|
||||
"functional_pending": 58,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"candidate": "1565aa4ffb13fae75e925ccdb2b7553a9b111dd5",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"AUD-SIM-L1-P052": [
|
||||
"ship_provisions_import",
|
||||
"ship_fuel_import",
|
||||
"ship_technical_products_import",
|
||||
"ship_cargo_accessories_import",
|
||||
"aircraft_provisions_import",
|
||||
"aircraft_fuel_import",
|
||||
"aircraft_technical_products_import",
|
||||
"aircraft_cargo_accessories_import"
|
||||
]
|
||||
},
|
||||
"branch_count": 1,
|
||||
"treatment_count": 8,
|
||||
"source": "Lei13/2016,CIVA12(1)(g),12(3)(a)(i–iv)/(b),PDF11–12/p1426(269–270),13(3);2022PDF2/2025PDF13g/3 maintainedbyreference",
|
||||
"source_scope": "Ship maritime/fluvial/lacustrine international navigation;aircraft international navigation,not requirement of principallyinternationalcompany fromP054. Provisions,fuel,technicalproducts,cargoaccessories;ownexclusions only. No deduction/field11,customsduties/base ornormalVAT assumed.",
|
||||
"new_checks": {
|
||||
"frontend": {
|
||||
"passed": 3,
|
||||
"failed": 0,
|
||||
"record": "node:test outputthissession,no savedstdoutclaimed;acceptedoldproofs reused,not rerun"
|
||||
},
|
||||
"API": {
|
||||
"tests": 2,
|
||||
"assertions": 32,
|
||||
"target": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"rollback": true,
|
||||
"xml": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6e-supplies-20261024\\api-tests.xml"
|
||||
},
|
||||
"types": "passed",
|
||||
"production_build": "passed;existingplugin/chunkwarnings",
|
||||
"QA_storage": "1testpassed,save/recover/reject,multibyteUTF8bufferboundary",
|
||||
"UI": "CodexEdgeCUArealcomponent:coastalfishingexclusion unknown/nonapplication/exempt states,fueltanksconditiononlyfuel,save/recover/report8rows,5historyversions;notmanualTempoacceptance"
|
||||
},
|
||||
"technical_obstacle_resolved": {
|
||||
"origin": "Implementationchoice,notTempobusinessrule",
|
||||
"old_limit_characters": 1000000,
|
||||
"observed_preserved_draft_characters": 1040665,
|
||||
"new_limit_characters": 4000000,
|
||||
"why": "Do not discardacceptedbooks/explicit history tofitnewbranch;allowcurrentdraftwithboundedJSONstringvalidation",
|
||||
"evidence": "InitialAPI422 recorded intooloutput;afterfix exactprivate recoveryand4millionboundnegative testpassed;no activechange"
|
||||
},
|
||||
"preservation": {
|
||||
"base": "4e4bf6b8595f1b92d5a9de1b90be8d639f34feb5",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P048",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P053",
|
||||
"AUD-SIM-L1-P054",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P080",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_delivery": "P048/P0802d889a21/e3269079;174tables/369files/29published/28relations/408geometries,postgres equivalent;reuse accepted code/schema/data/config/permission delta proof",
|
||||
"frozen_fixture": "tests\\frontend\\fixtures\\iva-offers-inputs.json",
|
||||
"sha256": "98e7e77d3ed4bd0151b8192f190b3ed1eba4e2174385c8d0aa613be04331023e",
|
||||
"preserved": "26fiscalbooks+1offerpreparationbook(2rows,histories,deliberatepurposeedit);no recalculation",
|
||||
"counts_before": {
|
||||
"delivered": 74,
|
||||
"QA": 0,
|
||||
"functional_pending": 58,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"active_changes": 0
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 74,
|
||||
"QA": 1,
|
||||
"functional_pending": 57,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"active": "2d889a21/e3269079unchanged;no push/install/migration",
|
||||
"P084_P085": "ModelI.1diploma checked jointly;Despacho06/01/2010->Decreto56/2009art2->CBFLei4/2009;no direct14(3)correspondence found inreadRegulation1–16;scenario mapping recorded separately;drafts5ec2f198preservedexactly",
|
||||
"limits": "L1open;P001only05/06;beveragesL2/L3;Statistics/AIdeferred;closingL1doesnotrequireL2/L3/alltaxes",
|
||||
"qa_status_preserved": "P052 demonstratedQA;notinstalled;P084/P085 drafts preserved,benefits pending",
|
||||
"delivery_followup": {
|
||||
"served_commit": "c1314cdf90c368b51489aaf04c2cdbe59cfb88e6",
|
||||
"receipt": "l1-g6e-delivery-20261024.json",
|
||||
"record": "iva_l1_g6e_P052_homologation_20261024",
|
||||
"counts": {
|
||||
"delivered": 75,
|
||||
"QA": 0,
|
||||
"functional_pending": 57,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"candidate": "29e708ba2dca2450eaff8eff6f5e19079ffc7f8e",
|
||||
"implementation_commit": "f9fb04f9",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"AUD-SIM-L1-P050": [
|
||||
"service_in_customs_value",
|
||||
"accessory_service_first_destination"
|
||||
]
|
||||
},
|
||||
"branch_count": 1,
|
||||
"treatment_count": 2,
|
||||
"source": "CIVA12(1)(e),Lei13/2016PDF11/p1426(269);16(1)/16(1)(b)/16(2),PDF14/p1426(272);2022PDF1–2/2025PDF12–13maintain12e byreference,notchange16;2020/2025May9(13)scopes reused",
|
||||
"meaning": "Servicefullvalueincluded in importtaxbase,eitheralreadyincustomsvalue oraddedaccessorycharge untilfirstdestination;destinationfromtransportdocument or,initsabsence,firstbreakbulk inland. Two technicalprofiles,notnewbusinessconditions.",
|
||||
"tax_separation": "serviceVat0.00 onlywhenownconditions;importVat remainsnull forP050,no customsbase/duties/importedgoodstaxcalculation,normalratepresumption,invoicecorrection orfield11deduction",
|
||||
"new_inputs": "OptionalserviceImportRef,serviceBaseRef,serviceFirstDestination(onlyaccessoryprofile),serviceProvider. Servicevalue/reference/context requiredforclassification;not newmandatoryfields onoldgoods. HidegoodsCode/originCountry forservices;preserve hiddenanswers across profilechange.",
|
||||
"reference_reuse": "P050services filteredout from importedgoods references ofP084/P085;no newtaxbenefit or goodsconversion",
|
||||
"verification": {
|
||||
"frontend": {
|
||||
"new_tests": 4,
|
||||
"passed": 4,
|
||||
"failed": 0,
|
||||
"scope": "allownconditions,missing/failednotzero,ownfirstdestination,switchpreservation,service/importVATseparation,27priorfiscalbooks+946fc089drafts exact,currentoldkeys,report,malformedservicewriteguard,no service-as-goods reuse",
|
||||
"record": "latestnode:test outputthissession;acceptedoldproofs reused,not rerun"
|
||||
},
|
||||
"privateAPI": {
|
||||
"tests": 1,
|
||||
"assertions": 30,
|
||||
"target": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"rollback": true,
|
||||
"xml": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6f-services-20261024\\api-tests.xml"
|
||||
},
|
||||
"types_final": "passed",
|
||||
"production_build_final": "passed,existingplugin/chunkwarnings",
|
||||
"synthetic_QA_storage": "1passed includingmultibyteUTF8bufferboundary",
|
||||
"UI": "CodexEdgeCUArealcomponent:inclusionunknown->indeterminate,no->nonapplication,yes->serviceexemption;servicefieldsownrefs;goodsfields hidden,firstdestination onlyaccessory;save/recover/report2rows/opportunity336/Manica/2026-12/document14Nov2026/5historyversions;notmanualTempoacceptance"
|
||||
},
|
||||
"integrity_fix": "Newmalformedoptionalreference test initially detected savepath lacked validating newencodedbook;now validatesbefore returning/onChange,no original overwritten;validoldbooks/keys preserved",
|
||||
"preservation": {
|
||||
"base": "77d1290258a925a104b616dc4648db33d43064ea",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P048",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P052",
|
||||
"AUD-SIM-L1-P053",
|
||||
"AUD-SIM-L1-P054",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P080",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"preservation": "Reuse acceptedG6Ec1314cdf/536559d6:174tables/369files/29published/28relations/408geometries;code/schema/data/config/permissions delta accepted;no active query/write in this QA task",
|
||||
"frozen": "tests\\frontend\\fixtures\\iva-offer-paths-inputs.json",
|
||||
"sha256": "78125d82172cda05c16b10e74ac2fdfd4eaaa25bd99cd3bde33281c2bfdd01e6",
|
||||
"books": "27fiscalbooks+offerbook;requeststates/scopes/normal-conditionalscenarios/history946fc089preservedexactly",
|
||||
"counts_before": {
|
||||
"delivered": 75,
|
||||
"QA": 0,
|
||||
"functional_pending": 57,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
}
|
||||
},
|
||||
"counts": {
|
||||
"delivered": 75,
|
||||
"QA": 1,
|
||||
"functional_pending": 56,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
},
|
||||
"active": "Unchangedc1314cdf/536559d6;no push/install/activequeries/schema/data/config/permissionchange",
|
||||
"P084_P085": {
|
||||
"authorized_partial_candidate": "946fc089",
|
||||
"next_delivery_scope": "Preparation/requeststates/scopeddecisions/humanitarianpurpose/normalandconditionalhypotheticalscenarios,notconfirmedbenefit/notclosing2branches",
|
||||
"documentary_scope": "IdentifyexpressapplicableCIVA14(3)procedure/link and amendments;distinguishP084internal/import andP085acquisition. ModelI.1/Despacho/CBFcotejo reused,not repeated;do notinventcertificates/approvers orclaimregulation nonexistent",
|
||||
"preserved": "Allrows,scopeversions,deliberateedits,normal/conditionalinputs andhistory exactlyequalfrozenfixture"
|
||||
},
|
||||
"limits": "L1open;P001only05/06;beveragesL2/L3;classificationseparatefromfield11;Statistics/AIdeferred;L1closuredoesnotrequireL2/L3/alltaxes",
|
||||
"qa_status_preserved": "P050 demonstratedQA,notinstalled;P084/P085 partialpreparation/scenarios includedinancestry andauthorizedfornextdelivery,notbenefitclosure",
|
||||
"delivery_followup": {
|
||||
"served_commit": "78c9f4a356d5eaf590d8d82168228a2f84b1f633",
|
||||
"receipt": "l1-g6f-delivery-20261024.json",
|
||||
"record": "iva_l1_g6f_P050_with_partial_P084_P085_homologation_20261024",
|
||||
"counts": {
|
||||
"delivered": 76,
|
||||
"QA": 0,
|
||||
"functional_pending": 56,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"implementation_commit": "9783c54b2c70b2d91fefd08a394a21b0690af5ba",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"AUD-SIM-L1-P056": [
|
||||
"authorized_investment_equipment"
|
||||
]
|
||||
},
|
||||
"branch_count": 1,
|
||||
"treatment_count": 1,
|
||||
"condition_count": 4,
|
||||
"source": "CIVA12(1)(k),Lei13/2016PDF11/p1426(269);Lei22/2022PDF2/p2252(26),Lei10/2025PDF13/p2180(13)maintain k by remissão;2017rectificationPDF1 concerns6/14/annexes,not12k",
|
||||
"meaning": "Imported equipment classified K,destined to investment in undertaking authorized under InvestmentLaw/regulation;authorization reference and applicable scope,not authorization request or automatic class lookup",
|
||||
"source_links": [
|
||||
"https://www.inm.gov.mz/pt-br/content/suplemento-n%C2%BA-10-de-301216-pag-1426-259-290-br-n%C2%BA-156-boletim-da-rep%C3%BAblica-i-serie",
|
||||
"https://pt.apiex.gov.mz/investir/"
|
||||
],
|
||||
"technical_choices": "Dedicated optional reference/scope fields required only for newP056 classification;traceability of statutory conditions,not additional certificate/approver or official-source revalidation;reuse importer,documents,value,period,evidence and opportunity",
|
||||
"scope": "2026/MZN,classification of import VAT only;no customsbase/duties,automatic authorization or goodsclassification,domestic purchaseexemption,deduction/field11/ModeloA totals",
|
||||
"result": "0.00 only ownconditions yes+reference/scope;projected assumption expressly labelled;missing=indeterminate;no=notapplicable and no presumed ordinaryrate",
|
||||
"verification": {
|
||||
"frontend": {
|
||||
"tests": 3,
|
||||
"passed": 3,
|
||||
"failed": 0,
|
||||
"scope": "own conditions,mistyped writes,switch retention,context invalidation,all delivered books unchanged,serviceVAT separate,partial offers unchanged,history,report"
|
||||
},
|
||||
"privateAPI": {
|
||||
"tests": 1,
|
||||
"assertions": 25,
|
||||
"target": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"xml": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6g-investment-20261024\\api-tests.xml"
|
||||
},
|
||||
"types": "passed",
|
||||
"build": "passed,existing plugin timing warnings",
|
||||
"UI": "Real component in Edge/CUA:unknown/no/yes authorization triad,own fields,save/recover/report one row,5 prior versions;synthetic only,not manual Tempo acceptance",
|
||||
"UI_screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6g-investment-20261024\\qa-recovered-report.png"
|
||||
},
|
||||
"preservation": {
|
||||
"base": "0f9f7e3409bdea1d4d6c351a572e7c37b3a09b4e",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P048",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P050",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P052",
|
||||
"AUD-SIM-L1-P053",
|
||||
"AUD-SIM-L1-P054",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P080",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_delivery": "78c9f4a3/0f9f7e34;174 tables/369 files;29 published fiches/28 relations/408 geometries;code/schema/data/config/permissions compared at delivery",
|
||||
"frozen": "tests\\frontend\\fixtures\\iva-g6g-preserved-inputs.json",
|
||||
"sha256": "43b5060803730e940bc1afa5e75dac16e634da2cf3ea80ce71cab9ef6185e51d",
|
||||
"counts_before": {
|
||||
"delivered": 76,
|
||||
"QA": 0,
|
||||
"functional_pending": 56,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"scope": "P056 only; preserve P050 service VAT separately and P084/P085 preparation/conditional simulation without closing benefits; no active query/write"
|
||||
},
|
||||
"runtime_delta": {
|
||||
"resources/js/lib/iva-import-exemptions.ts": "1b4fda4e7e363e68f951eca14a415a93d08b06aaa49b41e639b44fc5c9cee488",
|
||||
"resources/js/components/simulation-import-exemptions.tsx": "c6f36defc756eac0bb21f17e7165bb84efbbbfd120ea3be83787ffe368b7f1b1"
|
||||
},
|
||||
"counts_current": {
|
||||
"delivered": 76,
|
||||
"QA": 1,
|
||||
"functional_pending": 55,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"homologation_unchanged": "78c9f4a3",
|
||||
"remaining": "Integrate/deliver by existingGit process with manifest,fresh backup/isolatedrestore and installeddelta;no push in this QA task",
|
||||
"limits": "P084/P085 partial only,pending;P00105/06;L1open;beveragesL2/L3;Stats/AIdeferred",
|
||||
"sources_inventory": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6g-investment-20261024\\source-archive-inventory.json",
|
||||
"qa_status_preserved": "QA_complete_not_delivered",
|
||||
"delivery_followup": {
|
||||
"served_commit": "9b26217dec945fe07b665c6ad7638a965bf104a9",
|
||||
"receipt": "l1-g6g-delivery-20261024.json",
|
||||
"record": "iva_l1_g6g_P056_homologation_20261024",
|
||||
"counts": {
|
||||
"delivered": 77,
|
||||
"QA": 0,
|
||||
"functional_pending": 55,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"implementation_commit": "69b6c49a600c02352fdb9d10b0f756dca6c500a5",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"AUD-SIM-L1-P060": [
|
||||
"un_development_exemption",
|
||||
"un_development_reduction"
|
||||
]
|
||||
},
|
||||
"branch_count": 1,
|
||||
"treatment_count": 2,
|
||||
"conditions": "Seven common statutory conditions and one own customs-relief condition per treatment;not16 treatments",
|
||||
"source": "CIVA12(2)(c),Lei13/2016PDF11/p1426(269),12(4)PDF12/p1426(270);Lei22/2022PDF2–3 andLei10/2025PDF13 maintain2/4 by remissão;2017rectification does notchange12(2)c/4",
|
||||
"source_links": [
|
||||
"https://www.parlamento.mz/?post_documents=boletim-da-republica-i-serie-nr-250-quarta-feira-28-de-dezembro-de-2022",
|
||||
"https://www.inm.gov.mz/pt-br/content/suplemento-n%C2%BA-10-de-301216-pag-1426-259-290-br-n%C2%BA-156-boletim-da-rep%C3%BAblica-i-serie"
|
||||
],
|
||||
"meaning": "Materials/equipment for development projects funded by UN agencies/specialized institutions accredited with MozambiqueGovernment;exclusive project implementation;favourableFinanceMinisterdecision following priorrequest/list/importplan and customsconformity with approvedlist/plan;request is not grant",
|
||||
"technical_choices": "Dedicated optional document references shown/required only for P060 classification;reuse importer,goods,document,date,period,evidence,opportunity;keep all answers across treatment switches. Reference fields trace statutory conditions,not newcertificate/approver or revalidation of officialsources. Exact decimal arithmetic via integers with up to6inputfractiondigits/14outputfractiondigits,no assumed fiscalrounding;negative/malformed/unknown inputs not zero. Newversion only books containingP060,oldkeys exactlycurrent.",
|
||||
"scope": "2026/MZN,classification of own import VAT and application of expressly supplied rights-relief proportion to expressly supplied normalVAT. No customsbase/duties/normalVAT-rate calculation;no deduction/field11/ModeloA totals/serviceVAT alteration. Not automatically extend decisions/list/plan scope.",
|
||||
"formula": "normalVAT × (100 − rightsReductionPercent)/100;partial percentage strictly between0and100;100requires ownfull-exemptiontreatment;0notapplicable;explicitnormalVAT0 distinctfrommissing",
|
||||
"result": "Full customs exemption plus ownconditions -> importVat0.00;partial reduction -> statusreduced and exactresult;missing=indeterminate,no=notapplicable,no presumedordinarytax. Effective-declared versus projected-assumptions explicit.",
|
||||
"verification": {
|
||||
"frontend": {
|
||||
"tests": 4,
|
||||
"passed": 4,
|
||||
"failed": 0,
|
||||
"scope": "Every owncondition missing/no;references and full/partial boundaries;exact fractions/large numbers/zero;malformedwriteguard;switch/context/referenceinvalidation;history;priorbooks/partialoffers exactlypreserved;report"
|
||||
},
|
||||
"privateAPI": {
|
||||
"tests": 1,
|
||||
"assertions": 34,
|
||||
"target": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"xml": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6h-development-20261024\\api-tests.xml"
|
||||
},
|
||||
"types": "passed",
|
||||
"build": "passed,existing plugin timing warnings",
|
||||
"UI": "Realcomponent Edge/CUA:decision unknown/no/assumed;normalVATmissing indeterminate;800with25%=600,50%=400;full0.00;save/recover/report2rows and5priorversions;scope/list/plan/decision/references retained. Synthetic only,not manualTempoacceptance.",
|
||||
"UI_screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6h-development-20261024\\qa-recovered-report.png"
|
||||
},
|
||||
"preservation": {
|
||||
"base": "163e090ea547d0c046bddecc0b4f5db150910d7d",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P048",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P050",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P052",
|
||||
"AUD-SIM-L1-P053",
|
||||
"AUD-SIM-L1-P054",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P056",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P080",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_delivery": "9b26217d/163e090e;174tables/369files;29published/28relations/408geometries;code/schema/data/config/permissions verified at delivery",
|
||||
"frozen": "tests\\frontend\\fixtures\\iva-g6h-preserved-inputs.json",
|
||||
"sha256": "0eec529d197507a74d63a046761604fa31d10d8908d4d27b79719a1d116524d8",
|
||||
"counts_before": {
|
||||
"delivered": 77,
|
||||
"QA": 0,
|
||||
"functional_pending": 55,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"scope": "P060 only;preserve P056 authorization scope,P050 serviceVAT,and partial P084/P085;no active query/write"
|
||||
},
|
||||
"runtime_delta": {
|
||||
"resources/js/lib/iva-import-exemptions.ts": "799e9a0020e805af9f67387bfa36608c4973f15c4427d1b82c3f6b6398aed129",
|
||||
"resources/js/components/simulation-import-exemptions.tsx": "89ab431fa6810c1f56897f203a817a41722c4a9df0cb075561dee6208e8730d1"
|
||||
},
|
||||
"counts_current": {
|
||||
"delivered": 77,
|
||||
"QA": 1,
|
||||
"functional_pending": 54,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"homologation_unchanged": "9b26217d",
|
||||
"remaining": "Integrate/deliver by existingGit with manifest,freshbackup/realisolatedrestore and installeddelta;no push in QA task",
|
||||
"limits": "P084/P085 partial,pending;P00105/06;L1open;beveragesL2/L3;Stats/AI deferred",
|
||||
"sources_inventory": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g6h-development-20261024\\source-archive-inventory.json",
|
||||
"qa_status_preserved": "QA_complete_not_delivered",
|
||||
"delivery_followup": {
|
||||
"served_commit": "eb9294ef42c2cfc97eb8c2553d545e3bff7c0add",
|
||||
"receipt": "l1-g6h-delivery-20261024.json",
|
||||
"record": "iva_l1_g6h_P060_homologation_20261024",
|
||||
"counts": {
|
||||
"delivered": 78,
|
||||
"QA": 0,
|
||||
"functional_pending": 54,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"implementation_commit": "67a96085c8828231bb7e44ca26a4acb7b915ecee",
|
||||
"status": "delivered_with_QA_proofs_preserved",
|
||||
"coverage": {
|
||||
"AUD-SIM-L1-P096": [
|
||||
"regulated_energy"
|
||||
],
|
||||
"AUD-SIM-L1-P097": [
|
||||
"aeronautical_fees"
|
||||
],
|
||||
"AUD-SIM-L1-P098": [
|
||||
"public_works"
|
||||
],
|
||||
"AUD-SIM-L1-P099": [
|
||||
"public_water"
|
||||
]
|
||||
},
|
||||
"branch_count": 4,
|
||||
"treatment_count": 4,
|
||||
"source": "CIVA15(2)j/k/l/m,15(1)/(5)/(6)/(9),Lei13/2016PDF13–14/p1426(271–272);Lei22/2022PDF3mantém15(2)j–m;Lei10/2025não altera15,adita18(6),PDF13:dedução do imposto suportado relativo a essas operações na mesma proporção da base reduzida.",
|
||||
"meaning": {
|
||||
"P096": {
|
||||
"treatment": "regulated_energy",
|
||||
"formula": "contraprestação total antes redução,sempróprioIVA ×62%",
|
||||
"conditions": "Transmissão de energia com preço fixado porAutoridadePública,referência/âmbito/intervalo aplicáveis."
|
||||
},
|
||||
"P097": {
|
||||
"treatment": "aeronautical_fees",
|
||||
"formula": "contraprestação total antes redução,sempróprioIVA ×85%",
|
||||
"conditions": "Preço doserviço fixado através de taxas aeronáuticas. Não aplicar a comunicações/saneamento/telecomunicações genericamente pelo título histórico da matriz;é necessário demonstrar o preço fixado através de taxa aeronáutica15(2)k."
|
||||
},
|
||||
"P098": {
|
||||
"treatment": "public_works",
|
||||
"formula": "contraprestação total antes redução,sempróprioIVA ×40%(redução60%,não base60%)",
|
||||
"conditions": "Construção/reabilitação de obras públicas abrangidas15(2)l/15(9),projecto/contrato/categoria/âmbito identificados;estradas,pontes,infraestrutura água/saneamento,electrificação rural emprojectos públicos e hidráulicaagrícola noâmbito definido. Não exigir preço regulado a esta alínea."
|
||||
},
|
||||
"P099": {
|
||||
"treatment": "public_water",
|
||||
"formula": "contraprestação total antes redução,sempróprioIVA ×75%",
|
||||
"conditions": "Água potável através derede pública,preço fixado porAutoridadePública comreferência/âmbito/intervalo aplicáveis."
|
||||
}
|
||||
},
|
||||
"formula": "Base=fullconsideration before reduction excludingownVAT ×branchpercentage once;exactdecimal retained,no fiscalrounding assumed.",
|
||||
"scope": "2026/MZN,base apenas. Percentagens62/85/40/75 são de base,não taxasIVA. Proporção18(6)registada sem calcular/conceder dedução/campo11;dedução permanece nas condições/lotes próprios. Sem taxaIVA,IVA,isenção ou totaisModeloA. Não dividir bruto comIVA sem composição esclarecida,nem reduzir montante já reduzido.",
|
||||
"technical_choices": "NewregulatedReducedBases opportunity-scoped book in existingmodelo_a snapshot/API/dossier,no schema/permissions. Switchretainshiddenprice/projectrefs andclearsownconditions;sharedconditions retained. Inputs15integer/6fractiondigits,BigInt×percentage,outputupto8fractiondigits. Existingrefs or explicitassumptions,no certificates/approvers invented.",
|
||||
"verification": {
|
||||
"frontend": {
|
||||
"tests": 6,
|
||||
"passed": 6,
|
||||
"failed": 0,
|
||||
"by_branch": {
|
||||
"AUD-SIM-L1-P096": "Ownconditions unknown/no,source/scope/date/currency,exactpercentage/base,zero vsunknown passed",
|
||||
"AUD-SIM-L1-P097": "Ownconditions unknown/no,source/scope/date/currency,exactpercentage/base,zero vsunknown passed",
|
||||
"AUD-SIM-L1-P098": "Ownconditions unknown/no,source/scope/date/currency,exactpercentage/base,zero vsunknown passed",
|
||||
"AUD-SIM-L1-P099": "Ownconditions unknown/no,source/scope/date/currency,exactpercentage/base,zero vsunknown passed"
|
||||
},
|
||||
"additional": "Eightfractiondigit exactresults from6fractioninput,no float/fiscalrounding;works40not60,pricefields irrelevanttoworks,project/categories guard,switchhiddenretention,allpreviousbooks/history/report/corruptwriteguards"
|
||||
},
|
||||
"privateAPI": {
|
||||
"tests": 1,
|
||||
"assertions": 27,
|
||||
"target": "simulator_fiscal_delta_20261021_test:55441",
|
||||
"transaction_rollback": true,
|
||||
"xml": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g7-reduced-bases-20261024\\api-tests.xml"
|
||||
},
|
||||
"types": "passed",
|
||||
"build": "passed,existing plugin timing warnings",
|
||||
"UI": "Edge/CUArealcomponent:unknown full-before-reduction composition makesenergybaseindeterminate;unknown workscategory makesworksbaseindeterminate;restore confirms620.155/850.2125/400.10/750.1875MZN,proportion62/85/40/75%,VAT/ratenull. Save/recover/report4rows+3previousversions. SyntheticQA,notmanualTempoacceptance.",
|
||||
"UI_screenshot": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g7-reduced-bases-20261024\\qa-recovered-report.png"
|
||||
},
|
||||
"preservation": {
|
||||
"base": "b0e01ef737d26a991474e7fcc5d77cd330f20815",
|
||||
"delivered_ids": [
|
||||
"AUD-SIM-L1-P003",
|
||||
"AUD-SIM-L1-P004",
|
||||
"AUD-SIM-L1-P005",
|
||||
"AUD-SIM-L1-P006",
|
||||
"AUD-SIM-L1-P007",
|
||||
"AUD-SIM-L1-P008",
|
||||
"AUD-SIM-L1-P009",
|
||||
"AUD-SIM-L1-P010",
|
||||
"AUD-SIM-L1-P011",
|
||||
"AUD-SIM-L1-P012",
|
||||
"AUD-SIM-L1-P013",
|
||||
"AUD-SIM-L1-P014",
|
||||
"AUD-SIM-L1-P015",
|
||||
"AUD-SIM-L1-P016",
|
||||
"AUD-SIM-L1-P017",
|
||||
"AUD-SIM-L1-P018",
|
||||
"AUD-SIM-L1-P019",
|
||||
"AUD-SIM-L1-P020",
|
||||
"AUD-SIM-L1-P021",
|
||||
"AUD-SIM-L1-P022",
|
||||
"AUD-SIM-L1-P023",
|
||||
"AUD-SIM-L1-P024",
|
||||
"AUD-SIM-L1-P025",
|
||||
"AUD-SIM-L1-P026",
|
||||
"AUD-SIM-L1-P027",
|
||||
"AUD-SIM-L1-P028",
|
||||
"AUD-SIM-L1-P029",
|
||||
"AUD-SIM-L1-P030",
|
||||
"AUD-SIM-L1-P031",
|
||||
"AUD-SIM-L1-P032",
|
||||
"AUD-SIM-L1-P033",
|
||||
"AUD-SIM-L1-P034",
|
||||
"AUD-SIM-L1-P035",
|
||||
"AUD-SIM-L1-P036",
|
||||
"AUD-SIM-L1-P037",
|
||||
"AUD-SIM-L1-P038",
|
||||
"AUD-SIM-L1-P039",
|
||||
"AUD-SIM-L1-P040",
|
||||
"AUD-SIM-L1-P041",
|
||||
"AUD-SIM-L1-P042",
|
||||
"AUD-SIM-L1-P043",
|
||||
"AUD-SIM-L1-P044",
|
||||
"AUD-SIM-L1-P045",
|
||||
"AUD-SIM-L1-P048",
|
||||
"AUD-SIM-L1-P049",
|
||||
"AUD-SIM-L1-P050",
|
||||
"AUD-SIM-L1-P051",
|
||||
"AUD-SIM-L1-P052",
|
||||
"AUD-SIM-L1-P053",
|
||||
"AUD-SIM-L1-P054",
|
||||
"AUD-SIM-L1-P055",
|
||||
"AUD-SIM-L1-P056",
|
||||
"AUD-SIM-L1-P057",
|
||||
"AUD-SIM-L1-P060",
|
||||
"AUD-SIM-L1-P061",
|
||||
"AUD-SIM-L1-P062",
|
||||
"AUD-SIM-L1-P063",
|
||||
"AUD-SIM-L1-P064",
|
||||
"AUD-SIM-L1-P065",
|
||||
"AUD-SIM-L1-P066",
|
||||
"AUD-SIM-L1-P067",
|
||||
"AUD-SIM-L1-P068",
|
||||
"AUD-SIM-L1-P069",
|
||||
"AUD-SIM-L1-P070",
|
||||
"AUD-SIM-L1-P071",
|
||||
"AUD-SIM-L1-P072",
|
||||
"AUD-SIM-L1-P073",
|
||||
"AUD-SIM-L1-P074",
|
||||
"AUD-SIM-L1-P075",
|
||||
"AUD-SIM-L1-P076",
|
||||
"AUD-SIM-L1-P077",
|
||||
"AUD-SIM-L1-P078",
|
||||
"AUD-SIM-L1-P079",
|
||||
"AUD-SIM-L1-P080",
|
||||
"AUD-SIM-L1-P081",
|
||||
"AUD-SIM-L1-P082",
|
||||
"AUD-SIM-L1-P083",
|
||||
"AUD-SIM-L1-P086",
|
||||
"AUD-SIM-L1-P089",
|
||||
"AUD-SIM-L1-P090",
|
||||
"AUD-SIM-L1-P091",
|
||||
"AUD-SIM-L1-P094",
|
||||
"AUD-SIM-L1-P095",
|
||||
"AUD-SIM-L1-P100",
|
||||
"AUD-SIM-L1-P101",
|
||||
"AUD-SIM-L1-P102",
|
||||
"AUD-SIM-L1-P103",
|
||||
"AUD-SIM-L1-P105",
|
||||
"AUD-SIM-L1-P106",
|
||||
"AUD-SIM-L1-P107",
|
||||
"AUD-SIM-L1-P108",
|
||||
"AUD-SIM-L1-P109",
|
||||
"AUD-SIM-L1-P110",
|
||||
"AUD-SIM-L1-P111",
|
||||
"AUD-SIM-L1-P112",
|
||||
"AUD-SIM-L1-P113",
|
||||
"AUD-SIM-L1-P114",
|
||||
"AUD-SIM-L1-P115",
|
||||
"AUD-SIM-L1-P116",
|
||||
"AUD-SIM-L1-P117",
|
||||
"AUD-SIM-L1-P118",
|
||||
"AUD-SIM-L1-P119",
|
||||
"AUD-SIM-L1-P120"
|
||||
],
|
||||
"confirmed_losses": [],
|
||||
"accepted_delivery": "2a725440/b0e01ef7;174tables/369files;29published/28relations/408geometries;code/schema/data/config/permissions compared",
|
||||
"frozen": "tests\\frontend\\fixtures\\iva-g7-reduced-preserved.json",
|
||||
"sha256": "0f106010eb54921462d64f42bf44ac551ef2ab614f6fe0eb063908d858529b01",
|
||||
"scope": "P096–P099QA;all priorbooks/history includingP111–P120 andpartialP084/P085 immutable;no activewrites"
|
||||
},
|
||||
"runtime_delta": {
|
||||
"resources/js/lib/iva-reduced-bases.ts": "4fc79388066a4d012b9af279ef53212ee963ae43d858307155e9ae8a140b5285",
|
||||
"resources/js/components/simulation-reduced-bases.tsx": "00f1e0f1490240f578401c3a6e10b6812ee5b7978bbf7d7e5c9c1a81124bc6d1",
|
||||
"resources/js/components/simulation-monthly-iva.tsx": "6032b9605703e7e29d5fb45cc158e8da57b9af4980d312f3326ddf18ee1e8814",
|
||||
"resources/js/lib/project-dossier.ts": "ab044a0952cc4753925768f5ba7fd422637cfb9d826c1e8c26d21d7616b6dacf"
|
||||
},
|
||||
"counts_current": {
|
||||
"delivered": 103,
|
||||
"QA": 4,
|
||||
"functional_pending": 25,
|
||||
"P001": 1,
|
||||
"total": 133
|
||||
},
|
||||
"homologation_unchanged": "2a725440",
|
||||
"remaining": "OnejointGit delivery forfourcompatiblebranches,onebackup/realisolatedrestore,perbranchQAproofsreused,installeddeltaonly. No individualbranch deployment.",
|
||||
"limits": "P121digital/P084/P085pending;L1open;P00105/06;beveragesL2/L3;Statistics/AIdeferred",
|
||||
"sources_inventory": "C:\\laragon\\www\\portal-investidor\\outputs\\iva-l1-g7-reduced-bases-20261024\\source-archive-inventory.json",
|
||||
"qa_status_preserved": "QA_complete_not_delivered",
|
||||
"delivery_followup": {
|
||||
"served_commit": "2fdca86367d8bad524113b4cf6c49a0110130be9",
|
||||
"receipt": "l1-g7-reduced-delivery-20261024.json",
|
||||
"record": "iva_l1_g7_reduced_P096_P099_homologation_20261024",
|
||||
"counts": {
|
||||
"delivered": 107,
|
||||
"QA": 0,
|
||||
"functional_pending": 25,
|
||||
"documentary": 1,
|
||||
"total": 133
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user