Integrate authorised editorial package with current homologation changes

This commit is contained in:
2026-11-14 09:11:55 +02:00
30 changed files with 1787 additions and 65 deletions
+1
View File
@@ -58,6 +58,7 @@ final class InvestmentMapIndicators
'zone_id' => $zone->id, 'zone_name' => $zone->name, 'zone_version' => (int) $zone->version_no, 'url' => '/zonas/'.$zone->id,
'theme' => $row['indicator'] === 'employment' ? 'workforce' : 'economic_potential',
'value' => $value, 'knowledge' => $knowledge, 'missing_metadata' => $missing,
'measure_kind' => in_array($row['measure_kind'] ?? null, ['observed', 'planned', 'projected'], true) ? $row['measure_kind'] : 'unknown',
];
}
}
@@ -18,7 +18,7 @@ class InvestmentZoneIntegralDetails
'phases' => ['code', 'status', 'starts_at', 'ends_at', 'expansion'],
'plots' => ['code', 'phase_id', 'area', 'area_unit', 'status', 'geometry_id'],
'occupants' => ['organization_id', 'documentary_name', 'phase_id', 'plot_id', 'status', 'valid_from', 'valid_until'],
'observations' => ['indicator', 'value', 'unit', 'period_start', 'period_end', 'population', 'methodology', 'phase_id', 'plot_id'],
'observations' => ['indicator', 'measure_kind', 'value', 'unit', 'period_start', 'period_end', 'population', 'methodology', 'phase_id', 'plot_id'],
'chains' => ['kind', 'organization_id', 'documentary_name', 'zone_link_id', 'phase_id', 'plot_id'],
];
@@ -59,6 +59,7 @@ class InvestmentZoneIntegralDetails
$rules['titles.*.kind'] = 'nullable|in:creation,land_use,licence,special_regime,other';
$rules['chains.*.kind'] = 'nullable|in:anchor,supplier,unmet_need,other';
$rules['observations.*.indicator'] = 'required|in:employment,investment,production,exports,local_procurement,resource_use,waste,emissions,other';
$rules['observations.*.measure_kind'] = 'nullable|in:unknown,observed,planned,projected';
$data = Validator::make($input, $rules)->validate();
$result = $before;
foreach (self::GROUPS as $group) {
@@ -148,7 +149,7 @@ class InvestmentZoneIntegralDetails
foreach ($result['observations'] as $item) {
if (! isset($item['period_start'],$item['period_end'])) {
continue;
}$key = implode('|', [$item['indicator'], $item['period_start'], $item['period_end'], $item['scope'] ?? '', $item['population'] ?? '', $item['unit'] ?? '', $item['methodology'] ?? '', $item['source_reference'] ?? '', $item['source_date'] ?? '', $item['phase_id'] ?? '', $item['plot_id'] ?? '']);
}$key = implode('|', [$item['indicator'], $item['measure_kind'] ?? 'unknown', $item['period_start'], $item['period_end'], $item['scope'] ?? '', $item['population'] ?? '', $item['unit'] ?? '', $item['methodology'] ?? '', $item['source_reference'] ?? '', $item['source_date'] ?? '', $item['phase_id'] ?? '', $item['plot_id'] ?? '']);
abort_if(in_array($key, $keys, true), 422, 'A mesma observação/fonte já está registada; altere o detalhe existente para criar a sua nova revisão.');
$keys[] = $key;
}
@@ -9872,3 +9872,135 @@ Prova integrada: consulta das 19 etapas e 38 descargas reais pelo Edge (19 Word/
Validação nova, limitada ao delta: dois testes frontend (incluem exportação das 19 etapas e escape das limitações), tipos, lint dos três ficheiros de produto afectados e compilação QA. Não foram repetidas as provas aceites de APIEX/SWOT, cálculo fiscal, permissões ou instalação. O perfil antigo de QA referia contas que já não existem no cluster actualmente ligado; foi localizado e usado o perfil documentado da demografia, sem criar permissões ou reiniciar o cluster. A serialização da chave MFA da conta recém-criada foi corrigida apenas nessa conta sintética.
Estado: correcção implementada e conferida em QA, ainda sem instalação. A homologação mantém `07182c7c` para o recorte de relatórios anteriormente instalado. A prova demonstra resumos de preparação por etapa; não declara produzidos estudos financeiros, licenças, decisões, pacotes APIEX ou documentos que não existam nos respectivos percursos. A instalação deste novo delta é a pendência de entrega; os saldos próprios dos outros módulos mantêm-se.
## 13/11/2026 — resumos por etapa instalados na homologação em 8854b292
Autorização expressa do proprietário recebida e executada. Revisão servida `8854b2928c6907b6742a1eb622ae70e6b6a9e998`, construída em 2026-11-13T13:16:06Z; troca concluída em 13:16:09Z. Entrada habitual: `https://teste-investidor.tempo.co.mz/investimentos`, abrir o dossier e a secção de relatórios, seleccionar a etapa, consultar o resumo, preparar Word ou HTML e descarregar o ficheiro pronto. O relatório consolidado permanece disponível.
Backup/restauro fresco: 193 tabelas/409 ficheiros, conferidos por contagens e hashes. Verificação pós-entrega: quatro ficheiros por SHA256; 18 tabelas protegidas inalteradas, incluindo instituições, dossiers, rascunhos, documentos, jornadas, etapas e as três tabelas demográficas. Mantidos 12 lotes aprovados/920456 observações. Sem migrações, importações, publicação ou alterações de permissões. Preparação demográfica e os controlos autenticados foram preservados e a readiness foi vinculada à revisão instalada, mantendo o exemplar anterior arquivado.
O executor Git existente e os seus controlos obrigatórios de CI foram reutilizados sem alteração de fonte ou da configuração partilhada. A entrega ficou fixada na branch aprovada `entrega/relatorios-etapas-20261113`, pois a main já continha alterações alheias de pesquisa/suporte com integração falhada. Essas alterações e a main não foram substituídas nem instaladas por esta autorização.
Prova pós-instalação no Edge, na sessão autorizada já existente: dossier sintético `db85f087-0737-4164-892a-4fa4fe1313df`, resumo S01 e fundamento guardado recuperados; Word e HTML efectivamente descarregados e conferidos. Nenhum dossier foi alterado nesta conferência. Não se repetiram as 19 consultas/38 descargas aceites em QA; o CI obrigatório do executor correu normalmente. Recibo: `outputs/dossier-reports-20261113/stage-homologation-installed.json`; captura `visual/06-homologacao-S01-descarga.jpg` no mesmo directório.
Estado deste delta: instalado e conferido. Substitui a pendência de instalação do registo anterior, preservando os seus limites: são resumos de preparação dos registos disponíveis; não se declaram produzidas licenças, decisões, estudos ou outros originais ainda em falta nos respectivos percursos. O pacote QA selado `outputs/entrega-relatorios-etapas-20261113.zip` permanece inalterado; o recibo posterior comprova a instalação.
## 13/11/2026 — ACT-02: confronto de observações quantitativas, QA
Primeiro recorte executável da continuidade documentada de ACT-20261029-02: a comparação das observações públicas já recebidas no mapa. Reutiliza `InvestmentMapIndicators` e os filtros existentes; não acrescenta API, cadastro, publicação, fonte externa ou acesso a dados privados. Duas selecções progressivas mostram território, descrição/natureza da grandeza, indicador, valor textual exacto, unidade, período, âmbito, população/base, metodologia, situação, fonte/data e versão.
A conferência assinala diferenças de território/indicador/unidade/âmbito/base/metodologia, campos em falta (incluindo descrição/natureza), datas inválidas, escolha repetida e informação desconhecida/em divergência. Metadados alinhados não são certificação de comparabilidade. Duração, sobreposição dos períodos e natureza prevista/efectiva exigem leitura da fonte. Não soma, calcula evolução, converte grandezas, cria pontuação ou índice regional. Zero, precisão decimal e fontes originais são preservados.
Selecções identificadas por território, observação e versão. Fora dos filtros deixam de entrar na comparação; mudar a versão exige nova selecção. Conferência no Edge com três observações inteiramente sintéticas, sem API/escritas na base, exercitando o componente real compilado. Capturas e recibo em `outputs/map-comparison-20261113/`. Três testes novos dirigidos ao delta, tipos, lint e compilação QA aprovados; os testes aceites dos filtros anteriores não foram repetidos.
Estado: implementado e conferido em QA, sem instalação. V2-07/V2-10 e ACT-02 permanecem parciais: cobertura factual nacional/regional, séries estatisticamente comparáveis, oferta laboral, natureza estruturada das grandezas e metodologia de potencial económico continuam com o seu saldo próprio. Este incremento encerra apenas a ferramenta de confronto das observações recebidas. Não altera dossiers, relatórios de 8854b292, pesquisa ou suporte aceites. A candidata incorpora também o recibo documental de 8854b292, sem repetir a instalação ou as provas dessa entrega.
## 13/11/2026 — ACT-02: comparação de observações instalada em 1263b4f6
Instalação expressamente autorizada e concluída pelo executor Git existente na main, em `https://teste-investidor.tempo.co.mz`. A revisão servida é `1263b4f6b7609c3d4d293c2b9f8a68abb9d3e3c4`. No mapa das oportunidades, o bloco de indicadores apresenta «Comparar duas observações» quando há pelo menos duas observações públicas nos filtros actuais. A comparação mantém valores, descrições, unidades, períodos, âmbito, base, metodologia, situação, fontes e versões; não infere evolução, conversão, elegibilidade ou potencial regional.
Backup/restauro fresco de 193 tabelas/410 ficheiros, com contagens e hashes conferidos; verificação pós-entrega de 21 tabelas protegidas sem diferenças, incluindo dados territoriais, instituições, dossiers, jornadas, etapas, documentos, permissões e demografia. Mantidos 12 lotes aprovados/920456 observações. Fontes entregues verificadas por SHA256; configuração/fonte do executor preservadas. Sem migrações, importações, publicação de conteúdo ou novas permissões. A readiness demográfica foi vinculada à revisão, com o exemplar anterior arquivado.
Reutilizadas as provas aceites: três testes novos e a conferência visual sintética do componente; o CI obrigatório do executor correu normalmente, sem nova ronda manual dos percursos aceites. HTTPS do mapa, instituições, demografia e entrada privada conferido. Os originais e a pesquisa, suporte e relatórios de 8854b292 foram preservados. Não foram criadas observações sintéticas na homologação para fazer aparecer a comparação. Recibo: `outputs/map-comparison-20261113/homologation-installed.json`; pacote QA selado inalterado.
Estado do incremento: instalado. ACT-02/V2-07/V2-10 permanecem parciais quanto à cobertura factual, séries estatisticamente comparáveis, oferta laboral, natureza estruturada das grandezas e metodologia económica. Esta entrega fecha a ferramenta de confronto das observações recebidas, não esses requisitos amplos.
## 13/11/2026 — ACT-02: natureza estruturada das observações, QA
Continuidade do confronto instalado em 1263b4f6. O editor existente permite declarar segundo a fonte se o valor é observado/efectivo, planeado/meta, projectado/estimado ou por confirmar. A classificação segue o envelope privado, a recuperação e o fluxo editorial existentes; preparar uma alteração não modifica a versão pública. Omissão por clientes anteriores preserva uma escolha deliberada. Sem indicação anterior, a consulta apresenta por confirmar; não infere natureza pela descrição nem reescreve registos. Fonte, valor textual exacto, zero, identidade, versão, confidencialidade e histórico mantidos. Não há migrações, novas permissões, publicação automática ou importação.
A consulta e a comparação mostram a natureza separada da situação documental; naturezas diferentes ou desconhecidas exigem conferência. Metadados alinhados não provam comparabilidade estatística. QA: três testes backend/21 asserções, cinco testes frontend afectados, análise PHP, Pint, tipos, lint e compilação aprovados. Edge: componentes reais compilados com dados sintéticos; selector e edição preservados, observado/projectado distintos, legado por confirmar, zero/precisão mantidos. Provas em outputs/observation-nature-20261113/. Reutilizadas as provas aceites dos restantes percursos.
Estado: implementado em QA, instalação pendente de autorização concreta; homologação mantém 1263b4f6. Encerra a capacidade estruturada de declarar a natureza, não a classificação factual de todo o acervo. ACT-02/V2-07/V2-10 continuam parciais: cobertura factual nacional/regional, séries estatisticamente comparáveis, oferta laboral, conferência documental dos valores antigos e metodologia de potencial económico. Relatórios, pesquisa, suporte e entregas aceites preservados.
## 13/11/2026 — ACT-02: natureza das observações instalada em 42b9324a
Instalação expressamente autorizada e concluída pelo executor Git existente na main, em `https://teste-investidor.tempo.co.mz`. A revisão servida é `42b9324acbf0a15418424e8a539a0740932b2377`. O editor existente permite declarar observado/efectivo, planeado/meta, projectado/estimado ou por confirmar; a escolha segue a preparação privada e o fluxo editorial existente. Consulta e comparação distinguem natureza e situação documental, sem inferir a classificação dos registos antigos ou reescrever o acervo.
Backup/restauro fresco de 193 tabelas/411 ficheiros, com contagens e hashes conferidos; verificação pós-entrega de 21 tabelas protegidas sem diferenças, incluindo dados territoriais, instituições, dossiers, jornadas, etapas, documentos, permissões e demografia. Mantidos 12 lotes aprovados/920456 observações. Fontes entregues verificadas por SHA256; configuração/fonte do executor preservadas. Sem migrações, importações, publicação de conteúdo ou novas permissões. A readiness demográfica foi vinculada à revisão, com o exemplar anterior arquivado.
Reutilizadas as provas aceites: três testes backend/21 asserções, cinco frontend afectados, análise/tipos/lint/build e conferência visual sintética do componente; o CI obrigatório do executor correu normalmente, sem nova ronda manual dos percursos aceites. HTTPS do mapa, instituições, demografia e entrada privada conferido. Os originais e a pesquisa, suporte e relatórios de 8854b292 foram preservados. Não foram criadas observações sintéticas na homologação para fazer aparecer a comparação. Recibo: `outputs/observation-nature-20261113/homologation-installed.json`; pacote QA selado inalterado.
Estado do incremento: instalado. ACT-02/V2-07/V2-10 permanecem parciais quanto à cobertura factual, séries estatisticamente comparáveis, oferta laboral, classificação factual do legado e metodologia económica. Esta entrega fecha a capacidade de declarar/recuperar/consultar a natureza das observações, não esses requisitos amplos.
## 13/11/2026 — ACT-02: conferência temporal da comparação, QA
Continuidade após a natureza das observações instalada em 42b9324a. A comparação existente mostra a duração dos períodos de referência em dias de calendário, com ambos os limites incluídos. Durações diferentes exigem conferência; períodos distintos com sobreposição, incluindo um único dia comum, mostram aviso próprio. Datas incompletas, inválidas ou invertidas continuam por confirmar, sem duração inferida da data da fonte. Não anualiza, calcula taxas/evolução, soma observações ou declara comparabilidade estatística. Valores textuais, zeros, fontes, versões, filtros e âmbito territorial preservados.
QA: dois testes novos de duração/ano bissexto/sobreposição/limites e datas inválidas; sete testes afectados passaram. Tipos, lint e compilação aprovados. Edge com componente real compilado e dados sintéticos: duração inclusiva de 366/365 dias e aviso de sobreposição de um dia conferidos. Sem API, base de dados, migração, importação ou publicação. Provas: outputs/observation-period-20261113/. Provas aceites dos restantes percursos reutilizadas.
Estado: implementado em QA, sem instalação. ACT-02 permanece parcial: cobertura factual, classificação documental do legado, séries estatisticamente comparáveis, oferta laboral e metodologia económica conservam o saldo próprio. Esta entrega encerra a conferência automática de duração e sobreposição das datas recebidas, não a comparabilidade factual das séries.
## 13/11/2026 — ACT-02: comparação temporal instalada em 2bad72c0
Instalação autorizada e concluída na homologação https://teste-investidor.tempo.co.mz pelo executor Git existente. Revisão servida `2bad72c042f1a779849bfacc8a73ccd842b27fe7`. A comparação de observações mostra duração em dias com limites incluídos e assinala durações diferentes e sobreposição; não infere taxas, evolução ou comparabilidade estatística.
Backup/restauro fresco de193tabelas/412ficheiros;15fontes verificadas por hash;21tabelas protegidas sem diferenças. Mantidos12lotes/920456observações demográficas, instituições, dossiers, relatórios, pesquisa, suporte e permissões. Sem migrações/importações ou publicação de dados. Readiness demográfica vinculada à revisão com original arquivado. Configuração e fonte do executor inalteradas. HTTPS e entrada privada conferidos. CI obrigatório executado normalmente; provas anteriores reutilizadas sem nova ronda manual. Recibo: outputs/observation-period-20261113/homologation-installed.json; pacote QA selado preservado.
Este delta deixa de ser pendência de instalação; ACT-02 continua parcial quanto à cobertura factual, classificação do legado, oferta laboral, séries estatisticamente comparáveis e metodologia económica. A ferramenta não cria séries factuais por si.
## 13/11/2026 — lote PEC ordinário: último montante e PPC acima da base, QA
Conforme a directiva de agrupar melhorias relacionadas, este lote reúne dois resíduos do percurso de viabilidade fiscal, sem instalação por cada correcção. Reutiliza a preparação PEC, livro anual, rascunho privado, histórico e relatório do mesmo dossier; não cria um motor ou módulo paralelo. Recorte:2026, residente geral comercial/industrial/agrícola, ano civil, MZN. Homologação mantém2bad72c0.
1. Sem volume anterior, permite indicar o último montante PEC já determinado com NUIT, exercício, valor, fonte/fundamento e confirmação de que é o último aplicável ao caso. A hipótese documental29(5) conserva esse montante; não o confunde com volume de negócios ou pagamentos somados, não aplica de novo0,5%/limites nem deduz PPC pela segunda vez. Ausência, incompatibilidade, exercício futuro/anterior ao início ou falta de conferência mantêm a estimativa indeterminada. A referência fica conservada quando deixa de ser usada; alterações exigem reconferência. Uma rectificação documental para volume zero usa a mesma referência. Isto implementa a hipótese documentada, não uma inferência automática da qualificação fiscal ou do último montante a partir de dados inexistentes.
2. No cálculo ordinário29(2), PPC documentados acima da base limitada produzem estimativa PEC zero e diferença documental explícita. Não criam valor negativo, reembolso, crédito, transporte ou dedução anual. Esta regra tem marcador de versão na chave do livro apenas quando afecta o caso; obriga a recalcular resultados antigos dependentes, preservando histórico e casos ordinários anteriores. Fonte reutilizada:BR16/I16-04-2008,p122/PDF24,original SHA256fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee.
Os campos têm rótulos e controlos separados de largura completa; referências vazias aparecem por confirmar. Consulta do suporte e apresentação dos efeitos são parte do mesmo formulário. Não executa pagamentos ou declarações. Fonte, escolhas, original e histórico conservados no rascunho e no relatório.
QA:21testes frontend afectados, incluindo seis novos; uma prova nativa/42asserções de POST/PUT/GET em transacção com rollback na base isolada investmoz_demography_test:55441. Quatro cenários:último montante42500,25, fonte ausente, volume positivo com referência conservada e PPC60000/base50000/estimativa0/diferença10000. Recuperação e relatório financeiro do dossier conferidos; saldo anual138MZN e histórico anterior inalterados. Tipos, lint focado, sintaxe PHP, Pint e build QA aprovados. Edge:componentes reais compilados com dados sintéticos, ambos os efeitos, confirmação retirada após edição e apresentação final conferidos; sem API/escrita na base pela prévia. A primeira prévia importava um auxiliar Node com filesystem; foi substituído por JSON sintético gerado, sem alterar o produto para contornar o erro. Provas em outputs/irpc-pec-last-amount-20261113/. Não repetidas as provas aceites dos percursos não afectados.
Estado:implementado e verificado em QA; pacote agrupado, ainda sem instalação e dependente de autorização concreta. Não encerra L8 nem o portal. Restam períodos/regimes especiais, qualificação documental de casos fora deste recorte e os demais resíduos L1–L7/restanteL8/L9–L16 e módulos. Execução de pagamentos não é requisito do portal. A instalação deverá usar o executor Git, com backup/restauro fresco e reversão de release; preservar snapshots/históricos produzidos após a entrega, sem restaurar uma base antiga sobre edições posteriores.
## 13/11/2026 — lote PEC ordinário instalado em f059c226
Entrega expressamente autorizada e instalada pelo executor Git existente na main em https://teste-investidor.tempo.co.mz. Revisão servida f059c226d26ce0e9f5fec30ecfed44a65d61d6e3. Inclui referência ao último montante PEC documentado, reconciliação de PPC acima da base e apresentação do formulário/relatório. São hipóteses para viabilidade; não há execução de pagamentos, declarações, reembolso ou dedução criada pelo portal.
Backup/restauro fresco de193tabelas/413ficheiros e conferência pós-instalação de21tabelas protegidas sem diferenças. Mantidos instituições, dossiers, documentos, simulações, histórico, Educação, mapa, relatórios, pesquisa, suporte, permissões e12lotes/920456observações demográficas.23fontes conferidas por hash; revisão pública e HTTPS verificados. Executor/configuração inalterados, zero migrações/importações; readiness demográfica vinculada à revisão com original arquivado. CI obrigatório executado normalmente; provas anteriores reutilizadas sem nova ronda manual. Recibo: outputs/irpc-pec-last-amount-20261113/homologation-installed.json. Pacote agrupado v2 selado preservado; a versão anterior do ZIP conserva-se como histórico de normalização de finais de linha, sem alteração do produto.
Este lote deixa de ser pendência de instalação. L8 continua parcial para períodos/regimes especiais e casos fora do recorte; os resíduos L1–L7/restanteL8/L9–L16 e restantes módulos mantêm o saldo próprio. Próximo lote compatível: projecção conjunta PPC/PEC no ecrã e relatório, separada dos comprovativos pagos e do fecho anual.
## 13/11/2026 — lote integrado de projecção mensal PPC/PEC, QA
Continuidade do saldo de integração e relatórios AUD-SIM-INT02/INT06, após f059c226 instalado. Antes deste delta, as estimativas PPC e a hipótese PEC eram apresentadas separadamente; faltava a vista mensal conjunta. O lote reúne a apresentação no ecrã L8, a mesma projecção no relatório do dossier e a separação entre estimativas e comprovativos documentados como pagos. Reutiliza os calculadores aceites, o livro, o rascunho, a recuperação e os exportadores existentes. Não acrescenta um motor fiscal, formulário de pagamento, API, tabela ou permissão.
A vista agrega cada componente uma única vez por mês, com fracções exactas. A opção PEC concentrada conserva o mês escolhido; a repartida conserva os três montantes explícitos. Uma componente desconhecida ou uma distribuição por reconciliar produz total por determinar e subtotais conhecidos identificados; não vira zero. Zero documentado é distinguido da falta de dados. São previsões PPC/PEC do recorte ordinário2026, não o saldo a executar ou todos os impostos do negócio. Valores pagos, deduções, saldo anual, fluxo financeiro e métricas do investimento não são acrescidos ou substituídos.
O relatório usa as opções guardadas na versão calculada, preservando as escolhas posteriores ainda por recalcular. Fontes e explicação aparecem no resumo da projecção; as linhas mensais conservam os montantes sem repetir extensamente as mesmas citações. A identificação de dossier/oportunidade e os exportadores existentes mantêm-se. Legados sem estas preparações não recebem uma projecção presumida.
QA:42testes frontend afectados, incluindo sete novos de agregação exacta, sobreposição, ausência, distribuição decimal, zero, recuperação, relatório e apresentação. Os sete testes do delta foram reconferidos após o ajuste de apresentação; o ensaio da vista L8 confirma os grupos separados, além da tabela. Prova nativa de POST/PUT/GET/recuperação em quatro cenários com42asserções e rollback na base isolada investmoz_demography_test:55441. Cenários:112000MZN totais com64000emMaio, distribuição por seis meses, PEC por completar sem total presumido, e zero explícito sem saída. Saldo anual sintético138MZN e histórico anterior preservados; relatório financeiro do mesmo dossier conferido. Tipos, lint focado sem reformatação dos ficheiros legados, Pint/sintaxe e build QA aprovados. Edge:componente real compilado e relatório, estados completo/parcial/zero e apresentação final sem explicações repetidas. Prévia sintética sem API/BD; provas aceites dos restantes percursos reutilizadas. Provas:outputs/irpc-cashflow-group-20261113/.
Estado:implementado e verificado em QA, ainda sem instalação. Homologação mantém f059c226. Pacote único compatível; instalação depende de autorização concreta, com backup/restauro fresco e reversão de release preservando snapshots e edições posteriores. Este lote encerra a integração visual e documental destas previsões mensais; não encerra L8 nem o portal. Períodos/regimes especiais, anualização PPC28(3), outros casos documentais e resíduos L1–L7/restanteL8/L9–L16 e módulos conservam o saldo próprio. Execução de pagamentos não é requisito do portal.
## 13/11/2026 — projecção fiscal agrupada instalada em 95b33275
Instalação expressamente autorizada, concluída pelo executor Git existente na homologação https://teste-investidor.tempo.co.mz. Revisão servida95b33275aae48a6209c67002788c177b3dd80fcb. Inclui vista mensal conjunta PPC/PEC, distinção completo/parcial/zero, separação de estimativas e comprovativos e o mesmo conteúdo no relatório do dossier, com fontes concentradas no resumo. Sem alteração dos totais do negócio, pagamentos, deduções ou histórico.
Backup/restauro fresco de193tabelas/414ficheiros,29fontes conferidas por hash e21tabelas protegidas sem diferenças. Mantidos instituições, dossiers, documentos, simulações, Educação, mapa, relatórios, pesquisa, suporte, permissões e12lotes/920456observações demográficas. Zero migrações/importações; executor e configuração intactos. Readiness demográfica vinculada à revisão com original arquivado. HTTPS e revisão pública conferidos; CI obrigatório executado normalmente. Provas aceites reutilizadas sem nova ronda manual de percursos. Recibo:outputs/irpc-cashflow-group-20261113/homologation-installed.json. Pacote agrupado selado inalterado.
Este lote deixa de ser pendência de instalação. Não encerra o simulador ou o portal: períodos/regimes especiais, anualização28(3), casos documentais e resíduos L1–L7/restanteL8/L9–L16 e módulos mantêm o saldo próprio. Próximo lote compatível: coerência do período L6 nas estimativas, projecção e relatórios, preservando a preparação e o fecho anual independente.
## 14/11/2026 — lote de coerência do período L6 em PPC/PEC, QA
Continuidade do saldo AUD-SIM-INT02/INT06 após95b33275 instalado. Conferência delimitada do código: PPC/PEC ordinários e a projecção recebiam titular/exercício/tipo, sem confrontar as datas L6 conhecidas. Podiam apresentar meses civis num dossier com período curto ou não civil. O lote liga o contexto já existente aos dois estimadores, à vista mensal e ao relatório da mesma versão, sem duplicar o motor fiscal, criar API, tabela ou permissão.
As datas preparadas em L6 prevalecem para a conferência da preparação, mesmo quando ainda diferem do último cálculo L6; não são substituídas silenciosamente pelo resultado anterior. O período usado fica conservado no modelo da versão anual calculada. O relatório lê esse exemplar, preservando edições posteriores ainda por recalcular. Uma divergência face ao recorte civil2026 mantém as estimativas dependentes por reconciliar, sem calendário ou total presumidos. As entradas e as hipóteses de caixa ficam guardadas, sem alterar o cálculo anual independente.
A dispensa PEC do ano de início documentado mantém zero nos períodos válidos contidos em2026, incluindo o período curto de início; datas inválidas, outro exercício ou período não civil fora deste recorte não recebem dispensa presumida. Os avisos identificam só a componente afectada; se PPC e PEC estiverem ambos por reconciliar, o resumo reúne a causa comum. A chave de cálculo recebe um marcador apenas para os casos afectados pela nova conferência, sem invalidar globalmente os livros ordinários aceites. Livros e históricos antigos conservados. A preparação independente antiga, sem metadados L6 disponíveis, mantém o seu âmbito documental explícito.
QA iniciada em13/11 e fechada em14/11:61testes frontend afectados, incluindo oito novos de período/edição/recuperação/consumidores. Prova nativa POST/PUT/GET com42asserções, quatro cenários e rollback na base isolada investmoz_demography_test:55441. Civil completo mantém112000MZN de projecção; não civil e curto ficam por reconciliar; início preserva PECzero e PPC separado. Nos quatro cenários, o resultado anual independente coincide com o exemplar anterior sem as preparações e o histórico mantém-se. Originais L6 e datas são produzidos pelo percurso normal existente, sem fabricar um calendário oficial. Relatório financeiro do mesmo dossier conferido. Tipos, lint/formatação focados, Pint/sintaxe PHP e build QA aprovados. Edge:componentes reais compilados, quatro contextos, dispensa e aviso específico PPC, ecrã/relatório concordantes. Prévia sintética sem API ou base de dados. Provas aceites dos percursos não afectados reutilizadas; dependências financeiras e exportadores conferidos idênticos a95b33275. Provas:outputs/irpc-period-consistency-20261113/.
Estado:implementado e verificado em QA, sem instalação. Homologação mantém95b33275. Lote único para estimadores, projecção, recuperação e relatório. Encerra a lacuna de coerência do período conhecido; não implementa ainda os cálculos/calendários dos períodos ou regimes especiais nem a anualização PPC28(3). L8 e portal permanecem parciais; resíduos L1–L7/restanteL8/L9–L16 e módulos mantidos. Sem pagamentos, novas deduções, migrações, importações ou permissões. Instalação exige autorização concreta e o executor Git existente, com backup/restauro fresco e reversão de release preservando snapshots/edições posteriores.
File diff suppressed because one or more lines are too long
@@ -22,6 +22,7 @@ export type QuantitativeMapContext = {
source_reference?: string | null;
source_date?: string | null;
knowledge: string;
measure_kind?: 'unknown' | 'observed' | 'planned' | 'projected';
missing_metadata: string[];
}[];
zones_consulted?: number;
@@ -47,6 +48,17 @@ const metadataLabels: Record<string, string> = {
source_date: 'data da fonte',
};
const text = (value?: string | null) => value?.trim() || 'Não indicado';
const measureKinds: Record<string, string> = {
unknown: 'Por confirmar',
observed: 'Observado / efectivo',
planned: 'Planeado / meta',
projected: 'Projectado / estimado',
};
const measureKind = (row: QuantitativeMapContext['records'][number]) =>
row.measure_kind &&
['observed', 'planned', 'projected'].includes(row.measure_kind)
? row.measure_kind
: 'unknown';
const completeDate = (value?: string | null) =>
Boolean(
value &&
@@ -55,6 +67,286 @@ const completeDate = (value?: string | null) =>
new Date(value).toISOString().slice(0, 10) === value,
);
type MapObservation = QuantitativeMapContext['records'][number];
// Calendar metadata only: inclusive source dates, never a rate or annualisation.
function periodDays(row: MapObservation): number | null {
if (
!completeDate(row.period_start) ||
!completeDate(row.period_end) ||
row.period_start! > row.period_end!
)
return null;
return (
(Date.parse(row.period_end!) - Date.parse(row.period_start!)) /
86_400_000 +
1
);
}
const observationKey = (row: MapObservation) =>
JSON.stringify([row.zone_id, row.id, row.zone_version]);
export function compareMapObservations(
first: MapObservation,
second: MapObservation,
) {
const issues: string[] = [];
if (observationKey(first) === observationKey(second))
issues.push('Escolha duas observações distintas.');
if (first.zone_id !== second.zone_id)
issues.push(
'Territórios distintos: os valores não representam uma série do mesmo local.',
);
for (const [label, row] of [
['Primeira', first],
['Segunda', second],
] as const) {
if (measureKind(row) === 'unknown')
issues.push(
`${label} observação: natureza do valor por confirmar na fonte.`,
);
if (row.knowledge !== 'documented')
issues.push(
`${label} observação: informação desconhecida ou em divergência.`,
);
if (
row.value === null ||
!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(row.value)
)
issues.push(
`${label} observação: valor quantitativo por confirmar.`,
);
const comparisonMetadata: Record<string, string> = {
...metadataLabels,
description: 'descrição/natureza da grandeza',
};
const missing = Object.keys(comparisonMetadata).filter(
(field) => !row[field as keyof MapObservation]?.toString().trim(),
);
if (missing.length)
issues.push(
`${label} observação sem ${missing.map((field) => comparisonMetadata[field]).join(', ')}.`,
);
if (
!completeDate(row.period_start) ||
!completeDate(row.period_end) ||
(row.period_start ?? '') > (row.period_end ?? '')
)
issues.push(`${label} observação: período incompleto ou inválido.`);
if (!completeDate(row.source_date))
issues.push(
`${label} observação: data da fonte incompleta ou inválida.`,
);
}
const firstDays = periodDays(first);
const secondDays = periodDays(second);
if (firstDays !== null && secondDays !== null) {
if (firstDays !== secondDays)
issues.push(
'Duração dos períodos diferente; não trate os valores como bases temporais equivalentes.',
);
if (
(first.period_start !== second.period_start ||
first.period_end !== second.period_end) &&
first.period_start! <= second.period_end! &&
second.period_start! <= first.period_end!
)
issues.push(
'Os períodos sobrepõem-se; as observações podem abranger os mesmos dias.',
);
}
if (measureKind(first) !== measureKind(second))
issues.push(
'Natureza do valor diferente: observado, planeado e projectado não são equivalentes.',
);
for (const [field, label] of Object.entries({
indicator: 'Indicador',
unit: 'Unidade',
scope: 'Âmbito',
population: 'População/base',
methodology: 'Metodologia',
})) {
if (
(first[field as keyof MapObservation] ?? '').toString().trim() !==
(second[field as keyof MapObservation] ?? '').toString().trim()
)
issues.push(`${label} diferente entre as observações.`);
}
return {
state: issues.length ? 'review_required' : 'metadata_aligned',
issues,
};
}
export function MapObservationComparison({
first,
second,
}: {
first?: MapObservation;
second?: MapObservation;
}) {
if (!first || !second)
return (
<p role="status">
Seleccione duas observações disponíveis nos filtros actuais.
</p>
);
const assessment = compareMapObservations(first, second);
const fields: [string, (row: MapObservation) => string][] = [
['Território', (row) => row.zone_name],
['Observação', (row) => text(row.label)],
['Descrição e natureza da grandeza', (row) => text(row.description)],
['Natureza do valor', (row) => measureKinds[measureKind(row)]],
['Indicador', (row) => labels[row.indicator] ?? row.indicator],
[
'Valor original',
(row) => (row.value === null ? 'Por confirmar' : row.value),
],
['Unidade', (row) => text(row.unit)],
[
'Período',
(row) => `${text(row.period_start)} — ${text(row.period_end)}`,
],
[
'Duração do período (dias, limites incluídos)',
(row) => {
const days = periodDays(row);
return days === null ? 'Por confirmar' : String(days);
},
],
['Âmbito', (row) => text(row.scope)],
['População/base', (row) => text(row.population)],
['Metodologia', (row) => text(row.methodology)],
[
'Situação da informação',
(row) =>
row.knowledge === 'documented'
? 'Documentada'
: row.knowledge === 'disputed'
? 'Em divergência'
: 'Por confirmar',
],
['Fonte', (row) => text(row.source_reference)],
['Data da fonte', (row) => text(row.source_date)],
['Versão da ficha territorial', (row) => String(row.zone_version)],
];
return (
<div className="space-y-3">
<p role="status">
{assessment.state === 'metadata_aligned'
? 'Metadados alinhados; confirme nas fontes se os valores podem ser comparados.'
: 'Há diferenças ou lacunas a conferir antes de comparar os valores.'}
</p>
{!!assessment.issues.length && (
<ul className="list-disc pl-5">
{assessment.issues.map((issue) => (
<li key={issue}>{issue}</li>
))}
</ul>
)}
<p>
Períodos distintos ou sobrepostos exigem conferência da duração
e da base na fonte. Esta apresentação não calcula evolução,
soma, conversão, pontuação ou estimativa regional. A natureza
declarada deve ser conferida com a descrição e a fonte.
</p>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<caption className="sr-only">
Conferência de duas observações quantitativas
</caption>
<thead>
<tr>
<th className="p-2">Campo</th>
<th className="p-2">Primeira observação</th>
<th className="p-2">Segunda observação</th>
</tr>
</thead>
<tbody>
{fields.map(([label, value]) => (
<tr key={label} className="border-t">
<th className="p-2" scope="row">
{label}
</th>
<td className="p-2 break-words">
{value(first)}
</td>
<td className="p-2 break-words">
{value(second)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
function MapObservationComparisonPicker({ rows }: { rows: MapObservation[] }) {
const id = useId();
const [firstKey, setFirst] = useState('');
const [secondKey, setSecond] = useState('');
const first = rows.find((row) => observationKey(row) === firstKey);
const second = rows.find((row) => observationKey(row) === secondKey);
return (
<details className="space-y-3 rounded border p-3">
<summary>Comparar duas observações</summary>
<p>
Confronte os dados e as fontes antes de os usar na decisão.
Selecções fora dos filtros deixam de ser comparadas; uma versão
nova exige nova selecção.
</p>
<div className="grid gap-3 sm:grid-cols-2">
{(['Primeira', 'Segunda'] as const).map((label, index) => (
<label
className="grid gap-1"
key={label}
htmlFor={`${id}-${index}`}
>
{label} observação
<select
className="w-full min-w-0 rounded border p-2"
id={`${id}-${index}`}
value={
index === 0
? first
? firstKey
: ''
: second
? secondKey
: ''
}
onChange={(event) =>
(index === 0 ? setFirst : setSecond)(
event.target.value,
)
}
>
<option value="">Seleccionar observação</option>
{rows.map((row) => (
<option
key={observationKey(row)}
value={observationKey(row)}
>
{row.zone_name} —{' '}
{row.label ||
labels[row.indicator] ||
row.indicator}{' '}
— {row.period_start || '?'} /{' '}
{row.period_end || '?'} — v
{row.zone_version}
</option>
))}
</select>
</label>
))}
</div>
<MapObservationComparison first={first} second={second} />
</details>
);
}
export function filterMapIndicators(
records: QuantitativeMapContext['records'],
indicator: string,
@@ -114,9 +406,9 @@ export default function InvestmentMapIndicators({
contexto territorial, independentes das evidências das
oportunidades. Emprego não mede trabalhadores disponíveis;
investimento, produção e exportações não constituem um índice de
potencial ou garantia de retorno. A natureza prevista ou
efectiva de cada valor depende da descrição da fonte, não é
determinada automaticamente.
potencial ou garantia de retorno. A natureza do valor é
declarada segundo a fonte; sem essa indicação permanece por
confirmar.
</p>
{!context ? (
<p role="status">A carregar indicadores quantitativos…</p>
@@ -242,6 +534,9 @@ export default function InvestmentMapIndicators({
observações. Refine a localização.
</p>
)}
{rows.length > 1 && (
<MapObservationComparisonPicker rows={rows} />
)}
{rows.length === 0 ? (
<p>
{filtered
@@ -308,6 +603,14 @@ export default function InvestmentMapIndicators({
'Por determinar'}{' '}
· {text(row.unit)}
</p>
<p>
Natureza:{' '}
{
measureKinds[
measureKind(row)
]
}
</p>
<p>
{row.knowledge ===
'documented'
@@ -60,7 +60,8 @@ const labels: Record<string, string> = {
geometry_id: 'Geometria já associada ao parque',
zone_link_id: 'Relação económica já associada ao parque',
indicator: 'Indicador',
value: 'Valor observado',
measure_kind: 'Natureza do valor segundo a fonte',
value: 'Valor indicado na fonte',
unit: 'Unidade de medida',
period_start: 'Início do período',
period_end: 'Fim do período',
@@ -86,6 +87,7 @@ const extras: Record<Group, string[]> = {
],
observations: [
'indicator',
'measure_kind',
'value',
'unit',
'period_start',
@@ -105,6 +107,12 @@ const extras: Record<Group, string[]> = {
],
};
const choices: Record<string, Record<string, string>> = {
measure_kind: {
unknown: 'Por confirmar',
observed: 'Observado / efectivo',
planned: 'Planeado / meta',
projected: 'Projectado / estimado',
},
knowledge: {
unknown: 'Não informado',
documented: 'Documentado pela fonte',
@@ -248,7 +256,11 @@ export default function ParkIntegralEditor({
})
}
>
<option value="">Não informado / sem associação</option>
<option value="">
{key === 'measure_kind'
? 'Por confirmar na fonte'
: 'Não informado / sem associação'}
</option>
{Object.entries(options).map(([k, v]) => (
<option key={k} value={k}>
{v}
@@ -541,7 +553,9 @@ export function ParkIntegralPublic({ value }: { value?: ParkIntegral }) {
)
: item[k] == null ||
item[k] === ''
? 'Não informado'
? k === 'measure_kind'
? 'Por confirmar'
: 'Não informado'
: k === 'expansion'
? item[k]
? 'Sim, segundo a fonte'
@@ -1,3 +1,4 @@
import type { AdvanceFiscalPeriod } from '@/lib/irpc-ppc-obligation';
import {SimulationIrpcPecObligation} from './simulation-irpc-pec-obligation';
import { SimulationIrpcPpcObligation } from './simulation-irpc-ppc-obligation';
import { SimulationIrpcInvestment } from './simulation-irpc-investment';
@@ -8,6 +9,9 @@ import { SimulationIrpcInternational } from './simulation-irpc-international';
import { SimulationIrpcPec } from './simulation-irpc-pec';
import { useState } from 'react';
import {
advanceCashflowAmount,
annualAdvanceCashflow,
annualAdvancePeriod,
annualBook,
newAnnualBook,
saveAnnualBook,
@@ -77,6 +81,7 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
);
const patch = (p: Partial<AnnualBook>) => save({ ...b, ...p }),
a = annualAssessment(draft),
advancePeriod = annualAdvancePeriod(draft),
options = annualWithholdingOptions(draft);
const field = (
key: 'subjectNuit' | 'year' | 'reference' | 'assessmentReference',
@@ -341,10 +346,14 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
})}
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>PPC efectivamente pagos</legend>
<legend>Estimativas PPC e PEC para a viabilidade</legend>
<p>Simulação fiscal para avaliar a viabilidade do negócio e as necessidades de tesouraria. Os valores e meses previstos são estimativas; o portal não executa pagamentos nem submete declarações fiscais.</p>
<SimulationIrpcPecObligation book={b} onChange={pecObligation=>patch({pecObligation})}/>
<SimulationIrpcPpcObligation book={b} onChange={ppcObligation=>patch({ppcObligation})}/>
<SimulationIrpcPecObligation book={b} period={advancePeriod} onChange={pecObligation=>patch({pecObligation})}/>
<SimulationIrpcPpcObligation book={b} period={advancePeriod} onChange={ppcObligation=>patch({ppcObligation})}/>
<SimulationIrpcAdvanceCashflow book={b} period={advancePeriod}/>
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>PPC documentados como pagos</legend>
{group('ppc', 'PPC efectuados')}
<p>
Regista comprovativos de pagamentos por conta; não
@@ -613,3 +622,24 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
);
}
}
export function SimulationIrpcAdvanceCashflow({book,period}: {book: AnnualBook;period?: AdvanceFiscalPeriod}) {
if (!book.ppcObligation && !book.pecObligation) return null;
const m = annualAdvanceCashflow(book,period);
const show = advanceCashflowAmount;
return <section className="space-y-3 rounded border p-3" aria-label="Projecção conjunta PPC e PEC">
<h5 className="font-semibold">Saídas previstas PPC/PEC por mês</h5>
{period && <p>Período preparado em L6: {period.from || 'por confirmar'} — {period.to || 'por confirmar'}.</p>}
<p>Reúne as estimativas PPC e a hipótese de tesouraria PEC. Os comprovativos pagos e o fecho anual permanecem separados; esta vista não acrescenta outra saída ao fluxo financeiro do negócio.</p>
<p role="status">{m.total ? 'Total da projecção: '+show(m.total) : 'Projecção parcial — total por determinar.'}</p>
{!!m.pending.length && <ul className="list-disc pl-5">{m.pending.map(p=><li key={p}>{p}</li>)}</ul>}
{!!m.rows.length && <div className="overflow-x-auto"><table className="w-full text-left text-sm">
<caption className="sr-only">Projecção mensal das estimativas PPC e PEC</caption>
<thead><tr><th className="p-2">Mês</th><th className="p-2">PPC</th><th className="p-2">PEC</th><th className="p-2">{m.total ? 'Total previsto' : 'Subtotal conhecido'}</th></tr></thead>
<tbody>{m.rows.map(r=><tr key={r.month} className="border-t"><th scope="row" className="p-2">{r.month}</th><td className="p-2">{m.ppcKnown ? show(r.ppc) : 'Por determinar'}</td><td className="p-2">{m.pecKnown ? show(r.pec) : 'Por determinar'}</td><td className="p-2">{show(r.knownSubtotal)}</td></tr>)}</tbody>
</table></div>}
{!m.rows.length && m.total && <p>Sem saída PPC/PEC estimada neste recorte.</p>}
<p>Meses e montantes dependem das fontes e opções preparadas acima. A opção PEC concentrada conserva o mês escolhido; esta apresentação não certifica o prazo legal.</p>
</section>;
}
@@ -1,5 +1,6 @@
import {SimulationIrpcPecCorrection} from './simulation-irpc-pec-correction';
import {SimulationIrpcPecPlan} from './simulation-irpc-pec-plan';
import type { AdvanceFiscalPeriod } from '@/lib/irpc-ppc-obligation';
import { SimulationIrpcPecCorrection } from './simulation-irpc-pec-correction';
import { SimulationIrpcPecPlan } from './simulation-irpc-pec-plan';
import {
newPecObligation,
calculatePecObligation,
@@ -9,9 +10,11 @@ import { fractionPresentation } from '@/lib/fiscal-rational';
import type { AnnualBook } from '@/lib/irpc-annual-settlement';
export function SimulationIrpcPecObligation({
book,
period,
onChange,
}: {
book: AnnualBook;
period?: AdvanceFiscalPeriod;
onChange: (p: PecObligation) => void;
}) {
const p = book.pecObligation;
@@ -38,16 +41,21 @@ export function SimulationIrpcPecObligation({
book.subjectNuit,
book.year,
book.assessment,
period,
),
patch = (changes: Partial<PecObligation>) =>
onChange({ ...p, ...changes });
const field = (
key: Exclude<keyof PecObligation, 'version' | 'scope' | 'plan' | 'correction'>,
key: Exclude<
keyof PecObligation,
'version' | 'scope' | 'plan' | 'correction' | 'lastAmount'
>,
label: string,
) => (
<label className="block">
{label}
<input
className="mt-1 block w-full rounded border px-3 py-2"
aria-label={label}
value={p[key]}
onChange={(e) => patch({ [key]: e.target.value })}
@@ -63,6 +71,7 @@ export function SimulationIrpcPecObligation({
<label className="block">
Enquadramento PEC
<select
className="mt-1 block w-full rounded border px-3 py-2"
aria-label="Enquadramento PEC"
value={p.scope}
onChange={(e) =>
@@ -104,7 +113,131 @@ export function SimulationIrpcPecObligation({
'turnoverReference',
'Fonte do volume de negócios — PEC',
)}
<SimulationIrpcPecCorrection value={p.correction} original={p.previousTurnover} subjectNuit={p.subjectNuit} onChange={correction=>patch({correction})}/>
<SimulationIrpcPecCorrection
value={p.correction}
original={p.previousTurnover}
subjectNuit={p.subjectNuit}
onChange={(correction) => patch({ correction })}
/>
{(m.requiresLastAmount || p.lastAmount) && (
<details className="rounded border p-3">
<summary>
Sem volume anterior — referência PEC dos
anos anteriores
</summary>
<p>
Indique o último montante PEC já
determinado, com suporte do mesmo
titular, ano civil e MZN. Não introduza
volume de negócios ou somatório de
pagamentos. A hipótese conserva o valor
documental sem nova percentagem ou
segunda dedução de PPC.
</p>
{!p.lastAmount ? (
<button
type="button"
onClick={() =>
patch({
lastAmount: {
version: 1,
subjectNuit:
p.subjectNuit,
year: '',
amount: '',
reference: '',
confirmed: '',
},
})
}
>
Acrescentar referência PEC anterior
</button>
) : (
<>
{(
[
'subjectNuit',
'year',
'amount',
'reference',
] as const
).map((key) => {
const names = {
subjectNuit:
'NUIT do montante PEC anterior',
year: 'Exercício do último montante PEC',
amount: 'Último montante PEC já determinado — MZN',
reference:
'Fonte e fundamento do último montante PEC',
};
return (
<label
className="block"
key={key}
>
{names[key]}
<input
className="mt-1 block w-full rounded border px-3 py-2"
aria-label={
names[key]
}
value={
p.lastAmount![
key
]
}
onChange={(e) =>
patch({
lastAmount:
{
...p.lastAmount!,
[key]: e
.target
.value,
confirmed:
'',
},
})
}
/>
</label>
);
})}
<label className="block">
<input
type="checkbox"
checked={
p.lastAmount
.confirmed === 'yes'
}
onChange={(e) =>
patch({
lastAmount: {
...p.lastAmount!,
confirmed: e
.target
.checked
? 'yes'
: '',
},
})
}
/>
Conferi no suporte que é o
último montante PEC aplicável a
esta hipótese
</label>
<p>
Alterar a referência exige nova
conferência. Guardar conserva o
original e o histórico; não
certifica a situação fiscal.
</p>
</>
)}
</details>
)}
{field(
'previousPpcPaid',
'PPC efectivamente pagos no ano anterior — MZN',
@@ -124,18 +257,31 @@ export function SimulationIrpcPecObligation({
) : (
<>
<p>
Total estimado:{' '}
{fractionPresentation(m.total!).decimal} MZN.
Total estimado: {fractionPresentation(m.total!).decimal}{' '}
MZN.
</p>
<ul>
{m.months.map((month) => (
<li key={month}>{month}</li>
))}
</ul>
</>
)}
<SimulationIrpcPecPlan plan={p.plan} total={m.total} notApplicable={m.notApplicable} year={book.year} onChange={plan=>patch({plan})}/>
{m.ppcExcess && (
<p role="status">
Os PPC anteriores cobrem a base limitada desta hipótese.
Estimativa PEC: 0 MZN;diferença documental:{' '}
{fractionPresentation(m.ppcExcess).decimal} MZN. Esta
diferença não cria reembolso,crédito ou dedução anual.
</p>
)}
<SimulationIrpcPecPlan
plan={p.plan}
total={m.total}
notApplicable={m.notApplicable}
year={book.year}
onChange={(plan) => patch({ plan })}
/>
<p>
Guardar o rascunho conserva esta preparação. Calcular o fecho
anual inclui-a no relatório. Não regista pagamentos,cria
@@ -1,3 +1,4 @@
import type { AdvanceFiscalPeriod } from '@/lib/irpc-ppc-obligation';
import {
newPpcObligation,
calculatePpcObligation,
@@ -8,9 +9,11 @@ import { fractionPresentation } from '@/lib/fiscal-rational';
import type { AnnualBook } from '@/lib/irpc-annual-settlement';
export function SimulationIrpcPpcObligation({
book,
period,
onChange,
}: {
book: AnnualBook;
period?: AdvanceFiscalPeriod;
onChange: (value: PpcObligation) => void;
}) {
const p = book.ppcObligation;
@@ -37,6 +40,7 @@ export function SimulationIrpcPpcObligation({
book.subjectNuit,
book.year,
book.assessment,
period,
);
const patch = (changes: Partial<PpcObligation>) =>
onChange({ ...p, ...changes });
+84 -4
View File
@@ -1,7 +1,8 @@
import { calculatePecPlan } from './irpc-pec-plan.ts';
import {pecCorrectionVersion} from './irpc-pec-correction.ts';
import {pecPlanVersion} from './irpc-pec-plan.ts';
import {validatePecObligation,pecObligationReport,pecObligationVersion,type PecObligation} from './irpc-pec-obligation.ts';
import { validatePpcObligation, ppcObligationReport, ppcObligationVersion, type PpcObligation } from './irpc-ppc-obligation.ts';
import {validatePecObligation,pecObligationReport,pecObligationVersion,pecPpcExcessVersion,calculatePecObligation,type PecObligation} from './irpc-pec-obligation.ts';
import { ordinaryAdvancePeriodIssues, type AdvanceFiscalPeriod, calculatePpcObligation, validatePpcObligation, ppcObligationReport, ppcObligationVersion, type PpcObligation } from './irpc-ppc-obligation.ts';
import {
newInvestmentCredit,
validateInvestmentCredit,
@@ -421,6 +422,11 @@ export function annualAssessment(d: Record<string, unknown>) {
: zero(),
};
}
export function annualAdvancePeriod(d: Record<string, unknown>): AdvanceFiscalPeriod {
const prepared = normalBook(d).periodsFx;
return prepared ? {from: prepared.from, to: prepared.to} : annualAssessment(d).period;
}
export function reuseAnnualAssessment(
d: Record<string, unknown>,
b: AnnualBook,
@@ -568,13 +574,18 @@ function dependencies(d: Record<string, unknown>, includeInvestment = false) {
}
function key(d: Record<string, unknown>, b: AnnualBook) {
const { result: _, history: __, ...input } = b;
const period = annualAdvancePeriod(d);
const periodGuard = ordinaryAdvancePeriodIssues(period, b.year).length > 0 &&
(b.ppcObligation || b.pecObligation && !calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,period).notApplicable);
return closureFingerprint([
annualSettlementVersion,
...(periodGuard ? ['IRPC-advance-period-consistency/1'] : []),
acquisitionScope(d),
input,
dependencies(d, !!b.investmentCredit),
...(b.ppcObligation ? [ppcObligationVersion] : []),
...(b.pecObligation ? [pecObligationVersion] : []),
...(b.pecObligation && calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment).ppcExcess ? [pecPpcExcessVersion] : []),
...(b.pecObligation?.plan ? [pecPlanVersion] : []),
...(b.pecObligation?.correction ? [pecCorrectionVersion] : []),
...(b.investmentCredit && b.creditCarry?.lots.length
@@ -1592,6 +1603,7 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
return {
version: annualSettlementVersion,
input: structuredClone(input),
...(b.ppcObligation || b.pecObligation ? {advancePeriod: annualAdvancePeriod(d)} : {}),
subjectNuit: b.subjectNuit,
year: b.year,
assessment: assessment?.record
@@ -1724,8 +1736,9 @@ export function annualReportRows(
indeterminate: 'Indeterminado',
}[m.status];
return [
...(m.input.pecObligation ? pecObligationReport(m.input.pecObligation,m.subjectNuit,m.year,m.input.assessment) : []),
...(m.input.ppcObligation ? ppcObligationReport(m.input.ppcObligation,m.subjectNuit,m.year,m.input.assessment) : []),
...(m.input.pecObligation ? pecObligationReport(m.input.pecObligation,m.subjectNuit,m.year,m.input.assessment,m.advancePeriod ?? m.assessment?.period) : []),
...(m.input.ppcObligation ? ppcObligationReport(m.input.ppcObligation,m.subjectNuit,m.year,m.input.assessment,m.advancePeriod ?? m.assessment?.period) : []),
...annualAdvanceCashflowReport(m.input,m.advancePeriod ?? m.assessment?.period),
[
'IRPC — L8',
'Fecho anual — âmbito e origem',
@@ -2407,3 +2420,70 @@ export function annualHtml(d: Record<string, unknown>) {
'</html>'
);
}
/** Derived cash view only. Reuses the existing estimates; never changes the annual ledger. */
export function annualAdvanceCashflow(b: Pick<AnnualBook, 'subjectNuit' | 'year' | 'assessment' | 'ppcObligation' | 'pecObligation'>, period?: AdvanceFiscalPeriod) {
const pending: string[] = [];
const sources: string[] = [];
const amounts: Array<{month: string; kind: 'ppc' | 'pec'; amount: FiscalFraction}> = [];
let ppcKnown = false, pecKnown = false;
if (!b.ppcObligation) pending.push('Prepare a estimativa PPC para completar a projecção conjunta.');
else {
const p = calculatePpcObligation(b.ppcObligation, b.subjectNuit, b.year, b.assessment, period);
sources.push(b.ppcObligation.assessmentReference, b.ppcObligation.withholdingReference, p.source);
pending.push(...p.pending);
ppcKnown = p.pending.length === 0 && p.total !== null;
if (ppcKnown) amounts.push(...p.schedule.map(r => ({...r, kind: 'ppc' as const})));
}
if (!b.pecObligation) pending.push('Prepare a estimativa PEC para completar a projecção conjunta.');
else {
const p = calculatePecObligation(b.pecObligation, b.subjectNuit, b.year, b.assessment, period);
sources.push(b.pecObligation.turnoverReference, b.pecObligation.ppcReference, p.source);
pending.push(...p.pending);
if (!p.pending.length && p.total) {
if (BigInt(p.total.numerator) === 0n) pecKnown = true;
else if (!b.pecObligation.plan) pending.push('Distribua a hipótese de tesouraria PEC para completar os meses da projecção conjunta.');
else {
const allocation = calculatePecPlan(b.pecObligation.plan, p.total, p.notApplicable, b.year);
sources.push(b.pecObligation.plan.reference, allocation.source);
pending.push(...allocation.pending);
pecKnown = allocation.allocationReconciled;
if (pecKnown) amounts.push(...allocation.rows.map(r => ({...r, kind: 'pec' as const})));
}
}
}
const grouped = new Map<string, {month: string; ppc: FiscalFraction; pec: FiscalFraction}>();
for (const row of amounts) {
if (BigInt(row.amount.numerator) === 0n) continue;
const current = grouped.get(row.month) ?? {month: row.month, ppc: fraction(0n), pec: fraction(0n)};
current[row.kind] = addFractions([current[row.kind], row.amount]);
grouped.set(row.month, current);
}
const rows = [...grouped.values()].sort((a,b) => a.month.localeCompare(b.month)).map(r => ({...r, knownSubtotal: addFractions([r.ppc,r.pec])}));
const knownSubtotal = addFractions(rows.map(r => r.knownSubtotal));
let issues = [...new Set(pending)];
const periodIssues = ['PPC','PEC'].map(kind => ordinaryAdvancePeriodIssues(period,b.year,kind as 'PPC' | 'PEC')[0]);
if (periodIssues.every(issue => issue && issues.includes(issue))) {
issues = issues.filter(issue => !periodIssues.includes(issue));
issues.push(...ordinaryAdvancePeriodIssues(period,b.year));
}
return {version:'IRPC-advance-cashflow/1', ppcKnown, pecKnown, pending:issues, rows,
total: ppcKnown && pecKnown ? knownSubtotal : null, knownSubtotal, sources:[...new Set(sources.filter(s => s.trim()))]};
}
export function advanceCashflowAmount(value: FiscalFraction): string {
return fractionPresentation(value).decimal+' MZN';
}
export function annualAdvanceCashflowReport(b: Pick<AnnualBook, 'subjectNuit' | 'year' | 'assessment' | 'ppcObligation' | 'pecObligation'>, period?: AdvanceFiscalPeriod): Array<[string,string,string,string]> {
if (!b.ppcObligation && !b.pecObligation) return [];
const m = annualAdvanceCashflow(b, period);
const show = advanceCashflowAmount;
const source = m.sources.join('; ');
return [
['IRPC — L8', 'Projecção mensal conjunta PPC/PEC', (m.total ? 'Total da projecção: '+show(m.total)+'.' : 'Projecção parcial; total por determinar. '+m.pending.join(' '))+' Estimativas de tesouraria; comprovativos pagos, deduções e fecho anual separados. Esta vista não acrescenta outra saída ao fluxo financeiro.', source],
...m.rows.map(r => ['IRPC — L8', 'PPC/PEC — '+r.month,
'PPC: '+(m.ppcKnown ? show(r.ppc) : 'por determinar')+'; PEC: '+(m.pecKnown ? show(r.pec) : 'por determinar')+'; '+(m.total ? 'total previsto: ' : 'subtotal conhecido: ')+show(r.knownSubtotal)+'.', ''] as [string,string,string,string]),
];
}
+232 -35
View File
@@ -1,5 +1,18 @@
import {validatePecCorrection,correctedPecTurnover,pecCorrectionSource,type PecCorrection} from './irpc-pec-correction.ts';
import {validatePecPlan,pecPlanReport,type PecPlan} from './irpc-pec-plan.ts';
import {
ordinaryAdvancePeriodIssues,
type AdvanceFiscalPeriod,
} from './irpc-ppc-obligation.ts';
import {
validatePecCorrection,
correctedPecTurnover,
pecCorrectionSource,
type PecCorrection,
} from './irpc-pec-correction.ts';
import {
validatePecPlan,
pecPlanReport,
type PecPlan,
} from './irpc-pec-plan.ts';
import {
decimalFraction,
fraction,
@@ -8,9 +21,21 @@ import {
type FiscalFraction,
} from './fiscal-rational.ts';
export const pecObligationVersion = 'IRPC-PEC-obligation/1';
export const pecPpcExcessVersion = 'IRPC-PEC-PPC-excess/1';
export const pecObligationSource =
'RIRPC,Decreto9/2008,art29(1–5),BR16/I16-04-2008,original SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee.0,5% do volume de negócios anterior,limites30.000/100.000MZN,menos PPC efectivamente pagos no ano anterior.Junho/Agosto/Outubro;opção de pagamento único prevista,sem data ou repartição inventada. Ano de início excluído29(4);sem volume anterior exige último montante29(5). Recorte2026,residente geral comercial/industrial/agrícola,ano civil,MZN. Regimes/períodos especiais,rectificação29(3),base de anos anteriores29(5),guias/juros separados.';
export type PecLastAmount = {
version: 1;
subjectNuit: string;
year: string;
amount: string;
reference: string;
confirmed: '' | 'yes';
};
export const pecLastAmountSource =
'RIRPC,Decreto9/2008,art29(5),BR16/I16-04-2008,p122/PDF24,original SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee. Hipótese documentada: último montante PEC já determinado,para titular/ano civil/MZN correspondentes;sem recalcular 0,5%,limites ou deduzir PPC pela segunda vez. Não demonstra por si a qualificação fiscal do caso.';
export type PecObligation = {
lastAmount?: PecLastAmount;
version: 1;
plan?: PecPlan;
correction?: PecCorrection;
@@ -42,8 +67,24 @@ export const newPecObligation = (
ppcReference: '',
});
export function validatePecObligation(p: PecObligation) {
if(p?.correction!==undefined)validatePecCorrection(p.correction);
if(p?.plan!==undefined)validatePecPlan(p.plan);
if (p?.lastAmount !== undefined) {
const v = p.lastAmount;
if (
!v ||
v.version !== 1 ||
!['', 'yes'].includes(v.confirmed) ||
['subjectNuit', 'year', 'amount', 'reference'].some(
(k) =>
typeof (v as unknown as Record<string, unknown>)[k] !==
'string',
)
)
throw Error(
'Referência PEC anterior não recuperável;original preservado.',
);
}
if (p?.correction !== undefined) validatePecCorrection(p.correction);
if (p?.plan !== undefined) validatePecPlan(p.plan);
if (
!p ||
p.version !== 1 ||
@@ -65,14 +106,22 @@ export function calculatePecObligation(
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
) {
validatePecObligation(p);
const pending: string[] = [];
const turnover=correctedPecTurnover(p.correction,p.previousTurnover,p.subjectNuit,p.previousYear);
const turnover = correctedPecTurnover(
p.correction,
p.previousTurnover,
p.subjectNuit,
p.previousYear,
);
let percentage: FiscalFraction | null = null,
limited: FiscalFraction | null = null,
total: FiscalFraction | null = null,
notApplicable = false;
ppcExcess: FiscalFraction | null = null,
notApplicable = false,
lastAmountApplied = false;
if (
p.scope !== 'ordinary_resident' ||
assessment !== 'normal' ||
@@ -94,9 +143,28 @@ export function calculatePecObligation(
'PEC: falta ano de início efectivo da actividade e suporte;constituição da empresa não substitui início.',
);
if (!pending.length && p.activityStartYear === '2026') {
notApplicable = true;
total = fraction(0n);
const validStartDate = (value?: string | null) =>
Boolean(
value &&
/^2026-\d{2}-\d{2}$/.test(value) &&
Number.isFinite(Date.parse(value)) &&
new Date(value).toISOString().slice(0, 10) === value,
);
if (
period &&
!(
validStartDate(period.from) &&
validStartDate(period.to) &&
period.from! <= period.to!
)
)
pending.push(...ordinaryAdvancePeriodIssues(period, year, 'PEC'));
if (!pending.length) {
notApplicable = true;
total = fraction(0n);
}
} else if (!pending.length) {
pending.push(...ordinaryAdvancePeriodIssues(period, year, 'PEC'));
pending.push(...turnover.pending);
if (
p.previousYear !== '2025' ||
@@ -117,11 +185,26 @@ export function calculatePecObligation(
if (!pending.length) {
const v = decimalFraction(turnover.value),
ppc = decimalFraction(p.previousPpcPaid);
if (BigInt(v.numerator) === 0n)
pending.push(
'PEC29(5):sem volume de negócios em2025,recuperar último montante dos anos anteriores;zero não determina automaticamente o mínimo.',
);
else {
if (BigInt(v.numerator) === 0n) {
const last = p.lastAmount;
if (
!last ||
last.confirmed !== 'yes' ||
!last.reference.trim() ||
last.subjectNuit !== subjectNuit ||
!/^\d{4}$/.test(last.year) ||
Number(last.year) < Number(p.activityStartYear) ||
Number(last.year) >= Number(year) ||
!/^\d{1,18}(\.\d{1,2})?$/.test(last.amount)
) {
pending.push(
'PEC29(5):sem volume de negócios em2025,recuperar último montante PEC já determinado dos anos anteriores,com titular,exercício,fonte e confirmação de que é o último aplicável;zero não determina automaticamente o mínimo.',
);
} else {
total = decimalFraction(last.amount);
lastAmountApplied = true;
}
} else {
percentage = fraction(
BigInt(v.numerator),
BigInt(v.denominator) * 200n,
@@ -135,23 +218,51 @@ export function calculatePecObligation(
? fraction(100000n)
: percentage;
const residual = subtractFractions(limited, ppc);
if (BigInt(residual.numerator) < 0n)
pending.push(
'PEC:PPC anteriores excedem o montante limitado;caso separado para reconciliação,não gera estimativa negativa ou reembolso.',
);
else total = residual;
if (BigInt(residual.numerator) < 0n) {
total = fraction(0n);
ppcExcess = subtractFractions(ppc, limited);
} else total = residual;
}
}
}
let source = p.correction
? pecObligationSource.replace('rectificação29(3),', '') +
' ' +
pecCorrectionSource
: pecObligationSource;
if (p.lastAmount)
source =
source.replace('base de anos anteriores29(5),', '') +
' ' +
pecLastAmountSource;
if (ppcExcess)
source +=
' Reconciliação da estimativa29(2):PPC documentados cobrem a base limitada;diferença conservada sem gerar direito a reembolso ou crédito.';
return {
version: pecObligationVersion,
source: p.correction ? pecObligationSource.replace('rectificação29(3),','')+' '+pecCorrectionSource : pecObligationSource,
source,
lastAmountSource: p.lastAmount ? pecLastAmountSource : null,
pending,
percentage,
limited,
total,
notApplicable,
turnoverCorrection: p.correction ? {original:p.correction.originalTurnover,revised:p.correction.revisedTurnover,applied:!notApplicable&&turnover.applied,pending:turnover.pending,reference:p.correction.reference,source:pecCorrectionSource}:null,
lastAmountApplied,
ppcExcess,
requiresLastAmount:
!notApplicable &&
turnover.pending.length === 0 &&
/^0+(\.0+)?$/.test(turnover.value),
turnoverCorrection: p.correction
? {
original: p.correction.originalTurnover,
revised: p.correction.revisedTurnover,
applied: !notApplicable && turnover.applied,
pending: turnover.pending,
reference: p.correction.reference,
source: pecCorrectionSource,
}
: null,
months:
total && !notApplicable ? ['2026-06', '2026-08', '2026-10'] : [],
instalmentAmountsDetermined: false,
@@ -164,15 +275,92 @@ export function pecObligationReport(
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
): Array<[string, string, string, string]> {
const m = calculatePecObligation(p, subjectNuit, year, assessment),
const m = calculatePecObligation(p, subjectNuit, year, assessment, period),
show = (v: FiscalFraction | null) =>
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
const originalEstimate=p.correction ? calculatePecObligation({...p,correction:undefined},subjectNuit,year,assessment).total:null;
const correctionEffect=originalEstimate&&m.total ? fractionPresentation(subtractFractions(m.total,originalEstimate)).decimal+' MZN':'por determinar';
const originalEstimate = p.correction
? calculatePecObligation(
{ ...p, correction: undefined },
subjectNuit,
year,
assessment,
period,
).total
: null;
const correctionEffect =
originalEstimate && m.total
? fractionPresentation(subtractFractions(m.total, originalEstimate))
.decimal + ' MZN'
: 'por determinar';
return [
...(p.correction ? [['IRPC — L8','PEC — rectificação do volume','Original: '+p.correction.originalTurnover+' MZN;corrigido: '+p.correction.revisedTurnover+' MZN;'+(m.turnoverCorrection?.applied?'aplicada à estimativa.':'conservada sem aplicação à estimativa.')+' Estimativa original: '+show(originalEstimate)+';efeito na estimativa: '+correctionEffect+'. '+turnoverCorrectionNotice(m.turnoverCorrection),p.correction.reference+'; '+pecCorrectionSource] as [string,string,string,string]]:[]),
...(p.plan ? pecPlanReport(p.plan,m.total,m.notApplicable,year) : []),
...(m.ppcExcess
? [
[
'IRPC — L8',
'PEC — PPC acima da base limitada',
'PPC documentados: ' +
p.previousPpcPaid +
' MZN;base limitada: ' +
show(m.limited) +
';diferença: ' +
show(m.ppcExcess) +
';estimativa PEC: 0 MZN. A diferença é apenas reconciliação desta hipótese; não cria reembolso,crédito,transporte ou dedução anual.',
p.ppcReference +
'; ' +
p.turnoverReference +
'; RIRPC29(2); ' +
pecPpcExcessVersion,
] as [string, string, string, string],
]
: []),
...(p.lastAmount
? [
[
'IRPC — L8',
'PEC — último montante documentado',
'Exercício ' +
(p.lastAmount.year || 'por confirmar') +
';montante PEC já determinado: ' +
(p.lastAmount.amount || 'por confirmar') +
' MZN;' +
(m.lastAmountApplied
? 'usado como hipótese documentada29(5),sem novo cálculo percentual ou segunda dedução de PPC.'
: 'conservado sem aplicação à estimativa.') +
' Não regista pagamento nem dedução anual.',
(p.lastAmount.reference || 'Fonte por indicar') +
'; ' +
pecLastAmountSource,
] as [string, string, string, string],
]
: []),
...(p.correction
? [
[
'IRPC — L8',
'PEC — rectificação do volume',
'Original: ' +
p.correction.originalTurnover +
' MZN;corrigido: ' +
p.correction.revisedTurnover +
' MZN;' +
(m.turnoverCorrection?.applied
? 'aplicada à estimativa.'
: 'conservada sem aplicação à estimativa.') +
' Estimativa original: ' +
show(originalEstimate) +
';efeito na estimativa: ' +
correctionEffect +
'. ' +
turnoverCorrectionNotice(m.turnoverCorrection),
p.correction.reference + '; ' + pecCorrectionSource,
] as [string, string, string, string],
]
: []),
...(p.plan
? pecPlanReport(p.plan, m.total, m.notApplicable, year)
: []),
[
'IRPC — L8',
'PEC — estimativa fiscal',
@@ -180,15 +368,22 @@ export function pecObligationReport(
? m.pending.join(' ')
: m.notApplicable
? 'Não aplicável no ano de início da actividade29(4);total0 MZN.'
: '0,5%: ' +
show(m.percentage) +
';limitado antes de PPC: ' +
show(m.limited) +
';total estimado: ' +
show(m.total) +
';meses: ' +
m.months.join(', ') +
(p.plan ? '. Plano explícito apresentado separadamente;prazo legal não validado.' : '. Montantes por prestação e opção de pagamento único não determinados neste recorte.') + ' Preparação não é pagamento nem dedução anual.',
: m.lastAmountApplied
? 'Hipótese documentada29(5): ' +
show(m.total) +
';último montante PEC já determinado,sem segunda dedução de PPC. Confirmar enquadramento com o suporte indicado.'
: '0,5%: ' +
show(m.percentage) +
';limitado antes de PPC: ' +
show(m.limited) +
';total estimado: ' +
show(m.total) +
';meses: ' +
m.months.join(', ') +
(p.plan
? '. Plano explícito apresentado separadamente;prazo legal não validado.'
: '. Montantes por prestação e opção de pagamento único não determinados neste recorte.') +
' Preparação não é pagamento nem dedução anual.',
p.activityReference +
'; ' +
p.turnoverReference +
@@ -200,4 +395,6 @@ export function pecObligationReport(
];
}
function turnoverCorrectionNotice(c:{pending:string[]}|null){return c?.pending.join(' ')??'';}
function turnoverCorrectionNotice(c: { pending: string[] } | null) {
return c?.pending.join(' ') ?? '';
}
+28 -1
View File
@@ -6,6 +6,30 @@ import {
type FiscalFraction,
} from './fiscal-rational.ts';
export type AdvanceFiscalPeriod = {
from?: string | null;
to?: string | null;
} | null;
export function ordinaryAdvancePeriodIssues(
period: AdvanceFiscalPeriod | undefined,
year: string,
component: 'PPC' | 'PEC' | 'PPC/PEC' = 'PPC/PEC',
): string[] {
if (!period) return [];
return period.from === year + '-01-01' && period.to === year + '-12-31'
? []
: [
component +
': o período L6 ' +
(period.from || 'por confirmar') +
' — ' +
(period.to || 'por confirmar') +
' não corresponde ao ano civil ' +
year +
'. Reconciliar o enquadramento antes de usar as estimativas ordinárias; a preparação fica conservada.',
];
}
export const ppcObligationVersion = 'IRPC-PPC-obligation/1';
export const ppcObligationSource =
'RIRPC, Decreto 9/2008, arts.27(1)(a) e28(1–3), BR16/I,16-04-2008,exemplar preservado SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee: imposto liquidado no exercício anterior nos termos21(1), líquido das retenções na fonte;80%, dividido em três montantes iguais arredondados por excesso ao milhar de meticais. Maio,Julho,Setembro. Recorte2026,residente geral,actividade comercial/industrial/agrícola,ano civil e exercício anterior completo2025,MZN. Períodos especiais,anualização28(3),dispensas,juros e guias separados.';
@@ -57,6 +81,7 @@ export function calculatePpcObligation(
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
) {
validatePpcObligation(p);
const pending: string[] = [];
@@ -88,6 +113,7 @@ export function calculatePpcObligation(
pending.push(
'PPC: montantes anteriores em MZN desconhecidos ou inválidos; zero deve ser explícito.',
);
pending.push(...ordinaryAdvancePeriodIssues(period, year, 'PPC'));
if (!pending.length) {
base = subtractFractions(
decimalFraction(p.previousTax),
@@ -135,8 +161,9 @@ export function ppcObligationReport(
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
): Array<[string, string, string, string]> {
const m = calculatePpcObligation(p, subjectNuit, year, assessment),
const m = calculatePpcObligation(p, subjectNuit, year, assessment, period),
show = (v: FiscalFraction | null) =>
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
return [
@@ -0,0 +1,115 @@
<?php
namespace Tests\Feature;
use App\Models\InvestmentZone;
use App\Models\User;
use App\Services\InvestmentMapIndicators;
use App\Services\InvestmentZoneIntegralDetails;
use App\Services\InvestmentZoneOperationalDetails;
use App\Services\InvestmentZoneOperationalProfile;
use App\Support\IsolatedQaDatabase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;
class InvestmentObservationNatureTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$this->assertTrue(IsolatedQaDatabase::allowsSchemaChanges());
Http::preventStrayRequests();
}
private function observation(?string $kind): array
{
$item = ['id' => (string) Str::uuid(), 'label' => 'SYNTHETIC nature QA', 'description' => 'Synthetic source only.',
'indicator' => 'employment', 'value' => '0', 'unit' => 'pessoas', 'period_start' => '2025-01-01', 'period_end' => '2025-12-31',
'scope' => 'Synthetic zone', 'population' => 'Synthetic establishments', 'methodology' => 'Synthetic register',
'source_reference' => 'SYNTHETIC source; not real statistics', 'source_date' => '2026-01-15', 'knowledge' => 'documented', 'disclosure' => true];
return $kind === null ? $item : [...$item, 'measure_kind' => $kind];
}
private function zone(): InvestmentZone
{
$actor = User::factory()->create(['email' => 'measure-'.Str::uuid().'@example.test']);
return InvestmentZone::create(['code' => 'MEASURE-QA-'.Str::uuid(), 'category' => 'industrial_park', 'name' => 'SYNTHETIC nature QA',
'editor_id' => $actor->id, 'publication_status' => 'draft', 'validation_status' => 'pending']);
}
public function test_explicit_natures_persist_recover_and_project_without_inference_or_private_disclosure(): void
{
$zone = $this->zone();
$service = app(InvestmentZoneIntegralDetails::class);
$ops = app(InvestmentZoneOperationalDetails::class);
$rows = array_map(fn ($kind) => $this->observation($kind), ['observed', 'planned', 'projected', null]);
$hidden = [...$this->observation('observed'), 'disclosure' => false, 'source_reference' => 'PRIVATE-NATURE-SOURCE'];
$merged = $service->merge($zone, [], ['observations' => [...$rows, $hidden]]);
$zone->forceFill(['operational_details' => $ops->pack(['integral' => $merged])])->save();
$saved = $zone->fresh();
$before = $saved->getRawOriginal('operational_details');
$recovered = $ops->recover($saved)['integral']['observations'];
$this->assertSame(['observed', 'planned', 'projected'], array_column(array_slice($recovered, 0, 3), 'measure_kind'));
$this->assertArrayNotHasKey('measure_kind', $recovered[3]);
$this->assertSame('0', $recovered[0]['value']);
$view = app(InvestmentMapIndicators::class)->forZones(collect([$saved]));
$this->assertSame(['observed', 'planned', 'projected', 'unknown'], array_column($view['records'], 'measure_kind'));
$this->assertSame(['0', '0', '0', '0'], array_column($view['records'], 'value'));
$this->assertStringNotContainsString('PRIVATE-NATURE-SOURCE', json_encode($view, JSON_THROW_ON_ERROR));
$this->assertSame($before, $zone->fresh()->getRawOriginal('operational_details'));
$this->assertSame(1, $zone->fresh()->version_no);
}
public function test_invalid_nature_is_rejected_and_an_omitted_nature_does_not_erase_an_explicit_choice(): void
{
$zone = $this->zone();
$service = app(InvestmentZoneIntegralDetails::class);
$row = $this->observation('observed');
$before = $service->merge($zone, [], ['observations' => [$row]]);
$withoutKind = $row;
unset($withoutKind['measure_kind']);
$recovered = $service->merge($zone, $before, ['observations' => [$withoutKind]]);
$this->assertSame('observed', $recovered['observations'][0]['measure_kind']);
$changed = $service->merge($zone, $before, ['observations' => [[...$row, 'measure_kind' => 'planned']]]);
$this->assertSame('planned', $changed['observations'][0]['measure_kind']);
$this->assertSame('observed', $before['observations'][0]['measure_kind']);
try {
$service->merge($zone, $before, ['observations' => [[...$row, 'measure_kind' => 'inferred_from_description']]]);
$this->fail('Invalid nature accepted');
} catch (ValidationException $error) {
$this->assertArrayHasKey('observations.0.measure_kind', $error->errors());
}
}
public function test_nature_change_stays_in_private_preparation_with_history_until_existing_editorial_review(): void
{
$zone = $this->zone();
$ops = app(InvestmentZoneOperationalDetails::class);
$row = $this->observation('observed');
$fields = app(InvestmentZoneIntegralDetails::class)->merge($zone, [], ['observations' => [$row]]);
// Synthetic public snapshot only, inside the rolled-back QA transaction.
$zone->forceFill(['publication_status' => 'published', 'validation_status' => 'validated',
'reviewer_id' => User::factory()->create(['email' => 'nature-review-'.Str::uuid().'@example.test'])->id,
'published_at' => now(), 'physical_category' => 'industrial_park',
'source_evidence' => [['source_type' => 'promoter', 'title' => 'SYNTHETIC QA only', 'url' => 'https://example.invalid/qa', 'supports' => 'Synthetic test fixture']],
'operational_details' => $ops->pack(['integral' => $fields])])->save();
$original = $zone->fresh()->getRawOriginal('operational_details');
$profile = app(InvestmentZoneOperationalProfile::class);
$prepared = $profile->prepare($zone->fresh(), ['version_no' => 1,
'operational_details' => ['integral' => ['observations' => [[...$row, 'measure_kind' => 'planned']]]]], $zone->editor_id);
$this->assertSame($original, $prepared->getRawOriginal('operational_details'));
$this->assertSame('observed', $ops->publicData($prepared)['integral']['observations'][0]['measure_kind']);
$this->assertSame('planned', $profile->draft($prepared)['fields']['operational_details']['integral']['observations'][0]['measure_kind']);
$this->assertSame(1, $prepared->version_no);
$this->assertSame(1, DB::table('investment_zone_revisions')->where('zone_id', $zone->id)->where('action', 'profile_prepared')->count());
$this->assertSame(1, DB::table('audit_events')->where('entity_id', $zone->id)->where('action', 'profile_prepared')->count());
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcAdvanceCashflowDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_pec_obligation_and_calendar_survive_native_draft_without_new_paid_deductions(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('investmoz_demography_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$actor = User::factory()->create();
$this->actingAs($actor);
$id = null;
$etag = null;
foreach (['overlap' => 138, 'distributed' => 138, 'partial' => 138, 'zero' => 138] as $name => $balance) {
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-cashflow-group-20261113/'.$name.'.json')), true, flags: JSON_THROW_ON_ERROR);
$personal = ['person_type' => $snapshot['person_type']];
unset($snapshot['person_type']);
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $snapshot];
if ($id === null) {
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
} else {
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
}
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
$etag = $response->headers->get('ETag');
$back = $response->json('data.economic_snapshot');
$this->assertSame($snapshot['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$book = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$snapshot['context_opportunity']];
$this->assertCount(1, $book['history']);
$this->assertSame(['numerator' => '138', 'denominator' => '1'], $book['history'][0]['model']['arithmeticBalance']);
$this->assertSame($balance === null ? null : ['numerator' => (string) $balance, 'denominator' => '1'], $book['result']['model']['arithmeticBalance']);
$this->assertSame($book['pecObligation'], $book['result']['model']['input']['pecObligation']);
$this->assertSame($book['ppcObligation'], $book['result']['model']['input']['ppcObligation']);
$this->assertSame('2026', $book['ppcObligation']['year']);
$this->assertSame('SYNTHETIC effective activity'.($name === 'start' ? '2026' : '2020'), $book['pecObligation']['activityReference']);
file_put_contents(base_path('outputs/irpc-cashflow-group-20261113/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcAdvancePeriodDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_period_survives_native_recovery_without_changing_the_independent_annual_result(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('investmoz_demography_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$actor = User::factory()->create();
$this->actingAs($actor);
$id = null;
$etag = null;
foreach (['ordinary', 'noncivil', 'short', 'start'] as $name) {
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-period-consistency-20261113/'.$name.'.json')), true, flags: JSON_THROW_ON_ERROR);
$personal = ['person_type' => $snapshot['person_type']];
unset($snapshot['person_type']);
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $snapshot];
if ($id === null) {
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
} else {
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
}
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
$etag = $response->headers->get('ETag');
$back = $response->json('data.economic_snapshot');
$this->assertSame($snapshot['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$book = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$snapshot['context_opportunity']];
$this->assertCount(1, $book['history']);
$this->assertArrayHasKey('advancePeriod', $book['result']['model']);
$this->assertSame($book['history'][0]['model']['arithmeticBalance'], $book['result']['model']['arithmeticBalance']);
$this->assertSame($book['pecObligation'], $book['result']['model']['input']['pecObligation']);
$this->assertSame($book['ppcObligation'], $book['result']['model']['input']['ppcObligation']);
$this->assertSame('2026', $book['ppcObligation']['year']);
$this->assertSame('SYNTHETIC effective activity'.($name === 'start' ? '2026' : '2020'), $book['pecObligation']['activityReference']);
file_put_contents(base_path('outputs/irpc-period-consistency-20261113/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IrpcPecLastAmountDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_pec_obligation_and_calendar_survive_native_draft_without_new_paid_deductions(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('investmoz_demography_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$actor = User::factory()->create();
$this->actingAs($actor);
$id = null;
$etag = null;
foreach (['documented' => 138, 'missing' => 138, 'positive' => 138, 'excess' => 138] as $name => $balance) {
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-pec-last-amount-20261113/'.$name.'.json')), true, flags: JSON_THROW_ON_ERROR);
$personal = ['person_type' => $snapshot['person_type']];
unset($snapshot['person_type']);
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $snapshot];
if ($id === null) {
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
} else {
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
}
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
$etag = $response->headers->get('ETag');
$back = $response->json('data.economic_snapshot');
$this->assertSame($snapshot['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$book = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$snapshot['context_opportunity']];
$this->assertCount(1, $book['history']);
$this->assertSame(['numerator' => '138', 'denominator' => '1'], $book['history'][0]['model']['arithmeticBalance']);
$this->assertSame($balance === null ? null : ['numerator' => (string) $balance, 'denominator' => '1'], $book['result']['model']['arithmeticBalance']);
$this->assertSame($book['pecObligation'], $book['result']['model']['input']['pecObligation']);
$this->assertSame('42500.25', $book['pecObligation']['lastAmount']['amount']);
$this->assertSame('2024', $book['pecObligation']['lastAmount']['year']);
$this->assertSame('SYNTHETIC effective activity'.($name === 'start' ? '2026' : '2020'), $book['pecObligation']['activityReference']);
file_put_contents(base_path('outputs/irpc-pec-last-amount-20261113/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,23 @@
import {combinedCase} from './irpc-investment-combination-case.mjs';
import {plan as ppc} from './irpc-ppc-obligation-case.mjs';
import {plan as pec} from './irpc-pec-obligation-case.mjs';
import {newPecPlan} from '../../../resources/js/lib/irpc-pec-plan.ts';
import {normalBook,recordNormal,saveNormalBook} from '../../../resources/js/lib/irpc-normal.ts';
import {newNormalPeriodFx} from '../../../resources/js/lib/irpc-normal-period-fx.ts';
import {annualAssessment,reuseAnnualAssessment} from '../../../resources/js/lib/irpc-annual-settlement.ts';
export function periodAdvanceCase(kind='ordinary'){
const c=combinedCase();
if(kind!=='ordinary'){
const n=normalBook(c.d);
const dates=kind==='noncivil'?['annual','2025-07-01','2026-06-30','2025-07-01']:kind==='start'?['start','2026-07-01','2026-12-31','2026-01-01']:['cessation','2026-01-01','2026-06-30','2026-01-01'];
n.periodsFx={...newNormalPeriodFx(),kind:dates[0],from:dates[1],to:dates[2],planStart:dates[3],reference:'SYNTHETIC documented fiscal period '+kind,mznMapsReference:'SYNTHETIC existing MZN mapping; no real tax filing'};
c.d.modelo_a_snapshot=saveNormalBook(c.d,recordNormal(c.d,n,'2026-11-13T22:00:00Z'));
c.b=reuseAnnualAssessment(c.d,c.b);
}
c.b.ppcObligation={...ppc(),subjectNuit:c.b.subjectNuit};
c.b.pecObligation={...pec(),subjectNuit:c.b.subjectNuit,plan:{...newPecPlan(),mode:'single',singleMonth:'2026-05',reference:'SYNTHETIC cash hypothesis'}};
if(kind==='start'){
c.b.pecObligation.activityStartYear='2026';c.b.pecObligation.activityReference='SYNTHETIC effective activity2026';c.b.ppcObligation.previousTax='0';c.b.ppcObligation.previousWithholdings='0';
}
return {...c,period:annualAssessment(c.d).period};
}
@@ -29,6 +29,8 @@ compileFunction(compiled, ['require', 'module', 'exports'])(
);
const Component = module.exports.default;
const filterIndicators = module.exports.filterMapIndicators;
const compareObservations = module.exports.compareMapObservations;
const Comparison = module.exports.MapObservationComparison;
const row = {
id: '1',
zone_id: 'park-a',
@@ -219,3 +221,170 @@ test('quantitative filters are labelled, scoped and explain source date and cove
assert.match(html, /não alteram oportunidades ou limites de cobertura/);
assert.match(html, /Limpar filtros dos indicadores/);
});
test('comparison delta: metadata alignment never manufactures a trend or changes exact source values', () => {
const first = {
...row,
description: 'Emprego observado na fonte sintética.',
measure_kind: 'observed',
value: '1234567890123.456789',
};
const second = {
...first,
id: 'next-period',
period_start: '2026-01-01',
period_end: '2026-12-31',
value: '0',
};
const before = JSON.stringify([first, second]);
assert.equal(compareObservations(first, second).state, 'metadata_aligned');
const html = renderToStaticMarkup(
React.createElement(Comparison, { first, second }),
);
assert.match(html, /1234567890123\.456789/);
assert.match(html, />0</);
assert.match(html, /confirme nas fontes/);
assert.match(html, /não calcula evolução/);
assert.ok(!html.includes('<script>synthetic source</script>'));
assert.match(html, /&lt;script&gt;synthetic source/);
assert.equal(JSON.stringify([first, second]), before);
});
test('comparison delta: differences, missing evidence, unknown values and disputed information require review', () => {
const documented = {
...row,
description: 'Emprego observado na fonte sintética.',
measure_kind: 'observed',
};
const second = { ...documented, id: 'different' };
assert.equal(
compareObservations(documented, second).state,
'metadata_aligned',
);
for (const change of [
{ zone_id: 'other' },
{ indicator: 'exports' },
{ unit: 'USD' },
{ scope: 'other base' },
{ population: 'other population' },
{ methodology: 'other method' },
{ knowledge: 'disputed' },
{ knowledge: 'unknown', value: null },
{ value: 'NaN' },
{ source_reference: '' },
{ description: '' },
{ source_date: '2026-02-30' },
{ period_end: '2024-01-01' },
]) {
const result = compareObservations(documented, {
...second,
...change,
});
assert.equal(result.state, 'review_required', JSON.stringify(change));
assert.ok(result.issues.length);
}
assert.equal(compareObservations(row, row).state, 'review_required');
const html = renderToStaticMarkup(
React.createElement(Comparison, {
first: row,
second: { ...second, knowledge: 'unknown', value: null },
}),
);
assert.match(html, /Por confirmar/);
assert.match(html, /lacunas a conferir/);
});
test('measure kind delta: legacy nature stays unknown and observed quantities cannot align with plans or projections', () => {
const observed = {
...row,
description: 'Fonte sintética',
measure_kind: 'observed',
};
const same = { ...observed, id: 'second' };
assert.equal(compareObservations(observed, same).state, 'metadata_aligned');
for (const kind of ['planned', 'projected', 'unknown', null, undefined]) {
const other = { ...same, measure_kind: kind };
const before = JSON.stringify(other);
const result = compareObservations(observed, other);
assert.equal(result.state, 'review_required');
assert.ok(
result.issues.some((issue) =>
issue.includes('Natureza do valor diferente'),
),
);
assert.equal(JSON.stringify(other), before);
}
const legacy = compareObservations(
{ ...observed, measure_kind: undefined },
{ ...same, measure_kind: undefined },
);
assert.equal(legacy.state, 'review_required');
assert.ok(
legacy.issues.every((issue) =>
issue.includes('natureza do valor por confirmar'),
),
);
});
test('measure kind delta: public comparison labels nature independently of documentary confidence and retains zero', () => {
const first = {
...row,
description: 'Fonte sintética',
measure_kind: 'observed',
};
const second = { ...first, id: 'plan', measure_kind: 'planned' };
const html = renderToStaticMarkup(
React.createElement(Comparison, { first, second }),
);
assert.match(html, /Observado \/ efectivo/);
assert.match(html, /Planeado \/ meta/);
assert.match(html, /Documentada/);
assert.match(html, />0</);
const legacyHtml = renderToStaticMarkup(
React.createElement(Comparison, {
first: { ...first, measure_kind: undefined },
second,
}),
);
assert.match(legacyHtml, /natureza do valor por confirmar/);
assert.match(legacyHtml, /Por confirmar/);
});
test('comparison delta: progressive selection stays inside the received filters and shows an incomplete pair explicitly', () => {
const html = render(context);
assert.match(html, /Comparar duas observações/);
assert.match(html, /Primeira.*observação/);
assert.match(html, /Segunda.*observação/);
assert.match(html, /uma versão nova exige nova selecção/);
assert.match(html, /Seleccione duas observações/);
const single = render({ ...context, records: [row] });
assert.ok(!single.includes('Comparar duas observações'));
});
test('temporal delta: unequal duration and overlapping boundaries require review without annualising values', () => {
const first = {...row, description:'Synthetic source', measure_kind:'observed', period_start:'2024-01-01', period_end:'2024-12-31', value:'123.4500'};
const second = {...first, id:'second', period_start:'2025-01-01', period_end:'2025-12-31', value:'0'};
const before = JSON.stringify([first,second]);
assert.ok(compareObservations(first,second).issues.some(x=>x.includes('Duração dos períodos diferente')));
const boundary = {...second, period_start:'2024-12-31'};
assert.ok(compareObservations(first,boundary).issues.some(x=>x.includes('sobrepõem-se')));
const identical = {...first,id:'same-period'};
assert.equal(compareObservations(first,identical).state,'metadata_aligned');
const html = renderToStaticMarkup(React.createElement(Comparison,{first,second}));
assert.match(html,/Duração do período/);assert.match(html,/>366</);assert.match(html,/>365</);
assert.match(html,/123\.4500/);assert.match(html,/>0</);
assert.equal(JSON.stringify([first,second]),before);
});
test('temporal delta: missing invalid and inverted dates never become a duration', () => {
const first = {...row, description:'Synthetic', measure_kind:'observed'};
for (const change of [{period_start:null},{period_start:'2025'},{period_start:'2025-02-30'},{period_start:'2026-01-01'}]) {
const second = {...first,id:'invalid',...change};
const result = compareObservations(first,second);
assert.ok(result.issues.some(x=>x.includes('período incompleto ou inválido')));
assert.ok(!result.issues.some(x=>x.includes('Duração dos períodos diferente')));
const html = renderToStaticMarkup(React.createElement(Comparison,{first,second}));
assert.match(html,/Por confirmar/);assert.ok(!html.includes('NaN'));
}
});
@@ -0,0 +1,31 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import {compileFunction} from 'node:vm';
import ts from 'typescript';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import * as annual from '../../resources/js/lib/irpc-annual-settlement.ts';
import {combinedCase} from './helpers/irpc-investment-combination-case.mjs';
import {plan as ppc} from './helpers/irpc-ppc-obligation-case.mjs';
import {plan as pec} from './helpers/irpc-pec-obligation-case.mjs';
import {newPecPlan} from '../../resources/js/lib/irpc-pec-plan.ts';
const require=createRequire(import.meta.url),module={exports:{}};
const source=fs.readFileSync(new URL('../../resources/js/components/simulation-irpc-annual.tsx',import.meta.url),'utf8');
const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
compileFunction(code,['require','module','exports'])(name=>name==='@/lib/irpc-annual-settlement'?annual:name.startsWith('./')?new Proxy({},{get:()=>()=>null}):require(name),module,module.exports);
const Summary=module.exports.SimulationIrpcAdvanceCashflow;
const book=()=>({...combinedCase().b,ppcObligation:ppc(),pecObligation:{...pec(),plan:{...newPecPlan(),mode:'single',singleMonth:'2026-05',reference:'SYNTHETIC cash choice'}}});
const render=b=>renderToStaticMarkup(React.createElement(Summary,{book:b}));
test('joint view labels monthly contributions and distinguishes a partial subtotal from a complete forecast',()=>{
const b=book(),html=render(b);assert.match(html,/Saídas previstas PPC\/PEC por mês/);assert.match(html,/112000\.00/);assert.match(html,/64000\.00/);assert.match(html,/não acrescenta outra saída/);assert.match(html,/prazo legal/);
delete b.pecObligation;const partial=render(b);assert.match(partial,/Projecção parcial/);assert.match(partial,/Subtotal conhecido/);assert.match(partial,/Por determinar/);assert.ok(!partial.includes('Total da projecção:'));
});
test('explicit zero has a complete zero forecast and legacy absence does not create a forecast',()=>{
const b=book();b.ppcObligation.previousTax='0';b.ppcObligation.previousWithholdings='0';b.pecObligation.previousPpcPaid='60000';const html=render(b);assert.match(html,/Total da projecção: 0\.00 MZN/);assert.match(html,/Sem saída PPC\/PEC estimada/);
delete b.ppcObligation;delete b.pecObligation;assert.equal(render(b),'');const {d}=combinedCase(),prepared=book();d.modelo_a_snapshot=annual.saveAnnualBook(d,annual.recordAnnual(d,prepared,'2026-11-13T21:45:00Z'));
const full=renderToStaticMarkup(React.createElement(module.exports.SimulationIrpcAnnual,{draft:d,onChange:()=>{}}));
const estimation=full.indexOf('<legend>Estimativas PPC e PEC para a viabilidade</legend>'),paid=full.indexOf('<legend>PPC documentados como pagos</legend>');
assert.ok(estimation>=0 && paid>estimation);const between=full.slice(estimation,paid);assert.match(between,/Saídas previstas PPC\/PEC por mês/);assert.match(between,/<\/fieldset>/);
});
@@ -0,0 +1,35 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {plan as ppcPlan} from './helpers/irpc-ppc-obligation-case.mjs';
import {plan as pecPlan} from './helpers/irpc-pec-obligation-case.mjs';
import {newPecPlan} from '../../resources/js/lib/irpc-pec-plan.ts';
import {combinedCase} from './helpers/irpc-investment-combination-case.mjs';
import {annualAdvanceCashflow,annualAdvanceCashflowReport,annualHtml,recordAnnual,saveAnnualBook,annualBook,annualCurrent} from '../../resources/js/lib/irpc-annual-settlement.ts';
const f=n=>({numerator:String(n),denominator:'1'});
const book=()=>({...combinedCase().b,ppcObligation:ppcPlan(),pecObligation:{...pecPlan(),plan:{...newPecPlan(),mode:'single',singleMonth:'2026-05',reference:'SYNTHETIC cash hypothesis'}}});
test('joint cash projection groups the same month once using existing exact PPC and PEC estimates',()=>{
const b=book(),before=JSON.stringify(b),m=annualAdvanceCashflow(b);
assert.equal(m.pending.length,0);assert.deepEqual(m.total,f(112000));assert.equal(m.rows.length,3);assert.deepEqual(m.rows[0],{month:'2026-05',ppc:f(24000),pec:f(40000),knownSubtotal:f(64000)});
assert.equal(JSON.stringify(b),before);
const report=annualAdvanceCashflowReport(b);assert.ok(report[0][3].includes('SYNTHETIC'));assert.ok(report.slice(1).every(r=>r[3]===''));
});
test('missing or unreconciled component stays unknown; known rows never become a complete total',()=>{
const b=book();delete b.pecObligation;let m=annualAdvanceCashflow(b);assert.equal(m.total,null);assert.equal(m.pecKnown,false);assert.deepEqual(m.knownSubtotal,f(72000));
const c=book();c.pecObligation.plan={...c.pecObligation.plan,mode:'three',june:'1',august:'1',october:'1'};m=annualAdvanceCashflow(c);assert.equal(m.total,null);assert.equal(m.pecKnown,false);assert.ok(m.pending.length);assert.equal(m.rows.length,3);
delete c.ppcObligation;m=annualAdvanceCashflow(c);assert.equal(m.total,null);assert.equal(m.rows.length,0);
});
test('confirmed zero PEC and zero PPC differ from unavailable estimates and preserve deliberate dates',()=>{
const b=book();b.pecObligation.previousPpcPaid='60000';let m=annualAdvanceCashflow(b);assert.equal(m.pecKnown,true);assert.deepEqual(m.total,f(72000));
b.ppcObligation.previousTax='0';b.ppcObligation.previousWithholdings='0';m=annualAdvanceCashflow(b);assert.deepEqual(m.total,f(0));assert.equal(m.ppcKnown,true);assert.equal(m.rows.length,0);
const c=book();c.pecObligation.plan.singleMonth='2026-04';m=annualAdvanceCashflow(c);assert.equal(m.rows[0].month,'2026-04');assert.deepEqual(m.rows[0].pec,f(40000));
});
test('monthly decimal allocation retains source precision and sums only the accepted cash hypotheses',()=>{
const b=book();b.pecObligation.plan={...newPecPlan(),mode:'three',june:'10000.01',august:'14999.99',october:'15000',reference:'SYNTHETIC three explicit amounts'};
const m=annualAdvanceCashflow(b);assert.deepEqual(m.total,f(112000));assert.equal(m.rows.length,6);assert.deepEqual(m.rows.find(r=>r.month==='2026-06').pec,{numerator:'1000001',denominator:'100'});
});
test('recovery and frozen report use the same cash projection without adding to the annual balance or rewriting history',()=>{
const {d}=combinedCase(),b=book();const prior=recordAnnual(d,b,'2026-11-13T21:30:00Z');d.modelo_a_snapshot=saveAnnualBook(d,prior);const recovered=annualBook(d);
assert.equal(annualCurrent(d,recovered),true);assert.deepEqual(recovered.result.model.arithmeticBalance,f(138));assert.match(annualHtml(d),/Projecção mensal conjunta PPC\/PEC/);assert.match(annualHtml(d),/64000\.00/);
const changed={...recovered,pecObligation:{...recovered.pecObligation,plan:{...recovered.pecObligation.plan,singleMonth:'2026-06'}}};assert.equal(annualCurrent(d,changed),false);
assert.deepEqual(annualAdvanceCashflow(changed).total,f(112000));assert.match(annualHtml(d),/PPC\/PEC — 2026-05/);assert.equal(annualAdvanceCashflowReport({...b,ppcObligation:undefined,pecObligation:undefined}).length,0);
});
@@ -0,0 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import {compileFunction} from 'node:vm';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import ts from 'typescript';
import * as annual from '../../resources/js/lib/irpc-annual-settlement.ts';
import * as pec from '../../resources/js/lib/irpc-pec-obligation.ts';
import * as rational from '../../resources/js/lib/fiscal-rational.ts';
import {periodAdvanceCase} from './helpers/irpc-advance-period-case.mjs';
const require=createRequire(import.meta.url);
function load(name,resolver){const module={exports:{}};const s=fs.readFileSync(new URL('../../resources/js/components/'+name,import.meta.url),'utf8');const code=ts.transpileModule(s,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;compileFunction(code,['require','module','exports'])(resolver,module,module.exports);return module.exports;}
test('L8 supplies the actual prepared L6 period to both estimators and the joint view',()=>{
const seen={};const components=load('simulation-irpc-annual.tsx',name=>name==='@/lib/irpc-annual-settlement'?annual:name==='./simulation-irpc-ppc-obligation'?{SimulationIrpcPpcObligation:p=>(seen.ppc=p.period,null)}:name==='./simulation-irpc-pec-obligation'?{SimulationIrpcPecObligation:p=>(seen.pec=p.period,null)}:name.startsWith('./')?new Proxy({},{get:()=>()=>null}):require(name));
const {d,b,period}=periodAdvanceCase('noncivil');d.modelo_a_snapshot=annual.saveAnnualBook(d,annual.recordAnnual(d,b,'2026-11-13T22:04:00Z'));const html=renderToStaticMarkup(React.createElement(components.SimulationIrpcAnnual,{draft:d,onChange:()=>{}}));
assert.deepEqual(seen.ppc,period);assert.deepEqual(seen.pec,period);assert.match(html,/Período preparado em L6/);assert.match(html,/2025-07-01/);assert.match(html,/Projecção parcial/);assert.ok(!html.includes('Total da projecção: 112000'));
});
test('the existing PEC start-year exclusion remains visible with a short source period',()=>{
const components=load('simulation-irpc-pec-obligation.tsx',name=>name==='@/lib/irpc-pec-obligation'?pec:name==='@/lib/fiscal-rational'?rational:name.startsWith('./')?new Proxy({},{get:()=>()=>null}):require(name));
const {b,period}=periodAdvanceCase('start');const html=renderToStaticMarkup(React.createElement(components.SimulationIrpcPecObligation,{book:b,period,onChange:()=>{}}));assert.match(html,/Não aplicável no ano de início/);assert.ok(!html.includes('não corresponde ao ano civil'));
});
@@ -0,0 +1,39 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {periodAdvanceCase} from './helpers/irpc-advance-period-case.mjs';
import {normalBook,saveNormalBook} from '../../resources/js/lib/irpc-normal.ts';
import {newNormalPeriodFx} from '../../resources/js/lib/irpc-normal-period-fx.ts';
import {calculatePpcObligation} from '../../resources/js/lib/irpc-ppc-obligation.ts';
import {calculatePecObligation} from '../../resources/js/lib/irpc-pec-obligation.ts';
import {annualAdvancePeriod,annualAdvanceCashflow,annualAdvanceCashflowReport,recordAnnual,saveAnnualBook,annualBook,annualCurrent,annualHtml} from '../../resources/js/lib/irpc-annual-settlement.ts';
const f=n=>({numerator:String(n),denominator:'1'});
test('ordinary calendar estimates and legacy independent preparation retain accepted amounts',()=>{
const {b,period}=periodAdvanceCase();assert.deepEqual(annualAdvanceCashflow(b,period).total,f(112000));assert.deepEqual(annualAdvanceCashflow(b).total,f(112000));
});
test('actual L6 noncivil and short periods cannot produce ordinary PPC PEC dates or a complete cash total',()=>{
for(const kind of ['noncivil','short']){
const {b,period}=periodAdvanceCase(kind),before=JSON.stringify(b);assert.ok(period);assert.ok(period.to!=='2026-12-31');
const ppc=calculatePpcObligation(b.ppcObligation,b.subjectNuit,b.year,b.assessment,period),pec=calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,period),joint=annualAdvanceCashflow(b,period);
assert.equal(ppc.total,null);assert.equal(pec.total,null);assert.equal(joint.total,null);assert.deepEqual(joint.rows,[]);assert.match(joint.pending.join(' '),/período L6/);assert.equal(JSON.stringify(b),before);
assert.ok(!JSON.stringify(annualAdvanceCashflowReport(b,period)).includes('112000'));
}
});
test('documented PEC activity-start exclusion survives a short L6 period while unsupported PPC remains separate',()=>{
const {b,period}=periodAdvanceCase('start');const pec=calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,period);assert.equal(pec.notApplicable,true);assert.deepEqual(pec.total,f(0));
const joint=annualAdvanceCashflow(b,period);assert.equal(joint.pecKnown,true);assert.equal(joint.ppcKnown,false);assert.equal(joint.total,null);assert.ok(joint.pending.some(p=>p.startsWith('PPC: o período L6')));assert.ok(!joint.pending.some(p=>p.startsWith('PEC: o período L6')));
for(const outside of [{from:'2025-07-01',to:'2026-06-30'},{from:'2025-01-01',to:'2025-12-31'},{from:'2026-02-30',to:'2026-12-31'}]) assert.equal(calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,outside).total,null);
});
test('recovery and frozen reports use the recorded L6 period without changing the independent annual calculation',()=>{
const {d,b,period}=periodAdvanceCase('noncivil');const without={...b,ppcObligation:undefined,pecObligation:undefined};const prior=recordAnnual(d,without,'2026-11-13T22:01:00Z');
const current=recordAnnual(d,{...prior,ppcObligation:b.ppcObligation,pecObligation:b.pecObligation},'2026-11-13T22:02:00Z');assert.deepEqual(current.result.model.arithmeticBalance,prior.result.model.arithmeticBalance);
d.modelo_a_snapshot=saveAnnualBook(d,current);const recovered=annualBook(d);assert.equal(annualCurrent(d,recovered),true);assert.equal(recovered.history.length,1);assert.deepEqual(recovered.result.model.assessment.period,period);assert.match(annualHtml(d),/período L6/);assert.ok(!annualHtml(d).includes('Total da projecção: 112000'));
});
test('incomplete invalid and contradictory calendar metadata require reconciliation rather than replacing dates',()=>{
const {b}=periodAdvanceCase();for(const period of [{from:'2026-01-01',to:null},{from:'2026-02-30',to:'2026-12-31'},{from:'2025-01-01',to:'2025-12-31'}])assert.equal(annualAdvanceCashflow(b,period).total,null);
});
test('edited L6 period is not silently replaced by the prior calculated period and is frozen on annual recalculation',()=>{
const {d,b}=periodAdvanceCase();const n=normalBook(d);n.periodsFx={...newNormalPeriodFx(),kind:'cessation',from:'2026-01-01',to:'2026-06-30',planStart:'2026-01-01',reference:'SYNTHETIC period edit',mznMapsReference:'SYNTHETIC pending review'};
d.modelo_a_snapshot=saveNormalBook(d,n);const period=annualAdvancePeriod(d);assert.equal(period.to,'2026-06-30');assert.equal(annualAdvanceCashflow(b,period).total,null);
const saved=recordAnnual(d,b,'2026-11-13T22:03:00Z');assert.deepEqual(saved.result.model.advancePeriod,period);d.modelo_a_snapshot=saveAnnualBook(d,saved);assert.match(annualHtml(d),/período L6/);
});
@@ -0,0 +1,55 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {plan} from './helpers/irpc-pec-obligation-case.mjs';
import {combinedCase} from './helpers/irpc-investment-combination-case.mjs';
import {calculatePecObligation,validatePecObligation} from '../../resources/js/lib/irpc-pec-obligation.ts';
import {recordAnnual,saveAnnualBook,annualBook,annualCurrent,annualHtml} from '../../resources/js/lib/irpc-annual-settlement.ts';
import {calculatePecPlan,newPecPlan} from '../../resources/js/lib/irpc-pec-plan.ts';
const f=n=>({numerator:String(n),denominator:'1'});
const sample=()=>{const p={...plan(),previousTurnover:'0.00'};return {...p,lastAmount:{version:1,subjectNuit:p.subjectNuit,year:'2024',amount:'42500.25',reference:'SYNTHETIC final PEC amount; latest applicable annual ordinary MZN reference',confirmed:'yes'}}};
const calc=p=>calculatePecObligation(p,p.subjectNuit,p.year,'normal');
test('documented latest PEC amount is retained without percentage, caps or a second PPC deduction',()=>{
const p=sample(),before=JSON.stringify(p),m=calc(p);
assert.deepEqual(m.total,{numerator:'170001',denominator:'4'});assert.equal(m.percentage,null);assert.equal(m.limited,null);assert.equal(m.lastAmountApplied,true);assert.equal(m.requiresLastAmount,true);
assert.equal(m.paymentExecuted,false);assert.equal(m.annualDeductionCreated,false);assert.equal(JSON.stringify(p),before);
p.lastAmount.amount='0';assert.deepEqual(calc(p).total,f(0));
});
test('absent stale unconfirmed unsupported or malformed historical references never determine the estimate',()=>{
for(const changes of [{confirmed:''},{reference:''},{subjectNuit:'999999999'},{year:'2026'},{year:'2019'},{amount:''},{amount:'-1'},{amount:'NaN'}]){const p=sample();Object.assign(p.lastAmount,changes);assert.equal(calc(p).total,null,JSON.stringify(changes));}
const p=sample();delete p.lastAmount;assert.equal(calc(p).total,null);
assert.throws(()=>validatePecObligation({...sample(),lastAmount:{...sample().lastAmount,amount:null}}));
});
test('ordinary positive turnover and activity start do not use or erase a saved historical reference',()=>{
const p=sample();p.previousTurnover='10000000';assert.deepEqual(calc(p).total,f(40000));assert.equal(calc(p).lastAmountApplied,false);assert.equal(p.lastAmount.amount,'42500.25');
p.activityStartYear='2026';assert.deepEqual(calc(p).total,f(0));assert.equal(calc(p).lastAmountApplied,false);
});
test('native annual recovery and report retain history and require reconfirming cash assumptions after the estimate changes',()=>{
const {d,b}=combinedCase();b.pecObligation={...sample(),subjectNuit:b.subjectNuit,lastAmount:{...sample().lastAmount,subjectNuit:b.subjectNuit}};
const prior=recordAnnual(d,b,'2026-11-13T21:00:00Z');
const changed={...prior,pecObligation:{...prior.pecObligation,lastAmount:{...prior.pecObligation.lastAmount,amount:'50000'}}};
assert.equal(annualCurrent(d,changed),false);
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,changed,'2026-11-13T21:01:00Z'));
const recovered=annualBook(d);assert.equal(annualCurrent(d,recovered),true);assert.equal(recovered.history.length,1);assert.deepEqual(recovered.result.model.arithmeticBalance,f(138));
assert.match(annualHtml(d),/PEC — último montante documentado/);assert.match(annualHtml(d),/50000/);
const cash={...newPecPlan(),mode:'three',june:'42500.25',august:'0',october:'0',reference:'SYNTHETIC prior cash hypothesis'};
const cm=calculatePecPlan(cash,calc(recovered.pecObligation).total,false,'2026');assert.ok(cm.pending.length);
});
test('unknown turnover cannot be bypassed and a documented correction to zero reuses the historical reference once',()=>{
const p=sample();p.previousTurnover='';assert.equal(calc(p).total,null);
p.previousTurnover='10000000';p.correction={version:1,apply:'yes',originalTurnover:p.previousTurnover,revisedTurnover:'0.00',subjectNuit:p.subjectNuit,turnoverYear:'2025',reference:'SYNTHETIC corrected sales to zero'};
assert.equal(calc(p).requiresLastAmount,true);assert.equal(calc(p).lastAmountApplied,true);assert.deepEqual(calc(p).total,{numerator:'170001',denominator:'4'});
p.correction.reference='';assert.equal(calc(p).total,null);
p.correction.apply='no';assert.deepEqual(calc(p).total,f(40000));assert.equal(calc(p).lastAmountApplied,false);
});
test('excess documented PPC covers the PEC estimate without refund credit or duplicate annual deduction',()=>{
for(const [paid,expected] of [['50000','0'],['60000','10000'],['50000.01','0.01']]){
const p={...plan(),previousPpcPaid:paid};const m=calc(p);assert.deepEqual(m.total,f(0));assert.equal(m.pending.length,0);
if(expected==='0')assert.equal(m.ppcExcess,null);else assert.ok(m.ppcExcess);
assert.equal(m.paymentExecuted,false);assert.equal(m.annualDeductionCreated,false);
}
const {d,b}=combinedCase();b.pecObligation={...plan(),subjectNuit:b.subjectNuit,previousPpcPaid:'60000'};
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,b,'2026-11-13T21:20:00Z'));
const recovered=annualBook(d);assert.equal(annualCurrent(d,recovered),true);assert.deepEqual(recovered.result.model.arithmeticBalance,f(138));assert.match(annualHtml(d),/PEC — PPC acima da base limitada/);assert.match(annualHtml(d),/não cria reembolso/);
});
@@ -53,7 +53,6 @@ test('unknown turnover is distinct from zero; no turnover requires29(5),excess P
{ previousTurnover: '' },
{ previousTurnover: '0' },
{ previousPpcPaid: '' },
{ previousPpcPaid: '60000' },
{ scope: 'other' },
{ subjectNuit: '999999999' },
{ previousYear: '2024' },
+31
View File
@@ -0,0 +1,31 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import {combinedCase} from '../tests/frontend/helpers/irpc-investment-combination-case.mjs';
import {plan as ppc} from '../tests/frontend/helpers/irpc-ppc-obligation-case.mjs';
import {plan as pec} from '../tests/frontend/helpers/irpc-pec-obligation-case.mjs';
import {newPecPlan} from '../resources/js/lib/irpc-pec-plan.ts';
import {annualAdvanceCashflow,annualBook,annualCurrent,annualHtml,recordAnnual,saveAnnualBook} from '../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
const out=new URL('../outputs/irpc-cashflow-group-20261113/',import.meta.url);fs.mkdirSync(out,{recursive:true});
for(const name of ['overlap','distributed','partial','zero']){
if(process.argv[2]==='generate'){
const {d,b}=combinedCase();const prior=recordAnnual(d,b,'2026-11-13T21:40:00Z');
prior.ppcObligation={...ppc(),subjectNuit:b.subjectNuit};
prior.pecObligation={...pec(),subjectNuit:b.subjectNuit,plan:{...newPecPlan(),mode:'single',singleMonth:'2026-05',reference:'SYNTHETIC cash hypothesis'}};
if(name==='distributed')prior.pecObligation.plan={...newPecPlan(),mode:'three',june:'10000.01',august:'14999.99',october:'15000',reference:'SYNTHETIC explicit allocation'};
if(name==='partial')delete prior.pecObligation.plan;
if(name==='zero'){prior.ppcObligation.previousTax='0';prior.ppcObligation.previousWithholdings='0';prior.pecObligation.previousPpcPaid='60000';}
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,prior,'2026-11-13T21:41:00Z'));
fs.writeFileSync(new URL(name+'.json',out),JSON.stringify(d));
}else{
const d=JSON.parse(fs.readFileSync(new URL('recovered-'+name+'.json',out),'utf8')),b=annualBook(d),before=JSON.stringify(b),m=annualAdvanceCashflow(b);
assert.equal(annualCurrent(d,b),true);assert.equal(b.history.length,1);assert.deepEqual(b.result.model.arithmeticBalance,{numerator:'138',denominator:'1'});assert.equal(JSON.stringify(b),before);
assert.deepEqual(m.total,name==='partial'?null:{numerator:name==='zero'?'0':'112000',denominator:'1'});
if(name==='overlap')assert.deepEqual(m.rows[0].knownSubtotal,{numerator:'64000',denominator:'1'});
if(name==='partial')assert.equal(m.pecKnown,false);
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-11-13T21:42:00Z'});
assert.match(html,/Projecção mensal conjunta PPC\/PEC/);assert.match(JSON.stringify(report),/Projecção mensal conjunta PPC\/PEC/);
fs.writeFileSync(new URL(name+'-report.html',out),html);fs.writeFileSync(new URL(name+'-verification.json',out),JSON.stringify({synthetic:true,nativeApiRecovery:true,annualCurrent:true,historyPreserved:true,annualBalanceUnchanged:true,report:true,model:m},null,2));
}
}
console.log(process.argv[2]==='generate'?'Generated four synthetic cash projection cases':'Verified four recovered cash projections and dossier reports');
+23
View File
@@ -0,0 +1,23 @@
<?php
$root = dirname(__DIR__);
if (($argv[1] ?? '') !== 'test' || basename($root) !== 'portal-investidor') {
throw new RuntimeException('Explicit isolated QA test operation required');
}
foreach (['APP_ENV' => 'testing', 'APP_KEY' => 'base64:'.base64_encode(str_repeat('Q', 32)),
'APP_CONFIG_CACHE' => $root.'/outputs/irpc-cashflow-group-20261113/no-config.php', 'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441',
'DB_DATABASE' => 'investmoz_demography_test', 'DB_USERNAME' => 'demography_qa', 'DB_PASSWORD' => '', 'DB_URL' => '', 'DATABASE_URL' => '',
'CACHE_STORE' => 'array', 'SESSION_DRIVER' => 'array', 'QUEUE_CONNECTION' => 'sync', 'MAIL_MAILER' => 'array', 'SUITECRM_ENABLED' => 'false'] as $key => $value) {
putenv($key.'='.$value);
$_ENV[$key] = $_SERVER[$key] = $value;
}
$pdo = new PDO('pgsql:host=127.0.0.1;port=55441;dbname=investmoz_demography_test', 'demography_qa', '');
$identity = $pdo->query('select current_database(),inet_server_port()')->fetch(PDO::FETCH_NUM);
if ($identity !== ['investmoz_demography_test', 55441] && $identity !== ['investmoz_demography_test', '55441']) {
throw new RuntimeException('Isolated QA identity required');
}
$argv = ['phpunit', '-c', 'phpunit.investmoz.xml', '--do-not-cache-result', 'tests/Feature/IrpcAdvanceCashflowDraftTest.php'];
$_SERVER['argv'] = $argv;
$_SERVER['argc'] = count($argv);
require $root.'/vendor/autoload.php';
require $root.'/vendor/phpunit/phpunit/phpunit';
+22
View File
@@ -0,0 +1,22 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import {periodAdvanceCase} from '../tests/frontend/helpers/irpc-advance-period-case.mjs';
import {annualAdvanceCashflow,annualAdvancePeriod,annualBook,annualCurrent,annualHtml,recordAnnual,saveAnnualBook} from '../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
const out=new URL('../outputs/irpc-period-consistency-20261113/',import.meta.url);fs.mkdirSync(out,{recursive:true});
for(const name of ['ordinary','noncivil','short','start']){
if(process.argv[2]==='generate'){
const {d,b}=periodAdvanceCase(name);const prior=recordAnnual(d,{...b,ppcObligation:undefined,pecObligation:undefined},'2026-11-13T22:05:00Z');
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,{...prior,ppcObligation:b.ppcObligation,pecObligation:b.pecObligation},'2026-11-13T22:06:00Z'));fs.writeFileSync(new URL(name+'.json',out),JSON.stringify(d));
}else{
const d=JSON.parse(fs.readFileSync(new URL('recovered-'+name+'.json',out),'utf8')),b=annualBook(d),period=annualAdvancePeriod(d),m=annualAdvanceCashflow(b,period);
assert.equal(annualCurrent(d,b),true);assert.equal(b.history.length,1);assert.deepEqual(b.result.model.arithmeticBalance,b.history[0].model.arithmeticBalance);assert.deepEqual(b.result.model.advancePeriod,period);
if(name==='ordinary')assert.deepEqual(m.total,{numerator:'112000',denominator:'1'});else{assert.equal(m.total,null);assert.equal(m.rows.length,0);}
if(name==='start')assert.equal(m.pecKnown,true);
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-11-13T22:07:00Z'});
assert.match(html,/Projecção mensal conjunta PPC\/PEC/);assert.match(JSON.stringify(report),/Projecção mensal conjunta PPC\/PEC/);
if(name!=='ordinary'){assert.match(html,/período L6/);assert.ok(!html.includes('Total da projecção: 112000'));}
fs.writeFileSync(new URL(name+'-report.html',out),html);fs.writeFileSync(new URL(name+'-verification.json',out),JSON.stringify({synthetic:true,nativeApiRecovery:true,annualCurrent:true,historyPreserved:true,annualCalculationUnchanged:true,report:true,period,model:m},null,2));
}
}
console.log(process.argv[2]==='generate'?'Generated four synthetic period consistency cases':'Verified four recovered period contexts and reports');
+23
View File
@@ -0,0 +1,23 @@
<?php
$root = dirname(__DIR__);
if (($argv[1] ?? '') !== 'test' || basename($root) !== 'portal-investidor') {
throw new RuntimeException('Explicit isolated QA test operation required');
}
foreach (['APP_ENV' => 'testing', 'APP_KEY' => 'base64:'.base64_encode(str_repeat('Q', 32)),
'APP_CONFIG_CACHE' => $root.'/outputs/irpc-period-consistency-20261113/no-config.php', 'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441',
'DB_DATABASE' => 'investmoz_demography_test', 'DB_USERNAME' => 'demography_qa', 'DB_PASSWORD' => '', 'DB_URL' => '', 'DATABASE_URL' => '',
'CACHE_STORE' => 'array', 'SESSION_DRIVER' => 'array', 'QUEUE_CONNECTION' => 'sync', 'MAIL_MAILER' => 'array', 'SUITECRM_ENABLED' => 'false'] as $key => $value) {
putenv($key.'='.$value);
$_ENV[$key] = $_SERVER[$key] = $value;
}
$pdo = new PDO('pgsql:host=127.0.0.1;port=55441;dbname=investmoz_demography_test', 'demography_qa', '');
$identity = $pdo->query('select current_database(),inet_server_port()')->fetch(PDO::FETCH_NUM);
if ($identity !== ['investmoz_demography_test', 55441] && $identity !== ['investmoz_demography_test', '55441']) {
throw new RuntimeException('Isolated QA identity required');
}
$argv = ['phpunit', '-c', 'phpunit.investmoz.xml', '--do-not-cache-result', 'tests/Feature/IrpcAdvancePeriodDraftTest.php'];
$_SERVER['argv'] = $argv;
$_SERVER['argc'] = count($argv);
require $root.'/vendor/autoload.php';
require $root.'/vendor/phpunit/phpunit/phpunit';
+26
View File
@@ -0,0 +1,26 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import {combinedCase} from '../tests/frontend/helpers/irpc-investment-combination-case.mjs';
import {plan} from '../tests/frontend/helpers/irpc-pec-obligation-case.mjs';
import {calculatePecObligation} from '../resources/js/lib/irpc-pec-obligation.ts';
import {recordAnnual,saveAnnualBook,annualBook,annualCurrent,annualHtml} from '../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
const out=new URL('../outputs/irpc-pec-last-amount-20261113/',import.meta.url);fs.mkdirSync(out,{recursive:true});
for(const name of ['documented','missing','positive','excess']){
if(process.argv[2]==='generate'){
const {d,b}=combinedCase();const prior=recordAnnual(d,b,'2026-11-13T21:10:00Z');
prior.pecObligation={...plan(),subjectNuit:b.subjectNuit,previousTurnover:['positive','excess'].includes(name)?'10000000':'0',previousPpcPaid:name==='excess'?'60000':'10000',lastAmount:{version:1,subjectNuit:b.subjectNuit,year:'2024',amount:'42500.25',reference:name==='missing'?'':'SYNTHETIC final PEC already determined;latest applicable annual MZN reference',confirmed:'yes'}};
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,prior,'2026-11-13T21:11:00Z'));
fs.writeFileSync(new URL(name+'.json',out),JSON.stringify(d));
}else{
const d=JSON.parse(fs.readFileSync(new URL('recovered-'+name+'.json',out),'utf8')),b=annualBook(d),m=calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment);
assert.equal(annualCurrent(d,b),true);assert.equal(b.history.length,1);assert.deepEqual(b.result.model.arithmeticBalance,{numerator:'138',denominator:'1'});
const expected=name==='missing'?null:name==='excess'?{numerator:'0',denominator:'1'}:name==='positive'?{numerator:'40000',denominator:'1'}:{numerator:'170001',denominator:'4'};assert.deepEqual(m.total,expected);
if(name==='excess') {assert.deepEqual(m.ppcExcess,{numerator:'10000',denominator:'1'});}
assert.equal(m.lastAmountApplied,name==='documented');assert.equal(m.paymentExecuted,false);assert.equal(m.annualDeductionCreated,false);
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-11-13T21:12:00Z'});
assert.match(html,/PEC — último montante documentado/);assert.match(JSON.stringify(report),/PEC — último montante documentado/);
fs.writeFileSync(new URL(name+'-report.html',out),html);fs.writeFileSync(new URL(name+'-verification.json',out),JSON.stringify({synthetic:true,nativeApiRecovery:true,annualCurrent:true,historyPreserved:true,annualBalanceUnchanged:true,report:true,model:m},null,2));
}
}
console.log(process.argv[2]==='generate'?'Generated four synthetic cases':'Verified four recovered cases and dossier reports');
+23
View File
@@ -0,0 +1,23 @@
<?php
$root = dirname(__DIR__);
if (($argv[1] ?? '') !== 'test' || basename($root) !== 'portal-investidor') {
throw new RuntimeException('Explicit isolated QA test operation required');
}
foreach (['APP_ENV' => 'testing', 'APP_KEY' => 'base64:'.base64_encode(str_repeat('Q', 32)),
'APP_CONFIG_CACHE' => $root.'/outputs/irpc-pec-last-amount-20261113/no-config.php', 'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441',
'DB_DATABASE' => 'investmoz_demography_test', 'DB_USERNAME' => 'demography_qa', 'DB_PASSWORD' => '', 'DB_URL' => '', 'DATABASE_URL' => '',
'CACHE_STORE' => 'array', 'SESSION_DRIVER' => 'array', 'QUEUE_CONNECTION' => 'sync', 'MAIL_MAILER' => 'array', 'SUITECRM_ENABLED' => 'false'] as $key => $value) {
putenv($key.'='.$value);
$_ENV[$key] = $_SERVER[$key] = $value;
}
$pdo = new PDO('pgsql:host=127.0.0.1;port=55441;dbname=investmoz_demography_test', 'demography_qa', '');
$identity = $pdo->query('select current_database(),inet_server_port()')->fetch(PDO::FETCH_NUM);
if ($identity !== ['investmoz_demography_test', 55441] && $identity !== ['investmoz_demography_test', '55441']) {
throw new RuntimeException('Isolated QA identity required');
}
$argv = ['phpunit', '-c', 'phpunit.investmoz.xml', '--do-not-cache-result', 'tests/Feature/IrpcPecLastAmountDraftTest.php'];
$_SERVER['argv'] = $argv;
$_SERVER['argc'] = count($argv);
require $root.'/vendor/autoload.php';
require $root.'/vendor/phpunit/phpunit/phpunit';