Compare commits

..
12 changed files with 597 additions and 2 deletions
@@ -1819,3 +1819,24 @@ Incluídos: consulta provincial nacional/corredores e detalhe territorial; pesqu
Conferidos o commit servido, APIs filtradas e assets instalados. Reutilizadas as provas QA aceites; a bateria obrigatória do pipeline não reabre a sua aceitação. Mantém-se pendente a conferência visual autenticada, incluindo navegação, ida/volta a recursos e Gurué no mesmo dossier. A recusa relatada em Turismo ocorreu após um botão; a falha concreta do CTA de cadastro foi corrigida, mas não se declara reproduzida e resolvida a acção original sem a sessão real. A escolha do formulário fiscal concreto ainda precisa de ser esclarecida; os diplomas IVA mostrados não são formulários preenchíveis.
Estado actual: `investor-usability-git-delivery-20261028.json`. Os relatórios QA anteriores conservam o estado histórico antes da instalação. As 435 observações de Gurué não são 435 empresas; as listas são internas, a cobertura nacional é parcial e a comparação sustenta hipóteses. L8 permanece parcial. Actualizados V2-01/V2-07/V2-10 na matriz e manual existentes.
## 28/10/2026 — preparação de formulários fiscais, inventário antes das provas
Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir legislação. Base preservada: entrega instalada 789fffd0, Modelo A e aquisições/apuramento já aceites, L6 M20/M22 instalado em 5aaf1a56 e incrementos de L8. Nenhuma perda confirmada neste recorte; não se apagam as aceitações nem se repetem provas fiscais anteriores. Fontes reutilizadas do inventário de 20 exemplares acessíveis em outputs/irpc-l6-values-forms/accessible-source-inventory.json da QA anterior, incluindo temporários identificados; inventário do corpus e originais preservados. M22: três páginas conferidas visualmente nesta revisão, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88. Modelo A: correspondência e divergências aceites em modelo-a-reconciliacao-20261021.md; sem nova validação de proveniência.
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
## 28/10/2026 — formulários fiscais 332c16eb, QA concluída e autenticação pendente
Documentos e Formulários disponibiliza no candidato entradas directas IVA/IRPC: identificação, período, 27 campos manuais de Modelo A ou 24 campos manuais de M22, revisão, impressão, ficheiro privado e recuperação com confirmação antes de substituir. Sem geração de zeros, arredondamento, apuramento ou submissão à AT. IRPC permanece parcial: outros ajustamentos, regimes e anexos não incluídos; L8 parcial. Persistência no servidor e associação automática ao dossier não incluídas. Cinco testes frontend e um teste Feature/20 asserções passaram na base QA isolada 127.0.0.1:55441/proactive_investment_20261027_test; tipos, compilação, sintaxe PHP e diff conferidos. Não equivalem a validação visual autenticada. Relatório: docs/investmoz/unification/fiscal-form-preparation-qa-20261028.json.
Candidato publicado apenas no ramo Git codex/fiscal-forms-qa-20261028; main e activo não alterados. Instalação anterior 789fffd0 preservada. Preparação de entrega em outputs/fiscal-forms-delivery-20261028, com guarda do destino, backup fresco, restauro isolado e pipeline existentes; execução remota depende de nova autenticação SSH, pois a sessão anterior foi encerrada.
## 28/10/2026 — formulários fiscais instalados, 332c16eb
A entrega Git autorizada instalou 332c16eb no portal de teste, depois de capacidade, guarda da versão anterior 789fffd0, backup fresco e restauro isolado conferidos. Disponível: Documentos e Formulários → Preencher IVA — Modelo A / Preparar IRPC — Modelo 22. Rotas públicas IVA/IRPC conferidas HTTP200 e com imposto pedido; rótulos dos acessos e preenchimento conferidos nos assets instalados. Estas provas não substituem a conferência visual autenticada, que permanece pendente.
Cobertura: identificação/período, 27 campos manuais IVA e 24 campos manuais M22, conferência, impressão e ficheiro privado para recuperar mediante confirmação. Sem cálculo, submissão ou assinatura AT; sem gravação no servidor nem associação automática ao dossier. Outros ajustamentos, regimes e anexos M22 não incluídos; L8 permanece parcial. Motores, permissões e provas fiscais anteriores preservados. As 176 tabelas de domínio e os 369 ficheiros/configuração foram comparados; apenas cache/sessões operacionais podem diferir. Base interna preservada, sem publicação: 361.464 observações, não empresas únicas; Gurué 435 observações, cobertura parcial. Relatório instalado: docs/investmoz/unification/fiscal-form-preparation-git-delivery-20261028.json. O relatório QA anterior mantém-se como histórico, não estado corrente de instalação.
@@ -0,0 +1,213 @@
{
"status": "installed_delta_verified_authenticated_visual_conference_pending",
"candidate": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"expected_installed": "789fffd098fa222c85e1283de07f23ee6f42bd04",
"target": "https://teste-investidor.tempo.co.mz/",
"coverage": {
"IVA": "27 manually editable Modelo A fields; source correspondence reused",
"IRPC": "24 manually editable M22 fields from Q10/Q11 general regime/Q12; partial preparation",
"flow": [
"identification and period",
"amounts",
"review",
"download private recovery file",
"explicit recovery confirmation",
"print"
],
"entry": "Documentos e Formulários → IVA or IRPC"
},
"limits": [
"No automatic calculations or eligibility validation",
"No AT submission or signature",
"No server persistence or automatic dossier link",
"Remaining M22 adjustments, other regimes and annexes not covered",
"Manual credit entries require parcel conference; existing calculation engines unchanged",
"L8 remains partial",
"Authenticated visual browser validation not demonstrated"
],
"preserved": [
"installed 789fffd0 features",
"accepted Modelo A and M20/M22 proofs",
"L8 accepted increments",
"all originals, data, roles and internal supply base; no active writes"
],
"QA": {
"frontend_tests": 5,
"feature_tests": 1,
"feature_assertions": 20,
"isolated_database": "proactive_investment_20261027_test",
"port": 55441,
"types": "passed",
"build": "passed",
"PHP_syntax": "passed",
"diff_check": "passed",
"source_visual_check": "M22 three existing page renders inspected; accepted IVA mapping reused"
},
"schema_changes": false,
"role_changes": false,
"new_publications": false,
"active_changes": "332c16eb installed through normal main Git pipeline",
"main_ancestor_verified": true,
"delivery_next_steps": [
"Authenticated visual conference of filling, review, download and recovery"
],
"source_code_sha256": {
"resources\\js\\pages\\portal\\fiscal-forms.tsx": "d27d16b174f6a9784b2ad0a2f6e46563d75192ebe4064d700b0df59e953317e0",
"resources\\js\\lib\\fiscal-form-preparation.ts": "776a563b4ebc1ddc6ceb14e2e8b1e6c9542e8cc9c17053811f6381d83bf8b3ec",
"routes\\web.php": "a25b4dccbf64b123117573ea31b83cfcc1ee642db9b0f95ed2f77387696879d9"
},
"backup": {
"stage": "/var/backups/investmoz-fiscal-forms-delivery-20261028/backup",
"created_at": "2026-10-28T20:07:20Z",
"target": {
"database": "teste_investidor",
"port": 5432
},
"tables": 176,
"files_verified": 369,
"dump_sha256": "341662f3c8f6e6c7b42d18aa468f3270d9d6a08bb908b7f52cbb1e132e5bd976",
"files_sha256": "c6b242850118003792b420f1e2706633489dc874bef664b2245dfbd5dbb50430",
"restore_verified": false
},
"restore": {
"restore_verified": true,
"tables_verified": 176,
"container_network": "none",
"database": "fiscal_forms_delivery_20261028_test",
"port": 55441,
"source_dump_sha256": "341662f3c8f6e6c7b42d18aa468f3270d9d6a08bb908b7f52cbb1e132e5bd976",
"active_database_writes": 0
},
"preservation": {
"navigation_asset_and_shared_props_verified": true,
"at": "2026-10-28T20:13:13.258269+00:00",
"served_commit": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"backup_dump_sha256": "341662f3c8f6e6c7b42d18aa468f3270d9d6a08bb908b7f52cbb1e132e5bd976",
"prior_public_tables_checked": 176,
"prior_domain_rows_preserved": true,
"new_migration_registry_rows": 0,
"operational_delta": {
"cache": {
"before": {
"rows": 119,
"checksum": "c98548e1038986975b95a959d469b828"
},
"after": {
"rows": 119,
"checksum": "5375b8b28b8af40c8c3ffc891975d6d5"
}
},
"sessions": {
"before": {
"rows": 25,
"checksum": "bcf0421fabf8473d5862941ec18e96af"
},
"after": {
"rows": 28,
"checksum": "78066be68c90956a3ac31a90bbeba676"
}
}
},
"persistent_files_and_configuration_identical": 369,
"private_catalogue": {
"observations": 361464,
"distinct_administrative_observations": 214299,
"preserved_repetitions": 147165,
"unmapped_territory": 205608
},
"preservation_identity": "postgres, matching backup reader; no actor impersonation",
"anonymous_query_status": 401,
"public_revision_confirmed": true,
"actual_team_UI_journey": "pending authenticated browser conference;accepted QA is not repeated",
"unique_company_count": null,
"publication": false,
"national_coverage": "partial",
"investment_conclusion": "hypothesis,not proved demand or viability",
"L8": "unchanged,partial"
},
"public_delta": {
"at_UTC": "2026-10-28T20:13:02.411009+00:00",
"candidate": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"public_pages": [
{
"tax": "iva",
"HTTP": 200,
"component": "portal/fiscal-forms",
"requested_form": "iva"
},
{
"tax": "irpc",
"HTTP": 200,
"component": "portal/fiscal-forms",
"requested_form": "irpc"
}
],
"assets": {
"fiscal-forms": {
"asset": "assets/fiscal-forms-CQflZEeP.js",
"sha256": "f559122ea47ac15d061ed4b2e008beeb0e9815626afd50315bd465ea6109bde0",
"expected_labels": 4
},
"services": {
"asset": "assets/services-DOFOHuLj.js",
"sha256": "4a9de7c6dd6d1d48adf55e47b6dd8561af2a53163f63eb14b0f8d9d135fd66d7",
"expected_labels": 2
}
},
"authenticated_visual_conference": "pending; connector availability not established",
"AT_submission": false,
"server_draft_persistence": false,
"L8": "partial"
},
"pipeline": {
"reports": [
{
"status": "already_installed",
"commit": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"requested_commits": [
"332c16ebb555e8d6b244fa519bc777d89c1a8b8f"
],
"finished_at": "2026-10-28T20:12:04.008171+00:00"
},
{
"status": "installed",
"commit": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"release": "/var/www/investmoz-homologacao/releases/332c16ebb555e8d6b244fa519bc777d89c1a8b8f-f0dfc218",
"previous_release": "/var/www/investmoz-homologacao/releases/789fffd098fa222c85e1283de07f23ee6f42bd04-29ab1f0d",
"database_migrations": 0,
"content_imports": 0,
"requested_commits": [
"332c16ebb555e8d6b244fa519bc777d89c1a8b8f"
],
"finished_at": "2026-10-28T20:12:02.675406+00:00"
}
],
"build_logs": [
{
"file": "/var/lib/investmoz-deploy/logs/1793218102227589181.log",
"sha256": "670358412945027ec26a2c7e2129a06b0af164aac26990db5d73c9aeb5ba9b6c",
"summaries": [
"\u001b[30;42mOK (1 test, 3106 assertions)\u001b[0m",
"✓ built in 32.91s",
"\u001b[30;42mOK (59 tests, 320 assertions)\u001b[0m",
"# tests 1176",
"# pass 1176",
"# fail 0"
]
}
]
},
"delivery_completed": [
"SSH authentication",
"capacity and baseline guard",
"fresh backup",
"isolated restore",
"normal Git main delivery",
"existing pipeline: 1176 frontend tests passed",
"public route/assets delta",
"176 table preservation comparison and 369 identical files/configuration"
],
"verification_notes": [
"Initial X-Inertia probe without matching asset version received protocol HTTP409. Normal initial page requests returned200; initial public page payload and installed assets verified. Failed probe receipt retained; no application change needed."
]
}
@@ -0,0 +1,63 @@
{
"status": "QA_passed_not_installed_SSH_authentication_required",
"candidate": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"expected_installed": "789fffd098fa222c85e1283de07f23ee6f42bd04",
"target": "https://teste-investidor.tempo.co.mz/",
"coverage": {
"IVA": "27 manually editable Modelo A fields; source correspondence reused",
"IRPC": "24 manually editable M22 fields from Q10/Q11 general regime/Q12; partial preparation",
"flow": [
"identification and period",
"amounts",
"review",
"download private recovery file",
"explicit recovery confirmation",
"print"
],
"entry": "Documentos e Formulários → IVA or IRPC"
},
"limits": [
"No automatic calculations or eligibility validation",
"No AT submission or signature",
"No server persistence or automatic dossier link",
"Remaining M22 adjustments, other regimes and annexes not covered",
"Manual credit entries require parcel conference; existing calculation engines unchanged",
"L8 remains partial",
"Authenticated visual browser validation not demonstrated"
],
"preserved": [
"installed 789fffd0 features",
"accepted Modelo A and M20/M22 proofs",
"L8 accepted increments",
"all originals, data, roles and internal supply base; no active writes"
],
"QA": {
"frontend_tests": 5,
"feature_tests": 1,
"feature_assertions": 20,
"isolated_database": "proactive_investment_20261027_test",
"port": 55441,
"types": "passed",
"build": "passed",
"PHP_syntax": "passed",
"diff_check": "passed",
"source_visual_check": "M22 three existing page renders inspected; accepted IVA mapping reused"
},
"schema_changes": false,
"role_changes": false,
"new_publications": false,
"active_changes": false,
"main_ancestor_verified": true,
"delivery_next_steps": [
"authenticate SSH using existing strict host-key bridge",
"recheck installed baseline, capacity and schema contract",
"fresh backup and isolated restoration",
"normal Git main delivery and existing guarded pipeline",
"installed route/assets verification and full preservation delta"
],
"source_code_sha256": {
"resources\\js\\pages\\portal\\fiscal-forms.tsx": "d27d16b174f6a9784b2ad0a2f6e46563d75192ebe4064d700b0df59e953317e0",
"resources\\js\\lib\\fiscal-form-preparation.ts": "776a563b4ebc1ddc6ceb14e2e8b1e6c9542e8cc9c17053811f6381d83bf8b3ec",
"routes\\web.php": "a25b4dccbf64b123117573ea31b83cfcc1ee642db9b0f95ed2f77387696879d9"
}
}
@@ -7377,3 +7377,24 @@ Incluídos: consulta provincial nacional/corredores e detalhe territorial; pesqu
Conferidos o commit servido, APIs filtradas e assets instalados. Reutilizadas as provas QA aceites; a bateria obrigatória do pipeline não reabre a sua aceitação. Mantém-se pendente a conferência visual autenticada, incluindo navegação, ida/volta a recursos e Gurué no mesmo dossier. A recusa relatada em Turismo ocorreu após um botão; a falha concreta do CTA de cadastro foi corrigida, mas não se declara reproduzida e resolvida a acção original sem a sessão real. A escolha do formulário fiscal concreto ainda precisa de ser esclarecida; os diplomas IVA mostrados não são formulários preenchíveis.
Estado actual: `investor-usability-git-delivery-20261028.json`. Os relatórios QA anteriores conservam o estado histórico antes da instalação. As 435 observações de Gurué não são 435 empresas; as listas são internas, a cobertura nacional é parcial e a comparação sustenta hipóteses. L8 permanece parcial. Actualizados V2-01/V2-07/V2-10 na matriz e manual existentes.
## 28/10/2026 — preparação de formulários fiscais, inventário antes das provas
Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir legislação. Base preservada: entrega instalada 789fffd0, Modelo A e aquisições/apuramento já aceites, L6 M20/M22 instalado em 5aaf1a56 e incrementos de L8. Nenhuma perda confirmada neste recorte; não se apagam as aceitações nem se repetem provas fiscais anteriores. Fontes reutilizadas do inventário de 20 exemplares acessíveis em outputs/irpc-l6-values-forms/accessible-source-inventory.json da QA anterior, incluindo temporários identificados; inventário do corpus e originais preservados. M22: três páginas conferidas visualmente nesta revisão, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88. Modelo A: correspondência e divergências aceites em modelo-a-reconciliacao-20261021.md; sem nova validação de proveniência.
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
## 28/10/2026 — formulários fiscais 332c16eb, QA concluída e autenticação pendente
Documentos e Formulários disponibiliza no candidato entradas directas IVA/IRPC: identificação, período, 27 campos manuais de Modelo A ou 24 campos manuais de M22, revisão, impressão, ficheiro privado e recuperação com confirmação antes de substituir. Sem geração de zeros, arredondamento, apuramento ou submissão à AT. IRPC permanece parcial: outros ajustamentos, regimes e anexos não incluídos; L8 parcial. Persistência no servidor e associação automática ao dossier não incluídas. Cinco testes frontend e um teste Feature/20 asserções passaram na base QA isolada 127.0.0.1:55441/proactive_investment_20261027_test; tipos, compilação, sintaxe PHP e diff conferidos. Não equivalem a validação visual autenticada. Relatório: docs/investmoz/unification/fiscal-form-preparation-qa-20261028.json.
Candidato publicado apenas no ramo Git codex/fiscal-forms-qa-20261028; main e activo não alterados. Instalação anterior 789fffd0 preservada. Preparação de entrega em outputs/fiscal-forms-delivery-20261028, com guarda do destino, backup fresco, restauro isolado e pipeline existentes; execução remota depende de nova autenticação SSH, pois a sessão anterior foi encerrada.
## 28/10/2026 — formulários fiscais instalados, 332c16eb
A entrega Git autorizada instalou 332c16eb no portal de teste, depois de capacidade, guarda da versão anterior 789fffd0, backup fresco e restauro isolado conferidos. Disponível: Documentos e Formulários → Preencher IVA — Modelo A / Preparar IRPC — Modelo 22. Rotas públicas IVA/IRPC conferidas HTTP200 e com imposto pedido; rótulos dos acessos e preenchimento conferidos nos assets instalados. Estas provas não substituem a conferência visual autenticada, que permanece pendente.
Cobertura: identificação/período, 27 campos manuais IVA e 24 campos manuais M22, conferência, impressão e ficheiro privado para recuperar mediante confirmação. Sem cálculo, submissão ou assinatura AT; sem gravação no servidor nem associação automática ao dossier. Outros ajustamentos, regimes e anexos M22 não incluídos; L8 permanece parcial. Motores, permissões e provas fiscais anteriores preservados. As 176 tabelas de domínio e os 369 ficheiros/configuração foram comparados; apenas cache/sessões operacionais podem diferir. Base interna preservada, sem publicação: 361.464 observações, não empresas únicas; Gurué 435 observações, cobertura parcial. Relatório instalado: docs/investmoz/unification/fiscal-form-preparation-git-delivery-20261028.json. O relatório QA anterior mantém-se como histórico, não estado corrente de instalação.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,66 @@
import { modeloAFields, modeloAReference } from './iva-modelo-a.ts';
export type FiscalFormKind = 'iva' | 'irpc';
export const fiscalFormVersion = 'investmoz-fiscal-preparation/1';
export const fiscalForms = {
iva: { title: 'IVA — Modelo A', source: modeloAReference.file, hash: modeloAReference.sha256, coverage: 'Campos 01–27 do exemplar, para preenchimento manual. A edição e as divergências já documentadas continuam identificadas.' },
irpc: { title: 'IRPC — Modelo 22', source: 'IRPC M-22- DECLARACAO DE RENDIMENTOS.pdf', hash: '4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88', coverage: 'Preparação parcial: resultado, lucro, matéria colectável e liquidação nos campos abaixo. Os restantes ajustamentos e anexos do exemplar não são preenchidos aqui. L8 permanece parcial.' },
} as const;
const irpcFields = [
['200', 'Resultado do exercício'], ['259', 'Prejuízo fiscal'], ['260', 'Lucro tributável'],
['269', 'Prejuízo fiscal — regime geral'], ['270', 'Lucro tributável — regime geral'],
['276', 'Prejuízos fiscais deduzidos'], ['277', 'Benefícios fiscais a deduzir'], ['278', 'Matéria colectável — regime geral'],
['300', 'Imposto à taxa normal'], ['301', 'Imposto à taxa reduzida'], ['302', 'Colecta total'],
['303', 'Dupla tributação económica'], ['304', 'Dupla tributação internacional'], ['305', 'Benefícios fiscais'],
['306', 'Pagamento especial por conta'], ['308', 'Total das deduções'], ['309', 'IRPC liquidado'],
['310', 'Retenções na fonte'], ['311', 'Pagamentos por conta'], ['312', 'IRPC a pagar antes de juros'],
['313', 'IRPC a recuperar antes de juros'], ['314', 'Juros compensatórios'], ['315', 'IRPC a pagar'], ['316', 'IRPC a recuperar'],
] as const;
export function fiscalFormFields(kind: FiscalFormKind) {
return kind === 'iva'
? modeloAFields.map(f => ({ code: f.field, label: f.purpose, guidance: f.basis }))
: irpcFields.map(([code, label]) => ({ code, label, guidance: Number(code) < 269 ? 'Quadro 10 do exemplar.' : Number(code) < 300 ? 'Quadro 11, regime geral. Outros regimes não incluídos nesta preparação.' : 'Quadro 12 do exemplar. Preenchimento manual, sem cálculo ou validação automática da liquidação.' }));
}
export type FiscalPreparation = {
version: typeof fiscalFormVersion;
kind: FiscalFormKind;
sourceHash: string;
name: string;
nuit: string;
period: string;
notes: string;
values: Record<string, string>;
};
export function newFiscalPreparation(kind: FiscalFormKind): FiscalPreparation {
return { version: fiscalFormVersion, kind, sourceHash: fiscalForms[kind].hash, name: '', nuit: '', period: '', notes: '', values: {} };
}
export function readFiscalPreparation(raw: string): FiscalPreparation {
if (raw.length > 100_000) throw new Error('O ficheiro excede o tamanho de uma preparação.');
let p: unknown;
try { p = JSON.parse(raw); } catch { throw new Error('Não foi possível ler este ficheiro de preparação. Os campos actuais foram preservados.'); }
if (!p || typeof p !== 'object' || Array.isArray(p)) throw new Error('Ficheiro de preparação inválido.');
const v = p as Record<string, unknown>;
if (v.version !== fiscalFormVersion || (v.kind !== 'iva' && v.kind !== 'irpc') || v.sourceHash !== fiscalForms[v.kind].hash) throw new Error('Versão ou exemplar diferente. A preparação actual foi preservada.');
for (const key of ['name', 'nuit', 'period', 'notes']) {
if (typeof v[key] !== 'string' || v[key].length > 5000) throw new Error('Identificação ou notas inválidas.');
}
if (!v.values || typeof v.values !== 'object' || Array.isArray(v.values)) throw new Error('Campos inválidos.');
const allowed = new Set(fiscalFormFields(v.kind).map(f => f.code));
const values: Record<string, string> = {};
for (const [code, value] of Object.entries(v.values)) {
if (!allowed.has(code) || typeof value !== 'string' || value.length > 100) throw new Error('Campo não reconhecido. A preparação actual foi preservada.');
values[code] = value;
}
return { ...newFiscalPreparation(v.kind), name: v.name as string, nuit: v.nuit as string, period: v.period as string, notes: v.notes as string, values };
}
export function fiscalPreparationIssues(p: FiscalPreparation): string[] {
const issues: string[] = [];
if (!p.name.trim()) issues.push('Indique o nome ou designação social.');
if (!/^\d{9}$/.test(p.nuit)) issues.push('Indique o NUIT com 9 algarismos.');
if (!(p.kind === 'iva' ? /^\d{4}-(0[1-9]|1[0-2])$/ : /^\d{4}$/).test(p.period)) issues.push(p.kind === 'iva' ? 'Indique ano e mês do período.' : 'Indique o ano do exercício.');
for (const f of fiscalFormFields(p.kind)) {
const value = p.values[f.code] ?? '';
if (value !== '' && !/^-?\d{1,24}(?:[.,]\d{1,2})?$/.test(value)) issues.push(`Campo ${f.code}: use um montante sem separador de milhares, com até duas casas decimais.`);
}
return issues;
}
+100
View File
@@ -0,0 +1,100 @@
import { Head, Link, router } from '@inertiajs/react';
import { useEffect, useState } from 'react';
import { PortalMenu } from '@/layouts/portal-navigation';
import { fiscalForms, fiscalFormFields, fiscalPreparationIssues, newFiscalPreparation, readFiscalPreparation, type FiscalFormKind, type FiscalPreparation } from '@/lib/fiscal-form-preparation';
const input = 'mt-1 block w-full rounded border border-slate-400 bg-white p-3 text-slate-900';
const button = 'rounded border border-emerald-800 px-4 py-3 font-semibold text-emerald-900 disabled:opacity-50';
export default function FiscalForms({ initialTax = 'iva' }: { initialTax?: FiscalFormKind }) {
const [kind, setKind] = useState<FiscalFormKind>(initialTax);
const [drafts, setDrafts] = useState({ iva: newFiscalPreparation('iva'), irpc: newFiscalPreparation('irpc') });
const [step, setStep] = useState(0);
const [notice, setNotice] = useState('');
const [pending, setPending] = useState<FiscalPreparation | null>(null);
const [dirty, setDirty] = useState(false);
const p = drafts[kind], source = fiscalForms[kind], fields = fiscalFormFields(kind);
const issues = fiscalPreparationIssues(p);
useEffect(() => {
setKind(initialTax);
}, [initialTax]);
useEffect(() => {
const warn = (e: BeforeUnloadEvent) => { if (dirty) { e.preventDefault(); e.returnValue = ''; } };
window.addEventListener('beforeunload', warn);
const unsubscribe = router.on('before', e => {
if (dirty && !window.confirm('Os campos estão apenas nesta página. Guarde o ficheiro para continuar depois. Pretende sair?')) e.preventDefault();
});
return () => { window.removeEventListener('beforeunload', warn); unsubscribe(); };
}, [dirty]);
function patch(update: Partial<FiscalPreparation>) {
setDrafts(d => ({ ...d, [kind]: { ...d[kind], ...update } }));
setDirty(true); setNotice('');
}
function download() {
const url = URL.createObjectURL(new Blob([JSON.stringify(p)], { type: 'application/json' }));
const a = document.createElement('a'); a.href = url; a.download = `investmoz-preparacao-${kind}.json`; a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
setNotice('Ficheiro de preparação gerado. Guarde-o para recuperar os campos. Não foi enviado ao portal nem à AT.');
// Other tax preparation may still contain unsaved edits.
}
return <div className="min-h-screen bg-slate-50 text-slate-900">
<Head title="Preencher formulário fiscal" />
<div className="print:hidden"><PortalMenu /></div>
<main className="mx-auto max-w-5xl space-y-6 px-5 py-8">
<div className="print:hidden"><Link href="/documentos" className="underline">Documentos e Formulários</Link></div>
<h1 className="text-3xl font-bold">Preencher formulário fiscal</h1>
<p>Prepare os valores de IVA ou IRPC, confira e guarde um ficheiro para continuar depois. Os campos vazios significam informação em falta.</p>
<p className="rounded border border-amber-600 bg-amber-50 p-3">Preparação para conferência, sem submissão à Autoridade Tributária. Os valores são introduzidos por si; não há apuramento automático nem confirmação de elegibilidade de deduções.</p>
<label className="block max-w-lg print:hidden">Formulário
<select className={input} value={kind} onChange={e => { setKind(e.target.value as FiscalFormKind); setStep(0); setNotice(''); setPending(null); }}>
<option value="iva">IVA — Modelo A</option><option value="irpc">IRPC — Modelo 22</option>
</select>
</label>
<h2 className="text-xl font-bold">{source.title}</h2>
<p>{source.coverage}</p>
<nav aria-label="Etapas de preenchimento" className="flex flex-wrap gap-2 print:hidden">
{['Identificação e período', 'Preencher valores', 'Conferir e guardar'].map((label, n) => <button type="button" key={label} className={button} aria-current={step === n ? 'step' : undefined} onClick={() => setStep(n)}>{n + 1}. {label}</button>)}
</nav>
<form onSubmit={e => e.preventDefault()} className="space-y-5">
<fieldset className={`${step !== 0 ? 'hidden print:block' : ''} space-y-4 rounded border bg-white p-5`}>
<legend className="font-bold">Identificação e período</legend>
<label className="block">Nome ou designação social<input className={input} autoComplete="organization" maxLength={5000} value={p.name} onChange={e => patch({ name: e.target.value })} /></label>
<div className="grid gap-4 sm:grid-cols-2">
<label>NUIT<input className={input} inputMode="numeric" maxLength={9} value={p.nuit} onChange={e => patch({ nuit: e.target.value })} /></label>
<label>{kind === 'iva' ? 'Período — ano e mês' : 'Exercício — ano'}<input className={input} type={kind === 'iva' ? 'month' : 'text'} inputMode={kind === 'irpc' ? 'numeric' : undefined} placeholder={kind === 'iva' ? 'AAAA-MM' : 'AAAA'} maxLength={7} value={p.period} onChange={e => patch({ period: e.target.value })} /></label>
</div>
<p className="text-sm">Dados mantidos apenas nesta página até descarregar o ficheiro. Não há gravação automática. Guarde antes de sair ou actualizar.</p>
</fieldset>
<fieldset className={`${step !== 1 ? 'hidden print:block' : ''} space-y-4 rounded border bg-white p-5`}>
<legend className="font-bold">Valores em meticais (MZN)</legend>
<p>Sem separador de milhares; vírgula ou ponto para os cêntimos. Não preencha zero quando desconhece o valor.</p>
{kind === 'irpc' && <p>Crédito económico (303) e internacional (304) são distintos. Não utilize a mesma parcela nos dois campos. Esta preparação manual não verifica as parcelas; conserve o suporte para conferência.</p>}
<div className="grid gap-5 sm:grid-cols-2">{fields.map(f => <div key={f.code} className="break-inside-avoid">
<label className="block">{f.code} — {f.label}<input className={input} inputMode="decimal" maxLength={100} value={p.values[f.code] ?? ''} onChange={e => patch({ values: { ...p.values, [f.code]: e.target.value } })} /></label>
<details className="mt-1 text-sm print:hidden"><summary className="cursor-pointer underline">Orientação do campo</summary><p>{f.guidance}</p></details>
</div>)}</div>
</fieldset>
<section className={`${step !== 2 ? 'hidden print:block' : ''} space-y-4 rounded border bg-white p-5`} aria-label="Conferir e guardar">
<h3 className="font-bold">Conferência da preparação</h3>
<p>{fields.filter(f => (p.values[f.code] ?? '') !== '').length} de {fields.length} campos com valor. Campos vazios não foram convertidos em zero.</p>
{issues.length > 0 ? <div><p>Por conferir — pode guardar a preparação incompleta:</p><ul className="list-disc pl-6">{issues.map(i => <li key={i}>{i}</li>)}</ul></div> : <p>Formato dos campos conferido. Os montantes e a aplicação fiscal ainda devem ser conferidos.</p>}
<dl className="grid gap-3 sm:grid-cols-2 print:hidden">{fields.filter(f => (p.values[f.code] ?? '') !== '').map(f => <div key={f.code}><dt>{f.code} — {f.label}</dt><dd className="font-semibold">{p.values[f.code]} MZN</dd></div>)}</dl>
<label className="block">Notas e referências dos valores<textarea className={input} rows={4} maxLength={5000} value={p.notes} onChange={e => patch({ notes: e.target.value })} /></label>
<div className="flex flex-wrap gap-3 print:hidden"><button type="button" className={button} onClick={download}>Guardar ficheiro para continuar</button><button type="button" className={button} onClick={() => window.print()}>Imprimir preparação</button></div>
<p className="text-sm">O ficheiro contém os dados introduzidos. Guarde-o num local privado. Não fica associado automaticamente ao dossier.</p>
</section>
<div className="flex justify-between gap-3 print:hidden"><button type="button" className={button} disabled={step === 0} onClick={() => setStep(step - 1)}>Anterior</button><button type="button" className={button} disabled={step === 2} onClick={() => setStep(step + 1)}>Continuar</button></div>
</form>
<section className="space-y-3 rounded border p-4 print:hidden" aria-label="Recuperar preparação">
<label className="block">Recuperar ficheiro guardado<input className={input} type="file" accept=".json,application/json" onChange={async e => {
const file = e.target.files?.[0]; e.target.value = ''; if (!file) return;
try { if (file.size > 100_000) throw new Error('O ficheiro é demasiado grande.'); setPending(readFiscalPreparation(await file.text())); setNotice('Ficheiro lido. Confirme a recuperação abaixo.'); }
catch (error) { setPending(null); setNotice(error instanceof Error ? error.message : 'Não foi possível ler o ficheiro. Os campos actuais foram preservados.'); }
}} /></label>
{pending && <div className="space-y-2"><p>Recuperar {fiscalForms[pending.kind].title}, período {pending.period || 'por indicar'}? Substitui apenas os campos desse formulário nesta página.</p><button type="button" className={button} onClick={() => { setDrafts(d => ({ ...d, [pending.kind]: pending })); setKind(pending.kind); setStep(2); setPending(null); setDirty(true); setNotice('Preparação recuperada.'); }}>Confirmar recuperação</button> <button type="button" className={button} onClick={() => setPending(null)}>Cancelar</button></div>}
</section>
{notice && <p role="status" className="rounded border p-3">{notice}</p>}
<details className="rounded border p-4"><summary className="cursor-pointer font-semibold">Exemplar e cobertura</summary><p>{source.source}</p><p className="break-all text-sm">SHA256: {source.hash}</p><p>{source.coverage}</p><p>Os cálculos e correspondências já disponíveis no simulador permanecem separados desta introdução manual. Não se executam fórmulas nem se transportam valores entre edições automaticamente.</p></details>
<div className="flex flex-wrap gap-4 print:hidden"><Link href="/simulador-fiscal" className="underline">Consultar cálculos e preparação M20/M22 no simulador</Link><Link href="/legislacao" className="underline">Consultar legislação</Link></div>
</main>
</div>;
}
+9
View File
@@ -538,6 +538,15 @@ export default function Services(p: Props) {
opportunities={p.opportunities}
/>
)}
{p.section === 'documents' && <section aria-label="Preencher formulários fiscais" className="space-y-3 rounded-xl border bg-white p-5">
<h2 className="text-xl font-bold">Que formulário pretende preencher?</h2>
<p>Abra os campos do formulário fiscal. A legislação e os guias de consulta ficam abaixo.</p>
<div className="flex flex-wrap gap-3">
<Link className={button} href="/formularios/fiscais?imposto=iva">Preencher IVA — Modelo A</Link>
<Link className={button} href="/formularios/fiscais?imposto=irpc">Preparar IRPC — Modelo 22</Link>
</div>
<p className="text-sm">Preparação manual, com gravação em ficheiro. Cobertura parcial de IRPC; sem submissão à AT.</p>
</section>}
{['procedures', 'documents', 'legislation'].includes(
p.section,
) && <JourneyDocumentResources stages={p.stages} />}
+5
View File
@@ -50,6 +50,11 @@ use Laravel\Fortify\Features;
// Route::inertia('/', 'welcome')->name('home');
Route::get('/', [PortalServicesController::class, 'home'])->name('home');
Route::inertia('/contextos-de-investimento', 'portal/investment-contexts')->name('portal.investment-contexts');
Route::get('/formularios/fiscais', function (\Illuminate\Http\Request $request) {
$input = $request->validate(['imposto' => ['nullable', 'in:iva,irpc']]);
return Inertia::render('portal/fiscal-forms', ['initialTax' => $input['imposto'] ?? 'iva']);
})->name('portal.fiscal-forms');
Route::inertia('/politica-de-privacidade', 'portal/privacy-policy')->name('legal.privacy');
Route::inertia('/termos-de-uso', 'portal/terms-of-use')->name('legal.terms');
foreach (['sectores' => 'sectors', 'como-investir' => 'procedures', 'legislacao' => 'legislation', 'documentos' => 'documents',
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
final class FiscalFormEntryTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
\Tests\Support\ProactiveInvestmentFixture::guard();
$this->withoutVite();
}
public function test_both_fiscal_forms_are_public_and_do_not_redirect_to_legal_documents(): void
{
config(['portal.homologation_only' => false]);
foreach (['iva', 'irpc'] as $tax) {
$this->get('/formularios/fiscais?imposto='.$tax)
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('portal/fiscal-forms')->where('initialTax', $tax));
}
}
}
+34
View File
@@ -0,0 +1,34 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import ts from 'typescript';
import * as forms from '../../resources/js/lib/fiscal-form-preparation.ts';
const require=createRequire(import.meta.url);
const source=fs.readFileSync(new URL('../../resources/js/pages/portal/fiscal-forms.tsx',import.meta.url),'utf8');
const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
const module={exports:{}};
new Function('require','module','exports',compiled)(name=>name==='@inertiajs/react'?{Head:()=>null,Link:({href,children,...props})=>React.createElement('a',{href,...props},children),router:{on:()=>()=>{}}}:name==='@/layouts/portal-navigation'?{PortalMenu:()=>null}:name==='@/lib/fiscal-form-preparation'?forms:require(name),module,module.exports);
test('fiscal entry renders labelled editable fields and a guided preparation rather than legislation',()=>{
const html=renderToStaticMarkup(React.createElement(module.exports.default));
for(const label of ['Identificação e período','Nome ou designação social','NUIT','Período — ano e mês','Conferir e guardar','Guardar ficheiro para continuar','Recuperar ficheiro guardado','Confirmar recuperação']) {
// Confirmation appears only after a compatible file is selected.
if(label !== 'Confirmar recuperação') assert.ok(html.includes(label),label);
}
assert.equal((html.match(/inputMode="decimal"/g)??[]).length,27);
assert.match(html,/sem submissão à Autoridade Tributária/);
assert.match(html,/Não há gravação automática/);
assert.doesNotMatch(html,/value="0"/);
});
test('IRPC entry opens the requested form directly and keeps its partial coverage explicit',()=>{
const html=renderToStaticMarkup(React.createElement(module.exports.default,{initialTax:'irpc'}));
assert.match(html,/IRPC — Modelo 22/);
assert.match(html,/L8 permanece parcial/);
assert.match(html,/303 — Dupla tributação económica/);
assert.match(html,/304 — Dupla tributação internacional/);
assert.match(html,/Não utilize a mesma parcela/);
assert.equal((html.match(/inputMode="decimal"/g)??[]).length,24);
assert.doesNotMatch(html,/01 — Base das operações/);
});
@@ -0,0 +1,34 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {newFiscalPreparation, readFiscalPreparation, fiscalPreparationIssues, fiscalFormFields} from '../../resources/js/lib/fiscal-form-preparation.ts';
test('IVA and IRPC preparations round trip deliberate entries and unknown amounts without zero defaults', () => {
for (const kind of ['iva', 'irpc']) {
const p = newFiscalPreparation(kind);
p.name = 'Investidor de teste'; p.nuit = '123456789'; p.period = kind === 'iva' ? '2026-10' : '2026';
const [first, second] = fiscalFormFields(kind);
p.values[first.code] = '123456789012345678901234,01';
p.values[second.code] = '';
const restored = readFiscalPreparation(JSON.stringify(p));
assert.deepEqual(restored, p);
assert.deepEqual(fiscalPreparationIssues(restored), []);
assert.equal(restored.values[second.code], '');
assert.equal(Object.values(restored.values).includes('0'), false);
}
});
test('foreign editions, unknown fields and malformed or oversized files cannot replace preparation', () => {
const p = newFiscalPreparation('iva');
for (const v of [{...p, sourceHash: 'different'}, {...p, values: {'999': '1'}}, {...p, values: []}, {...p, nuit: 123}, {...p, version: 'later'}]) assert.throws(() => readFiscalPreparation(JSON.stringify(v)));
assert.throws(() => readFiscalPreparation('{broken'));
assert.throws(() => readFiscalPreparation('x'.repeat(100001)));
assert.equal(p.kind, 'iva'); assert.deepEqual(p.values, {});
});
test('invalid format is identified without rounding or discarding an incomplete preparation', () => {
const p = newFiscalPreparation('irpc');
p.values['303'] = '1.234'; p.values['304'] = '25,00';
const restored = readFiscalPreparation(JSON.stringify(p));
assert.equal(restored.values['303'], '1.234');
assert.ok(fiscalPreparationIssues(restored).some(i => i.startsWith('Campo 303:')));
assert.equal(fiscalFormFields('irpc').find(f => f.code === '303').label, 'Dupla tributação económica');
assert.equal(fiscalFormFields('irpc').find(f => f.code === '304').label, 'Dupla tributação internacional');
});