feat(irpc): qualify L7 expenses and compatible special treatments in QA

This commit is contained in:
2026-10-26 12:14:52 +02:00
parent d37fffb2d2
commit ab2ff2fdbc
14 changed files with 939 additions and 12 deletions
@@ -825,3 +825,45 @@ Para cada nova regra: condição satisfeita, negativa, informação em falta, pe
Reutilizado o inventário autorizado de **8143ficheiros**, sem nova varredura de pastas. Para delimitação foram consultados os originais CIRPC/alterações já arquivados, a Lei4/2009 local e a compilação oficial [APIEX — Legislação de Investimentos em Moçambique](https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf), que reúne Lei8/2023, Decreto8/2024 e CBF4/2009. Disponibilidade, situação documental/jurídica e uso no cálculo continuam estados independentes. O acesso actual à cópia CBF supera a indisponibilidade assinalada em notas antigas; não activa regra nem apaga a nota histórica. Artigos, páginas, hashes e limites no recibo. A compilação não dispensa o período/alterações de cada tratamento, nem cria validação jurídica genérica adicional.
**Código por implementar em L7:** qualificação e persistência específicas; efeitos segregados; regras das famílias acima, por parcelas; conciliação sem duplicação; relatório/exportação correspondente. **Documentação por caso:** estatuto/reconhecimento ou enquadramento individual quando a fonte o exige; identificação de parcela/limite/cronologia; norma sectorial ainda sem correspondência; edição/campo de transporte afectado. Cada uma conserva exemplo e condição de retoma na linha correspondente. Não é necessário resolver todos os documentos para começar o primeiro incremento independente. L6 operacional não elimina este desenvolvimento; L7 delimitado não é L7 implementado; L8 conserva a liquidação final.
## 26/10/2026 — L7: qualificação, despesas autónomas e casos compatíveis em QA
**QA, sem instalação.** Base instalada preservada `5aaf1a56`; delimitação `d37fffb2`; ramo `codex/irpc-l7-common-autonomous-qa-20261026`, fotografia opcional `IRPC-normal-adjustments/10`. L6 continua operacional no âmbito demonstrado, com os cinco grupos de código e cinco documentais preservados. A implementação abaixo acrescenta apenas os tratamentos demonstrados; não conclui L7 nem o IRPC. Saldo final, retenções, pagamentos e aplicação anual de créditos permanecem em L8.
**Qualificação comum executável:** reutiliza sujeito/NUIT, âmbito, período, origem da oportunidade/dossier, sector e território; guarda condições, prova, fonte/artigo e período por tratamento. «Informação em falta», «ausência declarada», «não aplicável/condição negativa» e «calculado» têm resultados distintos. Desconhecido não vira zero. Período incompatível, identidade por reconciliar ou contradição afectam só o resultado dependente. A localização não escolhe taxa nem activa benefício. Os snapshots de origem ficam internos; o relatório apresenta contexto legível e as condições documentadas.
**Despesas61(4):** cada parcela aponta para uma linha `undocumented` do mapa36(1)g de L6, com acréscimo integral já calculado. Montante, documento e linha são reutilizados; L7 não introduz outro acréscimo nem outra despesa. A classificação legal e incidência são declaradas com prova própria; falta de anexo não classifica uma despesa. O imposto autónomo usa35% apenas nos casos qualificados. Duplicação da referência, parcela não correspondida, ausência contraditória ou informação insuficiente deixam o imposto reconciliado indeterminado; as parcelas conhecidas permanecem discriminadas. Condição negativa documentada conserva o ajustamentoL6 e não aplica imposto autónomo à parcela.
**Cooperativas12/14:** agrárias, artesanato ou culturais, residentes, com base ordinária integralmente elegível e ausência documentada de rendimentos sujeitos a retenção, aplicam redução de50% à taxa geral32%, ou16%. Não depende de seleccionar um sector semelhante. A qualificação especial pode coexistir com a opção «outro tratamento» da colectaL6: não exige fingir que a taxa reduzida é regime geral. Parcela mista, rendimentos retidos, outra entidade ou ausência contraditória conservam pendência própria; não recebem redução global presumida. O imposto autónomo de despesas não é reduzido por esta taxa.
**CBF16:** reutiliza identificadores e valores dos activosL6 suportados; imóvel novo do projecto ou máquinas industriais/agro-industriais das categorias correspondidas, com enquadramentoCBF3/9 e reconhecimento10 quando exigido (ou excepção identificada). Recorte inicial: primeiro exercício anual civil2026, método constante pela taxa normal máxima da tabela, activo desde01/01/2026, custo histórico novo, sem quotas anteriores/saída e sem cumulação de outros benefícios. Incrementa a taxa normal em50% e reutiliza limites de contabilização/saldo fiscal. Deduz apenas a diferença entre o mapa normal e o acelerado, sem alterar o activo de origem, os documentos ou os saldos oficiais. Quotas constantes gerais e demais cálculosL6 conservam o comportamento anterior quandoL7 está ausente.
| Caso sintético recuperado da API QA | Lucro/base e colecta de referênciaL6 | EfeitoL7 demonstrado | Resultado e limite |
| --- | --- | --- | --- |
| Despesa100 já acrescida36g | Lucro/base3230;colecta1033,60MZN | Novo ajustamento ao lucro0;imposto autónomo35 | Colecta combinada1068,60MZN;saldo finalL8indeterminado |
| Classificação da mesma despesa em falta | Lucro/base3230;colecta1033,60MZN preservados | Imposto autónomo indeterminado | Colecta combinada indeterminada;sem perda dos cálculos independentes |
| Condição negativa documentada na parcela | Ajustamento36g100 preservado | Parcela não aplicável;imposto autónomo0 justificado | Não confunde condição negativa com informação em falta |
| Ausência declarada de despesas autónomas | Lucro/base3130;colecta1001,60MZN | Ausência com suporte;imposto autónomo0 | Ausência contraditória com despesa36g fica pendente |
| Cooperativa integralmente abrangida | Base3130;colecta geral1001,60MZN | Taxa16%;colecta500,80MZN | Base/proveitos não alterados;sem retenções/créditos abatidos |
| Máquina industrial existente:base1000,taxa10%,quota contabilística120 | Quota normal100;lucro/base6260;colecta2003,20MZN | Quota acelerada150,mas dedução limitada120;diferença adicional20 | Lucro/base6240;colecta1996,80MZN;sem deduzir50 nem150 outra vez |
**Cálculo → gravação → recuperação → relatório:** fotografia10 opcional no mesmo livro/snapshot, chave com entradas/contexto e histórico imutável anterior. SemL7, a fotografia9 aceite e a sua chave/totais permanecem iguais. Recalcular não acumula imposto nem duplica o acréscimo. HTML e dossier financeiro mostram referênciaL6, efeitosL7, parcelas autónomas, fonte, condições, período e limitações; recuperação e download demonstrados no componente real. Uma mudança de origem/entradas torna a fotografia anterior histórica; não altera o registo anterior. M20/M22 conservam a preparaçãoL6 de referência: **este incremento não transporta efeitosL7 para campos oficiais por aproximação**, nem resolve as divergências documentais existentes.
**Provas novas:** Wayfinder `--with-form` gerou routes/actions pelo wrapper QA; TypeScript e build QA passaram. Dezanove testes dirigidos novos passaram, incluindo comparação exacta com a fotografia9 aceite, duplicação, período curto, opção especial de cooperativa, ausência contraditória, dados em falta e contexto apresentado sem despejar snapshots internos. API privada:1teste/108asserções, `simulator_fiscal_delta_20261021_test`,127.0.0.1:55441, transacção com rollback e guarda de identidade/base; seis casos recuperados com plano/livro/históricos idênticos. UI do componente real: alteração de classificação, informação em falta, ausência, cooperativa mista, cumulação pendente, gravação/recuperação local e exportação; sem erros de página. A prova local é distinta da API. Não houve testes/escrita no activo nem repetição das suites funcionais aceites de L3, ISPC, câmbio ou L5/L6.
**Entrega agrupada preparada:** qualificação + despesas autónomas, cooperativa integralmente elegível e amortizaçãoCBF16 no recorte acima. Despesa autónoma e cooperativa também foram demonstradas juntas:parcela35 sem redução,colecta sobre a base516,80 e combinada551,80MZN no exemplo de base3230. Isto não autoriza cumulação cooperativa+CBF16: essa combinação mantém resultado dependente indeterminado. A futura instalação segue o procedimento Git existente, com backup/restauro fresco e conferência do delta; não foi executada nesta passagem.
**SaldoL7 na mesma matriz:**
| Natureza | Ainda fora da cobertura / exemplo | Condição concreta de retoma |
| --- | --- | --- |
| Código | Despesa autónoma fora de linha36g calculada ou parcela ainda não segregada | Implementar origem/segregação correspondente e prova da incidência;não copiar montante para uma segunda despesa |
| Código | Cooperativa com actividade abrangida e outra não abrangida,ou rendimento sujeito a retenção | Segregar proveitos/custos/base e aplicar o tratamento apenas à parcela elegível;L5 conserva a retenção |
| Código | CBF16 em imóvel reabilitado,outra categoria,quota inferior à máxima,método degressivo,duodécimos,quotas anteriores ou exercício posterior | Corresponder regra e cronologia respectivas e reutilizar o activo/quotas existentes;pendênciaL6 limita apenas o activo dependente |
| Código/documento por caso | Cooperativa+CBF16 ou outro benefício positivo a cumular | Identificar compatibilidade,ordem e limites específicos;implementar a combinação,sem presumir soma de benefícios |
| Código | Mais-valias61-A,demais isenções/deduçõesCBF17–19,regimes sectoriais/territoriais e operações especiais | Implementar a família/parcelas já delimitadas na secção anterior;período e fonte específicos,sem nova pesquisa transversal |
| Documento por caso | Estatuto/título/excepçãoCBF ou prova das condições em falta;endereçoZEE sem estatuto | Juntar o elemento que a disposição exige ou registar condição negativa;localização isolada permanece insuficiente |
| Transporte/documento | CampoM20/M22 para efeitoL7,edição divergente ou representação não correspondida | Correspondência do campo/edição e regra de representação demonstradas;preservar o cálculo independente exacto |
| ÂmbitoL8 | Direito potencial ao créditoCBF15,aplicação anual de créditos,retenções,pagamentos e saldo final | Executar o mecanismo próprioL8;nenhuma dedução/compensação anual é presumida neste lote |
Fontes reutilizadas: inventário autorizado8143ficheiros; originaisCIRPC/alterações e CBF/compilaçãoAPIEX já identificados na delimitação,com hashes conservados. Sem activação por consulta ou revalidação jurídica genérica. Recibo: `docs/investmoz/simulations/irpc-l7-common-qa-20261026.json`. Provas locais: `outputs/irpc-l7-common/`; reprodução dirigida: `node --test tests/frontend/irpc-special.test.mjs`, `php tools/irpc-special-qa.php routes`, `php tools/irpc-special-qa.php test`, `tsc --noEmit`, build com `INVESTMOZ_QA_BUILD=1`.
@@ -0,0 +1,570 @@
{
"status": "L7_common_qualification_autonomous_expenses_cooperative_acceleration_QA_prepared_not_installed",
"recorded_date": "2026-10-26",
"installed_baseline": "5aaf1a56f9d928331364fcc8b0d1528e51dba327",
"scope_baseline": "d37fffb2d2eacef6300f1c81fc29993178532f35",
"branch": "codex/irpc-l7-common-autonomous-qa-20261026",
"version": "IRPC-normal-adjustments/10",
"installed": false,
"L6_operational_scope_preserved": true,
"IRPC_integrally_complete": false,
"runtime_delta": {
"resources/js/lib/irpc-special.ts": "5d261cdb54506bd892fa49776bb53a20600e62e64a4c180ef26cdcdb8b6a3806",
"resources/js/components/simulation-irpc-special.tsx": "9ccd89e46703f9dd14c1110459b22673623a19bd652d137f40ec83e0f7c223db",
"resources/js/lib/irpc-normal.ts": "7e63e7e5928a90c5305fd1fca49db883fec32666381ee99ae6fa4b06c711d9a7",
"resources/js/lib/irpc-normal-depreciation.ts": "00ba22e449b23e570bfbbf3895bd334029a16f938d5a5ff999ca67b4318f58c3",
"resources/js/components/simulation-irpc-normal.tsx": "e01e54dee4cd2555a8a27ed77512cf220056b5899ea9dacbaaae6d4063ce2d42",
"resources/js/lib/project-dossier-report.ts": "cba66c88391da83b4fdb345a9570eca5ae83979cbbd508eb44b53995d812718d"
},
"cases_recovered": {
"expense": {
"L6_reference": {
"taxableProfit": {
"numerator": "3230",
"denominator": "1"
},
"taxableBase": {
"numerator": "3230",
"denominator": "1"
},
"colecta": {
"numerator": "5168",
"denominator": "5"
}
},
"L7_profit": {
"numerator": "3230",
"denominator": "1"
},
"L7_base": {
"numerator": "3230",
"denominator": "1"
},
"L7_colecta": {
"numerator": "5168",
"denominator": "5"
},
"autonomous_tax": {
"numerator": "35",
"denominator": "1"
},
"combined_colecta": {
"numerator": "5343",
"denominator": "5"
},
"expense_status": "calculated",
"cooperative_status": "absent",
"acceleration_status": "absent",
"additional_depreciation_deduction": {
"numerator": "0",
"denominator": "1"
},
"history_versions": [
"IRPC-normal-adjustments/1",
"IRPC-normal-adjustments/2",
"IRPC-normal-adjustments/3",
"IRPC-normal-adjustments/4",
"IRPC-normal-adjustments/5",
"IRPC-normal-adjustments/6",
"IRPC-normal-adjustments/7",
"IRPC-normal-adjustments/8",
"IRPC-normal-adjustments/9"
],
"final_balance": {
"scope": "L8",
"payable": null,
"recoverable": null
}
},
"missing": {
"L6_reference": {
"taxableProfit": {
"numerator": "3230",
"denominator": "1"
},
"taxableBase": {
"numerator": "3230",
"denominator": "1"
},
"colecta": {
"numerator": "5168",
"denominator": "5"
}
},
"L7_profit": {
"numerator": "3230",
"denominator": "1"
},
"L7_base": {
"numerator": "3230",
"denominator": "1"
},
"L7_colecta": {
"numerator": "5168",
"denominator": "5"
},
"autonomous_tax": null,
"combined_colecta": null,
"expense_status": "indeterminate",
"cooperative_status": "absent",
"acceleration_status": "absent",
"additional_depreciation_deduction": {
"numerator": "0",
"denominator": "1"
},
"history_versions": [
"IRPC-normal-adjustments/1",
"IRPC-normal-adjustments/2",
"IRPC-normal-adjustments/3",
"IRPC-normal-adjustments/4",
"IRPC-normal-adjustments/5",
"IRPC-normal-adjustments/6",
"IRPC-normal-adjustments/7",
"IRPC-normal-adjustments/8",
"IRPC-normal-adjustments/9"
],
"final_balance": {
"scope": "L8",
"payable": null,
"recoverable": null
}
},
"negative": {
"L6_reference": {
"taxableProfit": {
"numerator": "3230",
"denominator": "1"
},
"taxableBase": {
"numerator": "3230",
"denominator": "1"
},
"colecta": {
"numerator": "5168",
"denominator": "5"
}
},
"L7_profit": {
"numerator": "3230",
"denominator": "1"
},
"L7_base": {
"numerator": "3230",
"denominator": "1"
},
"L7_colecta": {
"numerator": "5168",
"denominator": "5"
},
"autonomous_tax": {
"numerator": "0",
"denominator": "1"
},
"combined_colecta": {
"numerator": "5168",
"denominator": "5"
},
"expense_status": "calculated",
"cooperative_status": "absent",
"acceleration_status": "absent",
"additional_depreciation_deduction": {
"numerator": "0",
"denominator": "1"
},
"history_versions": [
"IRPC-normal-adjustments/1",
"IRPC-normal-adjustments/2",
"IRPC-normal-adjustments/3",
"IRPC-normal-adjustments/4",
"IRPC-normal-adjustments/5",
"IRPC-normal-adjustments/6",
"IRPC-normal-adjustments/7",
"IRPC-normal-adjustments/8",
"IRPC-normal-adjustments/9"
],
"final_balance": {
"scope": "L8",
"payable": null,
"recoverable": null
}
},
"absent": {
"L6_reference": {
"taxableProfit": {
"numerator": "3130",
"denominator": "1"
},
"taxableBase": {
"numerator": "3130",
"denominator": "1"
},
"colecta": {
"numerator": "5008",
"denominator": "5"
}
},
"L7_profit": {
"numerator": "3130",
"denominator": "1"
},
"L7_base": {
"numerator": "3130",
"denominator": "1"
},
"L7_colecta": {
"numerator": "5008",
"denominator": "5"
},
"autonomous_tax": {
"numerator": "0",
"denominator": "1"
},
"combined_colecta": {
"numerator": "5008",
"denominator": "5"
},
"expense_status": "absent",
"cooperative_status": "absent",
"acceleration_status": "absent",
"additional_depreciation_deduction": {
"numerator": "0",
"denominator": "1"
},
"history_versions": [
"IRPC-normal-adjustments/1",
"IRPC-normal-adjustments/2",
"IRPC-normal-adjustments/3",
"IRPC-normal-adjustments/4",
"IRPC-normal-adjustments/5",
"IRPC-normal-adjustments/6",
"IRPC-normal-adjustments/7",
"IRPC-normal-adjustments/8",
"IRPC-normal-adjustments/9"
],
"final_balance": {
"scope": "L8",
"payable": null,
"recoverable": null
}
},
"cooperative": {
"L6_reference": {
"taxableProfit": {
"numerator": "3130",
"denominator": "1"
},
"taxableBase": {
"numerator": "3130",
"denominator": "1"
},
"colecta": {
"numerator": "5008",
"denominator": "5"
}
},
"L7_profit": {
"numerator": "3130",
"denominator": "1"
},
"L7_base": {
"numerator": "3130",
"denominator": "1"
},
"L7_colecta": {
"numerator": "2504",
"denominator": "5"
},
"autonomous_tax": {
"numerator": "0",
"denominator": "1"
},
"combined_colecta": {
"numerator": "2504",
"denominator": "5"
},
"expense_status": "absent",
"cooperative_status": "calculated",
"acceleration_status": "absent",
"additional_depreciation_deduction": {
"numerator": "0",
"denominator": "1"
},
"history_versions": [
"IRPC-normal-adjustments/1",
"IRPC-normal-adjustments/2",
"IRPC-normal-adjustments/3",
"IRPC-normal-adjustments/4",
"IRPC-normal-adjustments/5",
"IRPC-normal-adjustments/6",
"IRPC-normal-adjustments/7",
"IRPC-normal-adjustments/8",
"IRPC-normal-adjustments/9"
],
"final_balance": {
"scope": "L8",
"payable": null,
"recoverable": null
}
},
"acceleration": {
"L6_reference": {
"taxableProfit": {
"numerator": "6260",
"denominator": "1"
},
"taxableBase": {
"numerator": "6260",
"denominator": "1"
},
"colecta": {
"numerator": "10016",
"denominator": "5"
}
},
"L7_profit": {
"numerator": "6240",
"denominator": "1"
},
"L7_base": {
"numerator": "6240",
"denominator": "1"
},
"L7_colecta": {
"numerator": "9984",
"denominator": "5"
},
"autonomous_tax": {
"numerator": "0",
"denominator": "1"
},
"combined_colecta": {
"numerator": "9984",
"denominator": "5"
},
"expense_status": "absent",
"cooperative_status": "absent",
"acceleration_status": "calculated",
"additional_depreciation_deduction": {
"numerator": "20",
"denominator": "1"
},
"history_versions": [
"IRPC-normal-adjustments/1",
"IRPC-normal-adjustments/2",
"IRPC-normal-adjustments/3",
"IRPC-normal-adjustments/4",
"IRPC-normal-adjustments/5",
"IRPC-normal-adjustments/6",
"IRPC-normal-adjustments/7",
"IRPC-normal-adjustments/8",
"IRPC-normal-adjustments/9"
],
"final_balance": {
"scope": "L8",
"payable": null,
"recoverable": null
}
}
},
"source_inventory": {
"files_reused": 8143,
"broad_search_repeated": false,
"receipt": "irpc-l7-scope-20261026.json",
"sources": [
{
"id": "CIRPC",
"title": "Lei34/2007",
"exemplar": "C:\\laragon\\www\\portal-investidor-irpc-l7-scope-20261026\\docs\\investmoz\\simulations\\source-archive\\Lei-34-2007-BR52.pdf",
"bytes": 1676884,
"sha256": "cdbef875ad7ccc1517b12faf5de0e893a70ed2ed81e33913cd6dc4a674474e6e",
"references": "PDF5/p768(59):12,14,15; PDF15–16:54–56; PDF17/p768(71):61",
"official_url": null,
"available_for_consultation": true,
"documentary_status": "official_source_accepted; applicability/version/period_per_treatment",
"new_calculation_activation": false
},
{
"id": "IRPC-2026",
"title": "Lei12/2025",
"exemplar": "C:\\laragon\\www\\portal-investidor-irpc-l7-scope-20261026\\docs\\investmoz\\simulations\\source-archive\\BR-248-I-Suplemento-1-2025.pdf",
"bytes": 440018,
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
"references": "PDF18–20; PDF20:art2/61-A,art3,art6; vigência01/01/2026",
"official_url": null,
"available_for_consultation": true,
"documentary_status": "official_source_accepted; applicability/version/period_per_treatment",
"new_calculation_activation": false
},
{
"id": "IRPC-2022",
"title": "Lei20/2022",
"exemplar": "C:\\laragon\\www\\portal-investidor-irpc-l7-scope-20261026\\docs\\investmoz\\simulations\\source-archive\\BR252-I-Serie-2022-Lei20-PESOE.pdf",
"bytes": 17966640,
"sha256": "1159fd3ce1ba5581671796d083534da48bd3c5578298819e896f1c2a8026318d",
"references": "PDF1/p2731:61(2), termo31/12/2025",
"official_url": null,
"available_for_consultation": true,
"documentary_status": "official_source_accepted; applicability/version/period_per_treatment",
"new_calculation_activation": false
},
{
"id": "CBF-local",
"title": "Lei4/2009",
"exemplar": "C:\\laragon\\www\\portal-investidor-irpc-l7-scope-20261026\\outputs\\irpc-l7-scope\\CBF-Lei4-2009-APIEX.pdf",
"bytes": 10998765,
"sha256": "4231a5b6783e603cedb98992f3e93ecaf1c1d5be341b093560cf2fdd0845b0dc",
"references": "Original local disponível; scan sem extracção textual útil; cotejo textual pela compilação oficial",
"official_url": null,
"available_for_consultation": true,
"documentary_status": "official_source_accepted; applicability/version/period_per_treatment",
"new_calculation_activation": false,
"authorised_original": "\\\\Mac\\Home\\Documents\\TEMPO\\Portal Tempo\\Legislacao\\Apiex\\3.-Lei-no4-2009-de-12-de-Janeiro-Codigo-dos-Beneficios-Fiscais.pdf"
},
{
"id": "APIEX",
"title": "Compilação oficial APIEX",
"exemplar": "C:\\laragon\\www\\portal-investidor-irpc-l7-scope-20261026\\outputs\\irpc-l7-scope\\APIEX-legislacao-investimentos-2025.pdf",
"bytes": 6467655,
"sha256": "ba1e9eaabfec5d0c4cb2d9f2444a6860f7a90f222034764197829be402d408ed",
"references": "pp8–32:Lei8/2023; pp36–59:Decreto8/2024; pp64–92:CBF4/2009; p68:9/10;p71–74:15–19",
"official_url": "https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf",
"available_for_consultation": true,
"documentary_status": "official_source_accepted; applicability/version/period_per_treatment",
"new_calculation_activation": false
},
{
"id": "RCBF",
"title": "Decreto56/2009",
"exemplar": "C:\\laragon\\www\\portal-investidor-irpc-l7-scope-20261026\\outputs\\irpc-l7-scope\\Decreto56-2009-BR40.pdf",
"bytes": 422005,
"sha256": "6ea1b8d93daed48c6d590481d874010fa1708211d8f27c319dc9a31328456017",
"references": "Procedimento já cotejado em iva-l1-p084-p085-model-i1-cotejo-20261024.json; não estende isenções IVA a IRPC",
"official_url": null,
"available_for_consultation": true,
"documentary_status": "official_source_accepted; applicability/version/period_per_treatment",
"new_calculation_activation": false
}
],
"document_consultation_activates_calculation": false
},
"coverage": {
"optional_input_backward_compatible": true,
"accepted_version9_current_key_and_result_unchanged_without_L7": true,
"expense_source": "calculated36(1)g schedule ID;full amount reused;no parallel amount",
"expense_rate": "35/100",
"expense_additional_profit_adjustment": "0",
"cooperative_rate": "32/100 × 1/2 =16/100;eligible ordinary base only",
"accelerated_rate": "normal maximum table rate ×3/2;lower optional quota not covered;first full civil2026,new supported asset,prior quotas zero",
"accelerated_deduction": "min calculated quota/accounted eligible amount/fiscal remaining balance;only difference over normal map",
"mixed_cases": "cooperative+autonomous expense demonstrated;cooperative+CBF16 not supported",
"context": "reuses documented sector/territory/provenance;no benefit by location alone;raw snapshots not dumped in report",
"states": [
"information_missing",
"declared_absence",
"not_applicable_or_negative_condition",
"calculated"
],
"report": "same normalReportRows/HTML and financial project dossier;L6 reference separated from L7 effects",
"final_settlement": "not calculated;L8"
},
"pending": {
"code": [
"autonomous expenses outside already-calculated36g parcel",
"mixed cooperative income/withheld parcels",
"CBF16 rehabilitated/other categories/lower optional quota/methods/months/prior quotas/later years",
"specific benefit combinations and cumulation",
"61-A capital gains and other previously delimited treatment families",
"L7 official form transport"
],
"documents_per_case": [
"individual status/title/exception and qualification proof when required by applicable norm",
"parcel/limit/order/cumulation/chronology not evidenced",
"affected form field/edition/representation correspondence"
],
"resume_conditions": "same existing matrix/manual L7 QA section;no blanket validation or repeated transversal research",
"L6_pending_unchanged": true,
"L8_annual_credit_application_retentions_payments_final_balance_separate": true
},
"checks": {
"new_frontend_tests": 19,
"API_tests": 1,
"API_assertions": 108,
"API_database": "simulator_fiscal_delta_20261021_test",
"API_host": "127.0.0.1",
"API_port": 55441,
"API_transaction_rollback": true,
"QA_identity_guard": true,
"Wayfinder_generated_with_form": true,
"typescript_noEmit": true,
"QA_build": true,
"API_recovered_to_HTML_and_financial_dossier_cases": 6,
"UI": {
"actualComponent": true,
"expenseL6Addback100Autonomous35": true,
"missingClassificationDoesNotEraseNormalProfit": true,
"absenceExplicit": true,
"cooperative16MixedCaseBlockedOnly": true,
"acceleratedQuota150LimitedToAccounted120": true,
"netAdditionalDeduction20": true,
"cumulationMissingExplicit": true,
"saveRecoveryDownload": true,
"finalBalanceL8Unknown": true,
"errors": []
},
"old_functional_suites_repeated": false,
"building_and_lower_optional_quota_cases": true
},
"preservation": {
"baseline": "d37fffb2d2eacef6300f1c81fc29993178532f35",
"files_compared": 662,
"only_existing_changed": [
"resources/js/lib/irpc-normal-depreciation.ts",
"resources/js/lib/irpc-normal.ts",
"resources/js/lib/project-dossier-report.ts"
],
"protected_code_schema_config_routes_contract_files": 576,
"originals": {
"docs/investmoz/simulations/source-archive/AT-Comunicado-9-2026-ISPC.pdf": "106f8a9ce9255c03549c70bf07fe999d32e6e55302d009b509862aa74ccb4973",
"docs/investmoz/simulations/source-archive/AT-MRFTA-19-39.pdf": "8ea62c53066b482f87078a7a7b1ecf15ca98a12a122e8d7abca63acfc993186b",
"docs/investmoz/simulations/source-archive/AT-Modelo-30-ISPC.pdf": "6802edd06b6ff638a0b851a3ec7ecf69af8f07840faa47446becad67d35d445c",
"docs/investmoz/simulations/source-archive/AT-Modelo-B-Geral.pdf": "bd977c7c59d4d98befd92e2c566b1bf168cb1343bb519f51b41a3807f431fc57",
"docs/investmoz/simulations/source-archive/BM-Mercado-Monetario-MAIBOR-20261025.html": "00e8a7e0d3a1db4ca3beb56d5361b2084a0f1684f3d40fc1c203702a2190e063",
"docs/investmoz/simulations/source-archive/BR-16-I-Serie-2008-04-16.pdf": "fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee",
"docs/investmoz/simulations/source-archive/BR-248-I-Suplemento-1-2025.pdf": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
"docs/investmoz/simulations/source-archive/BR-89-I-Serie-2017-06-08.pdf": "11f0cab15eb6e51010952203ffa24fdc533a24fd642e19525fe4f46b17a1f5e5",
"docs/investmoz/simulations/source-archive/BR-95-I-Suplemento-2025-05-21.pdf": "e0e682e2c027c3cd9c1d46a73a3f48bc5f6bdbf05667fd97314d7a3f6a456358",
"docs/investmoz/simulations/source-archive/BR252-I-Serie-2022-Lei20-PESOE.pdf": "1159fd3ce1ba5581671796d083534da48bd3c5578298819e896f1c2a8026318d",
"docs/investmoz/simulations/source-archive/Decreto-22-2021-BR70.pdf": "a8fac38dcfe08f9c7a8e46254cff10e1ec82fc4f7a8d88ad6e4fe7d91ffc21a5",
"docs/investmoz/simulations/source-archive/Decreto-27-2026-BR123.pdf": "26249c3fa58f149c81de36e99d1aac2819a4771379d9e8833159660dabf539bb",
"docs/investmoz/simulations/source-archive/Decreto-29-1998-BR22-5-suplemento.pdf": "6242cbe675faadf085f43f95ec8c7e8868aafe67c3b2ab2bb010992288f954f8",
"docs/investmoz/simulations/source-archive/Decreto-3-2011-BR-11-I-mineiros.pdf": "5bca1133666ba9c69870b89823420175b7b5b8d0eccb667ecdf5c29716f3b6be",
"docs/investmoz/simulations/source-archive/Decreto-37-2023-BR124.pdf": "7e680b191481faa0e2e9057fa5053587eb2a29d1d8c819e0eb9f02814255a04c",
"docs/investmoz/simulations/source-archive/Decreto-9-2008-BR16.pdf": "96fccdecbfdc11c9eafb90fbc6c7bc259f88bbd4c7507d1ac3af1fa03f960697",
"docs/investmoz/simulations/source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf": "b90dc7c14ecea2e378c4a62e8e5a47d54a5644c457baeb8b8053b92fd2a5710b",
"docs/investmoz/simulations/source-archive/Lei-14-1992-BR42.pdf": "089fff7423e99a70a230a73ca379e4ea3a91d124703f91016e57ff82794b9e56",
"docs/investmoz/simulations/source-archive/Lei-17-2022-BR251-exemplar-MCNet5.pdf": "14eb34f0af91a8f5cb1770c7b52d6ff839c6818302d1ed96d79afbfb9fe999d4",
"docs/investmoz/simulations/source-archive/Lei-17-2022-IPP-BR250-Parlamento.pdf": "978cd22403ffd336a631704ecd7a5bbd814a427c815b9acdaca5515591dd9e2a",
"docs/investmoz/simulations/source-archive/Lei-19-2013-BR76.pdf": "bba96d645cc935ffc3d1a79b9f0f73ac1c693d6459c2ad41f56cd2841accae76",
"docs/investmoz/simulations/source-archive/Lei-19-2017-IRPS-BR-OCAM.pdf": "c3a0186cb9ad090d7a9533006295dc9bb6a62ad0d6150173cbd49dba4ee4b52d",
"docs/investmoz/simulations/source-archive/Lei-2-2011-BR-1-4-suplemento-MCNet.pdf": "a7543a68cc3287a46a83c39a04821b40f649dd06c4e9b5dc51ff0b47fba08899",
"docs/investmoz/simulations/source-archive/Lei-20-2009-BR36-AT-exemplar19.pdf": "fc1c8144db42960cbf73e717e095896f511ece0f907f58f78819b27d6917a7ee",
"docs/investmoz/simulations/source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf": "ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53",
"docs/investmoz/simulations/source-archive/Lei-34-2007-BR52.pdf": "cdbef875ad7ccc1517b12faf5de0e893a70ed2ed81e33913cd6dc4a674474e6e",
"docs/investmoz/simulations/source-archive/Lei-4-1994-BR37-3-suplemento.pdf": "210df9db8980d0a5690cd187a6eeec7e37a900d743e39062c16f4e0d12215c7e",
"docs/investmoz/simulations/source-archive/Lei-4-2012-BR3.pdf": "b8845cef41dcbd9ba50c43dbfa743a56030dd3d348242a107e1ae30279eec121",
"docs/investmoz/simulations/source-archive/Lei-7-1991-Partidos-IFES.pdf": "6b783a0bb3330688ef10660a58060d9724aa2da6a873d234fe902f0eaaeda42d",
"docs/investmoz/simulations/source-archive/PT-MZ-CDT-1992.pdf": "d504e1024b28aee5c6acf4592acb96ec9c915a8414f67a61ab6984d6acf9df9d",
"docs/investmoz/simulations/source-archive/PT-MZ-Protocolo-2009.pdf": "f353773dcf41f09f4daa6cf5ba5a5f7994f3e02615867c2dcffc61c3d901bf9b"
},
"active_writes": 0,
"migrations": 0,
"permission_changes": false
},
"evidence_directory": "outputs/irpc-l7-common",
"delivery": {
"prepared_only": true,
"Git_branch_isolated": true,
"active_installation": false,
"fresh_backup_restore_and_delta_check_required_at_future_installation": true
}
}
@@ -2523,7 +2523,7 @@ As estimativas são **horas de trabalho técnico**, não calendário nem mediç
| L4 — ISPC: elegibilidade e apuramento | Campos de enquadramento e motor genérico | Regime/actividade/volume elegível, opções/exclusões, escalões/taxas aplicáveis, mínimo, trimestre e retenções específicas; comparação só entre regimes admissíveis | Vendas por actividade, custos, NUIT, natureza jurídica e residência confirmada | Lei9/2025 e regulamentação/período; distinguir receita anual, trimestre e excesso | 40–64 h |
| L5 — retenções IRPC e guias | Documentos de compras/vendas e guia M/39 como exemplar | Rendimento pago/recebido, entidade que retém, residente/não residente/EP, evento temporal, excepções e casos digitais/e-money; guia e comprovativo ligados à operação | Contrapartes, facturas, pagamentos, período, moeda, oportunidade | Artigos aplicáveis por rendimento, taxa/convenção se invocada; corrigir remissões do exemplar. Sem submissão electrónica AT presumida | 32–56 h |
| L6 — IRPC anual: regime normal | Disponível5aaf1a56:percurso normal demonstrado atécolecta,períodos/moedas e preparação parcialM20/M22 | Operacional no âmbito demonstrado por decisãoTempo26/10;limitações explícitas;5grupos código+5documentais preservados;sem conclusão integral | Plano financeiro,dados comuns,activos/livros e histórico;retenções/pagamentos/créditos anuaisL8 | Pendências bloqueiam só os casos dependentes;L7/L8 próprios | Estimativa inicial48–80h;saldo não reestimado |
| L7 — IRPC: tratamentos específicos | Delimitação26/10 e fontes/contexto existentes;sem novo tratamento implementado | Qualificação e efeitos por parcela:autónomas61(4)/61-A,entidades/cooperativas12,CBF15–19,sector/território e operações especiais;ver secção L7 | L6/contexto herdado,despesas,activos e relações documentadas;sem benefício por localização isolada | Condições,fonte/versão,período,limite/cumulação e efeito por caso;crédito potencialL7/aplicação anualL8 | Estimativa inicial48–80h;sem reestimativa |
| L7 — IRPC: tratamentos específicos | Qualificação/despesas61(4),cooperativa12 eCBF16 no recorte demonstrado emQA,fotografia10;sem instalação | Gravação/recuperação/relatório com referênciaL6 separada dos efeitosL7;informação em falta/ausência/não aplicação explícitas. Restantes famílias,parcelas mistas,cumulação e transporte oficial pendentes por caso | Sujeito/período/contexto,despesas36g e activos/quotasL6 reutilizados,sem benefício por localização | Fontes,condições/período e critérios de retoma na secção L7QA;L8 mantém liquidação anual | Estimativa inicial48–80h;saldo não reestimado |
| L8 — IRPC: liquidação e pagamentos | Parcelas L5–L7 e formulários originais | PPC/PEC, deduções/créditos, dupla tributação quando aplicável, saldo anual a pagar/recuperar; calendário e reconciliação com pagamentos | Retenções, colecta, saldos anteriores, pagamentos/comprovativos | Base anterior para PPC/PEC, regras de limitação/transporte; não usar cinco anos ou compensação automática sem cotejo específico | 32–56 h |
| L9 — INSS: contribuição social | Massa salarial agregada e referência TCO3%+4% | Trabalhadores, remuneração/base contributiva, exclusões, empregador/trabalhador, TCO/TCP, declaração e encargos por período | Equipa/custos, entidade e calendário; cadastro de pessoa partilhado com L10 | Regulamento e alterações aplicáveis, natureza do vínculo; confirmar base antes da taxa. Contribuição social, distinta dos impostos | 24–40 h |
| L10 — IRPS: retenção mensal e líquido | Entrada IRPS, custos salariais e escalões anuais documentais | Tabela mensal própria, remunerações/benefícios, situação pessoal relevante, retenção, salário líquido e contribuição ligada a L9 | Mesmo trabalhador/folha salarial, pagamentos e retenções | Tabela mensal do período, residência e condições familiares necessárias; não dividir escalões anuais por12 | 40–64 h |
@@ -6342,3 +6342,45 @@ Para cada nova regra: condição satisfeita, negativa, informação em falta, pe
Reutilizado o inventário autorizado de **8143ficheiros**, sem nova varredura de pastas. Para delimitação foram consultados os originais CIRPC/alterações já arquivados, a Lei4/2009 local e a compilação oficial [APIEX — Legislação de Investimentos em Moçambique](https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf), que reúne Lei8/2023, Decreto8/2024 e CBF4/2009. Disponibilidade, situação documental/jurídica e uso no cálculo continuam estados independentes. O acesso actual à cópia CBF supera a indisponibilidade assinalada em notas antigas; não activa regra nem apaga a nota histórica. Artigos, páginas, hashes e limites no recibo. A compilação não dispensa o período/alterações de cada tratamento, nem cria validação jurídica genérica adicional.
**Código por implementar em L7:** qualificação e persistência específicas; efeitos segregados; regras das famílias acima, por parcelas; conciliação sem duplicação; relatório/exportação correspondente. **Documentação por caso:** estatuto/reconhecimento ou enquadramento individual quando a fonte o exige; identificação de parcela/limite/cronologia; norma sectorial ainda sem correspondência; edição/campo de transporte afectado. Cada uma conserva exemplo e condição de retoma na linha correspondente. Não é necessário resolver todos os documentos para começar o primeiro incremento independente. L6 operacional não elimina este desenvolvimento; L7 delimitado não é L7 implementado; L8 conserva a liquidação final.
## 26/10/2026 — L7: qualificação, despesas autónomas e casos compatíveis em QA
**QA, sem instalação.** Base instalada preservada `5aaf1a56`; delimitação `d37fffb2`; ramo `codex/irpc-l7-common-autonomous-qa-20261026`, fotografia opcional `IRPC-normal-adjustments/10`. L6 continua operacional no âmbito demonstrado, com os cinco grupos de código e cinco documentais preservados. A implementação abaixo acrescenta apenas os tratamentos demonstrados; não conclui L7 nem o IRPC. Saldo final, retenções, pagamentos e aplicação anual de créditos permanecem em L8.
**Qualificação comum executável:** reutiliza sujeito/NUIT, âmbito, período, origem da oportunidade/dossier, sector e território; guarda condições, prova, fonte/artigo e período por tratamento. «Informação em falta», «ausência declarada», «não aplicável/condição negativa» e «calculado» têm resultados distintos. Desconhecido não vira zero. Período incompatível, identidade por reconciliar ou contradição afectam só o resultado dependente. A localização não escolhe taxa nem activa benefício. Os snapshots de origem ficam internos; o relatório apresenta contexto legível e as condições documentadas.
**Despesas61(4):** cada parcela aponta para uma linha `undocumented` do mapa36(1)g de L6, com acréscimo integral já calculado. Montante, documento e linha são reutilizados; L7 não introduz outro acréscimo nem outra despesa. A classificação legal e incidência são declaradas com prova própria; falta de anexo não classifica uma despesa. O imposto autónomo usa35% apenas nos casos qualificados. Duplicação da referência, parcela não correspondida, ausência contraditória ou informação insuficiente deixam o imposto reconciliado indeterminado; as parcelas conhecidas permanecem discriminadas. Condição negativa documentada conserva o ajustamentoL6 e não aplica imposto autónomo à parcela.
**Cooperativas12/14:** agrárias, artesanato ou culturais, residentes, com base ordinária integralmente elegível e ausência documentada de rendimentos sujeitos a retenção, aplicam redução de50% à taxa geral32%, ou16%. Não depende de seleccionar um sector semelhante. A qualificação especial pode coexistir com a opção «outro tratamento» da colectaL6: não exige fingir que a taxa reduzida é regime geral. Parcela mista, rendimentos retidos, outra entidade ou ausência contraditória conservam pendência própria; não recebem redução global presumida. O imposto autónomo de despesas não é reduzido por esta taxa.
**CBF16:** reutiliza identificadores e valores dos activosL6 suportados; imóvel novo do projecto ou máquinas industriais/agro-industriais das categorias correspondidas, com enquadramentoCBF3/9 e reconhecimento10 quando exigido (ou excepção identificada). Recorte inicial: primeiro exercício anual civil2026, método constante pela taxa normal máxima da tabela, activo desde01/01/2026, custo histórico novo, sem quotas anteriores/saída e sem cumulação de outros benefícios. Incrementa a taxa normal em50% e reutiliza limites de contabilização/saldo fiscal. Deduz apenas a diferença entre o mapa normal e o acelerado, sem alterar o activo de origem, os documentos ou os saldos oficiais. Quotas constantes gerais e demais cálculosL6 conservam o comportamento anterior quandoL7 está ausente.
| Caso sintético recuperado da API QA | Lucro/base e colecta de referênciaL6 | EfeitoL7 demonstrado | Resultado e limite |
| --- | --- | --- | --- |
| Despesa100 já acrescida36g | Lucro/base3230;colecta1033,60MZN | Novo ajustamento ao lucro0;imposto autónomo35 | Colecta combinada1068,60MZN;saldo finalL8indeterminado |
| Classificação da mesma despesa em falta | Lucro/base3230;colecta1033,60MZN preservados | Imposto autónomo indeterminado | Colecta combinada indeterminada;sem perda dos cálculos independentes |
| Condição negativa documentada na parcela | Ajustamento36g100 preservado | Parcela não aplicável;imposto autónomo0 justificado | Não confunde condição negativa com informação em falta |
| Ausência declarada de despesas autónomas | Lucro/base3130;colecta1001,60MZN | Ausência com suporte;imposto autónomo0 | Ausência contraditória com despesa36g fica pendente |
| Cooperativa integralmente abrangida | Base3130;colecta geral1001,60MZN | Taxa16%;colecta500,80MZN | Base/proveitos não alterados;sem retenções/créditos abatidos |
| Máquina industrial existente:base1000,taxa10%,quota contabilística120 | Quota normal100;lucro/base6260;colecta2003,20MZN | Quota acelerada150,mas dedução limitada120;diferença adicional20 | Lucro/base6240;colecta1996,80MZN;sem deduzir50 nem150 outra vez |
**Cálculo → gravação → recuperação → relatório:** fotografia10 opcional no mesmo livro/snapshot, chave com entradas/contexto e histórico imutável anterior. SemL7, a fotografia9 aceite e a sua chave/totais permanecem iguais. Recalcular não acumula imposto nem duplica o acréscimo. HTML e dossier financeiro mostram referênciaL6, efeitosL7, parcelas autónomas, fonte, condições, período e limitações; recuperação e download demonstrados no componente real. Uma mudança de origem/entradas torna a fotografia anterior histórica; não altera o registo anterior. M20/M22 conservam a preparaçãoL6 de referência: **este incremento não transporta efeitosL7 para campos oficiais por aproximação**, nem resolve as divergências documentais existentes.
**Provas novas:** Wayfinder `--with-form` gerou routes/actions pelo wrapper QA; TypeScript e build QA passaram. Dezanove testes dirigidos novos passaram, incluindo comparação exacta com a fotografia9 aceite, duplicação, período curto, opção especial de cooperativa, ausência contraditória, dados em falta e contexto apresentado sem despejar snapshots internos. API privada:1teste/108asserções, `simulator_fiscal_delta_20261021_test`,127.0.0.1:55441, transacção com rollback e guarda de identidade/base; seis casos recuperados com plano/livro/históricos idênticos. UI do componente real: alteração de classificação, informação em falta, ausência, cooperativa mista, cumulação pendente, gravação/recuperação local e exportação; sem erros de página. A prova local é distinta da API. Não houve testes/escrita no activo nem repetição das suites funcionais aceites de L3, ISPC, câmbio ou L5/L6.
**Entrega agrupada preparada:** qualificação + despesas autónomas, cooperativa integralmente elegível e amortizaçãoCBF16 no recorte acima. Despesa autónoma e cooperativa também foram demonstradas juntas:parcela35 sem redução,colecta sobre a base516,80 e combinada551,80MZN no exemplo de base3230. Isto não autoriza cumulação cooperativa+CBF16: essa combinação mantém resultado dependente indeterminado. A futura instalação segue o procedimento Git existente, com backup/restauro fresco e conferência do delta; não foi executada nesta passagem.
**SaldoL7 na mesma matriz:**
| Natureza | Ainda fora da cobertura / exemplo | Condição concreta de retoma |
| --- | --- | --- |
| Código | Despesa autónoma fora de linha36g calculada ou parcela ainda não segregada | Implementar origem/segregação correspondente e prova da incidência;não copiar montante para uma segunda despesa |
| Código | Cooperativa com actividade abrangida e outra não abrangida,ou rendimento sujeito a retenção | Segregar proveitos/custos/base e aplicar o tratamento apenas à parcela elegível;L5 conserva a retenção |
| Código | CBF16 em imóvel reabilitado,outra categoria,quota inferior à máxima,método degressivo,duodécimos,quotas anteriores ou exercício posterior | Corresponder regra e cronologia respectivas e reutilizar o activo/quotas existentes;pendênciaL6 limita apenas o activo dependente |
| Código/documento por caso | Cooperativa+CBF16 ou outro benefício positivo a cumular | Identificar compatibilidade,ordem e limites específicos;implementar a combinação,sem presumir soma de benefícios |
| Código | Mais-valias61-A,demais isenções/deduçõesCBF17–19,regimes sectoriais/territoriais e operações especiais | Implementar a família/parcelas já delimitadas na secção anterior;período e fonte específicos,sem nova pesquisa transversal |
| Documento por caso | Estatuto/título/excepçãoCBF ou prova das condições em falta;endereçoZEE sem estatuto | Juntar o elemento que a disposição exige ou registar condição negativa;localização isolada permanece insuficiente |
| Transporte/documento | CampoM20/M22 para efeitoL7,edição divergente ou representação não correspondida | Correspondência do campo/edição e regra de representação demonstradas;preservar o cálculo independente exacto |
| ÂmbitoL8 | Direito potencial ao créditoCBF15,aplicação anual de créditos,retenções,pagamentos e saldo final | Executar o mecanismo próprioL8;nenhuma dedução/compensação anual é presumida neste lote |
Fontes reutilizadas: inventário autorizado8143ficheiros; originaisCIRPC/alterações e CBF/compilaçãoAPIEX já identificados na delimitação,com hashes conservados. Sem activação por consulta ou revalidação jurídica genérica. Recibo: `docs/investmoz/simulations/irpc-l7-common-qa-20261026.json`. Provas locais: `outputs/irpc-l7-common/`; reprodução dirigida: `node --test tests/frontend/irpc-special.test.mjs`, `php tools/irpc-special-qa.php routes`, `php tools/irpc-special-qa.php test`, `tsc --noEmit`, build com `INVESTMOZ_QA_BUILD=1`.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
import {SimulationIrpcSpecial} from '@/components/simulation-irpc-special';
import {SimulationIrpcFormPreparation} from '@/components/simulation-irpc-form-preparation';
import {SimulationIrpcPeriodFx} from '@/components/simulation-irpc-period-fx';
import {SimulationIrpcDepreciation} from '@/components/simulation-irpc-depreciation';
@@ -39,8 +40,9 @@ export function SimulationIrpcNormal({draft,onChange}:{draft:Record<string,unkno
<SimulationIrpcCosts draft={draft} book={b} save={save}/>
<SimulationIrpcTaxableBase draft={draft} book={b} save={save}/>
<SimulationIrpcAssessment book={b} save={save}/>
<SimulationIrpcSpecial draft={draft} book={b} save={save}/>
<SimulationIrpcFormPreparation draft={draft} book={b} save={save}/>
<button type="button" onClick={()=>{try{save(recordNormal(draft,b));}catch(e){setError(String(e));}}}>Calcular e guardar ajustamentos L6</button>
<button type="button" onClick={()=>{try{save(recordNormal(draft,b));}catch(e){setError(String(e));}}}>Calcular e guardar L6 e tratamentos L7 preparados</button>
<button type="button" onClick={()=>{try{const html=normalHtml(draft),url=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'})),a=document.createElement('a');a.href=url;a.download='IRPC-normal-ajustamentos.html';a.click();URL.revokeObjectURL(url);setError('');}catch(e){setError(String(e));}}}>Exportar mapa parcial L6</button>
{normalReportRows(draft).map((r,n)=><p key={n}>{r[1]} — {r[2]} — {r[3]}</p>)}
</section>;
@@ -0,0 +1,24 @@
import type {NormalBook} from '@/lib/irpc-normal';
import {newSpecialInput,newSpecialExpense,newAcceleratedAsset,specialSources,type SpecialInput,type Qualification} from '@/lib/irpc-special';
type Props={draft:Record<string,unknown>;book:NormalBook;save:(b:NormalBook)=>void};
const states=[['','Informação em falta'],['known','Tratamento conhecido — qualificar condições'],['absent','Ausência declarada com suporte'],['not_applicable','Tratamento não aplicável — justificar']]as const;
function QualificationFields({label,value,onChange}:{label:string;value:Qualification;onChange:(v:Qualification)=>void}){
const field=(key:'from'|'to'|'reference'|'periodReference',text:string)=><label className="block">{text}<input aria-label={label+' — '+text} value={value[key]} onChange={e=>onChange({...value,[key]:e.target.value})}/></label>;
return<div className="space-y-2"><label className="block">Estado — {label}<select aria-label={'Estado — '+label} value={value.state} onChange={e=>onChange({...value,state:e.target.value as Qualification['state'],eligible:''})}>{states.map(([v,t])=><option key={v} value={v}>{t}</option>)}</select></label>{value.state==='known'&&<label className="block">Condições — {label}<select aria-label={'Condições — '+label} value={value.eligible} onChange={e=>onChange({...value,eligible:e.target.value as Qualification['eligible']})}><option value="">Por esclarecer</option><option value="yes">Sim — condições documentadas</option><option value="no">Não — condição negativa documentada</option></select></label>}{field('reference','Prova das condições, ausência ou não aplicação')}{field('from','Período — início (AAAA-MM-DD)')}{field('to','Período — termo (AAAA-MM-DD)')}{field('periodReference','Suporte do período e da vigência aplicável')}</div>;
}
export function SimulationIrpcSpecial({draft,book:b,save}:Props){
const i=b.special;
const start=()=>{const next=newSpecialInput(),p=b.periodsFx??b.schedules?.period;for(const part of[next.expenses,next.cooperative,next.acceleration])Object.assign(part.qualification,{from:p?.from??b.year+'-01-01',to:p?.to??b.year+'-12-31'});save({...b,special:next});};
if(!i)return<section className="rounded border p-3"><h5>L7 — benefícios e casos especiais</h5><p>Preparação opcional e separada do percursoL6 já entregue. Localização e sector não activam benefícios. A liquidação final permanece emL8.</p><button type="button" onClick={start}>Preparar qualificação L7</button></section>;
const change=(patch:Partial<SpecialInput>)=>save({...b,special:{...i,...patch}});
const yesno=(label:string,value:string,onChange:(v:''|'yes'|'no')=>void)=><label className="block">{label}<select aria-label={label} value={value} onChange={e=>onChange(e.target.value as ''|'yes'|'no')}><option value="">Informação em falta</option><option value="yes">Sim — documentado</option><option value="no">Não — documentado</option></select></label>;
return<section aria-label="L7 — qualificação e tratamentos" className="space-y-3 rounded border p-3"><h5>L7 — qualificação comum e tratamentos documentados</h5><p>Reutiliza sujeito, NUIT, período, despesas e activos L6. Contexto herdado:sector {String(draft.sector_id??'por identificar')};província {String(draft.province_id??'por identificar')}. A localização, por si só, não concede benefícios. Os valores L6 continuam como referência;os efeitosL7 são apresentados separadamente. Saldo final a pagar/recuperar:âmbitoL8.</p>
<fieldset className="space-y-2 rounded border p-3"><legend>Despesas — tributação autónoma61(4)</legend><p>{specialSources.expenses}</p><QualificationFields label="Despesas autónomas" value={i.expenses.qualification} onChange={qualification=>change({expenses:{...i.expenses,qualification}})}/>
<button type="button" onClick={()=>change({expenses:{...i.expenses,rows:[...i.expenses.rows,...(b.schedules?.rows??[]).filter(r=>r.rule==='undocumented'&&!i.expenses.rows.some(x=>x.scheduleId===r.id)).map(r=>({...newSpecialExpense('expense-'+r.id),scheduleId:r.id}))]}})}>Reutilizar despesas36g de L6 para qualificar</button>
{i.expenses.rows.map((r,index)=>{const origin=b.schedules?.rows.find(x=>x.id===r.scheduleId),patch=(p:Partial<typeof r>)=>change({expenses:{...i.expenses,rows:i.expenses.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>{origin?origin.document+'/'+origin.line:'Parcela por corresponder'} — {origin?.amount??'Indeterminado'} MZN</legend><p>O acréscimo36g já pertence aL6;esta linha só apura o eventual imposto autónomo.</p><label className="block">Natureza legal da despesa<select aria-label={'Natureza da despesa '+(index+1)} value={r.nature} onChange={e=>patch({nature:e.target.value as typeof r.nature})}><option value="">Informação em falta</option><option value="undocumented">Encargo não devidamente documentado</option><option value="confidential">Despesa confidencial</option><option value="illicit">Despesa ilícita</option><option value="other">Outra natureza — correspondência própria</option></select></label>{yesno('Incidência61(4) — parcela '+(index+1),r.eligible,eligible=>patch({eligible}))}<label className="block">Suporte da classificação e incidência<input aria-label={'Suporte da despesa '+(index+1)} value={r.reference} onChange={e=>patch({reference:e.target.value})}/></label><button type="button" onClick={()=>change({expenses:{...i.expenses,rows:i.expenses.rows.filter(x=>x.id!==r.id)}})}>Retirar qualificação da parcela {index+1}</button></fieldset>;})}</fieldset>
<fieldset className="space-y-2 rounded border p-3"><legend>Cooperativas abrangidas — artigo12</legend><p>{specialSources.cooperative} Este incremento exige base integralmente elegível;parcelas mistas continuam explícitas.</p><QualificationFields label="Cooperativa" value={i.cooperative.qualification} onChange={qualification=>change({cooperative:{...i.cooperative,qualification}})}/>{(i.cooperative.qualification.state==='known'||i.cooperative.kind!==''||i.cooperative.allIncomeEligible!==''||i.cooperative.withheldIncome!=='')&&<><label className="block">Tipo de cooperativa documentado<select value={i.cooperative.kind} onChange={e=>change({cooperative:{...i.cooperative,kind:e.target.value as typeof i.cooperative.kind}})}><option value="">Por identificar</option><option value="agrarian">Agrária</option><option value="craft">Artesanato</option><option value="cultural">Cultural</option><option value="other">Outra — não correspondida</option></select></label>{yesno('Base ordinária integralmente elegível',i.cooperative.allIncomeEligible,allIncomeEligible=>change({cooperative:{...i.cooperative,allIncomeEligible}}))}<label className="block">Rendimentos sujeitos a retenção<select value={i.cooperative.withheldIncome} onChange={e=>change({cooperative:{...i.cooperative,withheldIncome:e.target.value as typeof i.cooperative.withheldIncome}})}><option value="">Informação em falta</option><option value="absent">Ausência documentada</option><option value="known">Existem — segregação própria em falta</option></select></label></>}</fieldset>
<fieldset className="space-y-2 rounded border p-3"><legend>Amortizações aceleradas — CBF16</legend><p>{specialSources.acceleration} Apenas activos já suportados,primeiro ano civil2026 e método constante;outras combinações permanecem por corresponder.</p><QualificationFields label="Amortização acelerada" value={i.acceleration.qualification} onChange={qualification=>change({acceleration:{...i.acceleration,qualification}})}/>{i.acceleration.qualification.state==='known'&&<>{yesno('EnquadramentoCBF3 e pressupostos9 documentados',i.acceleration.cbfRequirements,cbfRequirements=>change({acceleration:{...i.acceleration,cbfRequirements}}))}<label className="block">ReconhecimentoCBF10<select value={i.acceleration.recognition} onChange={e=>change({acceleration:{...i.acceleration,recognition:e.target.value as typeof i.acceleration.recognition}})}><option value="">Informação em falta</option><option value="documented">Instrumento/reconhecimento documentado</option><option value="not_required">Excepção prevista — identificar norma e suporte</option></select></label><label className="block">Instrumento ou norma da excepção<input value={i.acceleration.recognitionReference} onChange={e=>change({acceleration:{...i.acceleration,recognitionReference:e.target.value}})}/></label>{yesno('Ausência documentada de outros benefícios a cumular',i.acceleration.noOtherBenefits,noOtherBenefits=>change({acceleration:{...i.acceleration,noOtherBenefits}}))}</>}
<button type="button" onClick={()=>change({acceleration:{...i.acceleration,rows:[...i.acceleration.rows,newAcceleratedAsset(crypto.randomUUID())]}})}>Qualificar activo existente paraCBF16</button>{i.acceleration.rows.map((r,index)=>{const patch=(p:Partial<typeof r>)=>change({acceleration:{...i.acceleration,rows:i.acceleration.rows.map(x=>x.id===r.id?{...x,...p}:x)}});return<fieldset key={r.id} className="rounded border p-2"><legend>Activo qualificado {index+1}</legend><label className="block">Activo do mapaL6<select value={r.assetId} onChange={e=>patch({assetId:e.target.value})}><option value="">Seleccione activo existente</option>{b.depreciation?.assets.map(a=><option key={a.id} value={a.id}>{a.description} — {a.assetKey}</option>)}</select></label><label className="block">Afectação documentada<select value={r.kind} onChange={e=>patch({kind:e.target.value as typeof r.kind})}><option value="">Por identificar</option><option value="new_project_building">Imóvel novo utilizado no projecto</option><option value="rehabilitated_project_building">Imóvel reabilitado — regra ainda por corresponder</option><option value="industrial_equipment">Máquina/equipamento industrial</option><option value="agroindustrial_equipment">Máquina/equipamento agro-industrial</option><option value="other">Outro — correspondência própria</option></select></label>{yesno('Condições do activoCBF16 — '+(index+1),r.eligible,eligible=>patch({eligible}))}<label className="block">Prova da afectação e condições do activo<input value={r.reference} onChange={e=>patch({reference:e.target.value})}/></label><button type="button" onClick={()=>change({acceleration:{...i.acceleration,rows:i.acceleration.rows.filter(x=>x.id!==r.id)}})}>Retirar tratamento do activo {index+1}</button></fieldset>;})}</fieldset>
<p>Use «Calcular e guardar» abaixo para gravar uma nova versão e apresentar os efeitos. Os históricos anteriores são conservados. M20/M22 mantém apenas correspondências demonstradas;camposL7 não são preenchidos por aproximação.</p></section>;
}
+2 -2
View File
@@ -30,7 +30,7 @@ const ratio=(a:FiscalFraction,b:FiscalFraction)=>fraction(BigInt(a.numerator)*Bi
const date=(v:string)=>/^20\d{2}-\d{2}-\d{2}$/.test(v)&&Number.isFinite(Date.parse(v))&&new Date(v).toISOString().slice(0,10)===v;
const month=(v:string)=>Number(v.slice(0,4))*12+Number(v.slice(5,7))-1;
const show=(v:FiscalFraction|null)=>{if(!v)return'Indeterminado';const p=fractionPresentation(v);return(p.approximate?'≈':'')+p.decimal+' MZN'+(p.approximate?' (exacto '+v.numerator+'/'+v.denominator+')':'');};
export function calculateNormalDepreciation(input:NormalDepreciationInput,source:{currency:string;accountingLines:Array<{code:string;total:string}>}|null,period:{from:string;to:string},claims:Array<{sourceCode:string;document:string;line:string;amount:string}>,short=false){
export function calculateNormalDepreciation(input:NormalDepreciationInput,source:{currency:string;accountingLines:Array<{code:string;total:string}>}|null,period:{from:string;to:string},claims:Array<{sourceCode:string;document:string;line:string;amount:string}>,short=false,acceleratedIds:ReadonlySet<string>=new Set()){
validateNormalDepreciation(input);const pending:string[]=[];
if(!input.completenessReference.trim())pending.push('Reconciliação integral dos activos e quotas anteriores com a depreciação da origem em falta.');
if(source?.currency!=='MZN'||!date(period.from)||!date(period.to)||!period.to.startsWith('2026-')||(!short&&month(period.to)-month(period.from)!==11||short&&(month(period.to)<month(period.from)||month(period.to)-month(period.from)>=11)))pending.push('Período anual emMZN com termo em2026 por reconciliar; períodos curtos permanecem separados.');
@@ -60,7 +60,7 @@ export function calculateNormalDepreciation(input:NormalDepreciationInput,source
if(a.method==='low_value'){if(cmp(cost,fraction(20000n))>0n||!yesno(a.individual,'Activo individual fora de conjunto depreciado como um todo')||cmp(priorFiscal,zero())!==0n||a.start<period.from||a.start>period.to)throw Error('Art17:valor unitário até20000MZN,activo individual e início no período,sem amortização fiscal anterior; condições em falta/incompatíveis.');quota=remaining;months=month(period.to)-month(a.start)+1;notes.push('Art17:opção de depreciação integral em um período,limitada ao que está contabilizado e ao saldo fiscal.');}
else{
if(!entry||!maxRate||!maxLifeMonths)throw Error('Taxa aplicável não identificada.');
if(a.method==='constant'){if(!/^\d+(?:\.\d{1,6})?$/.test(a.rate))throw Error('Taxa constante escolhida por identificar.');const selected=decimalFraction(a.rate);if(cmp(selected,multiply(maxRate,fraction(1n,2n)))<0n||cmp(selected,maxRate)>0n)throw Error('Taxa constante fora do recorte entre metade da máxima e a máxima;quotas inferiores exigem correspondência própria dos limites de vida útil, artigos5/7.');quota=multiply(taxBasis,multiply(selected,fraction(1n,100n)));notes.push('Quota constante pela taxa escolhida:'+a.rate+'%;tabela'+entry.table+'/'+entry.code+'.');}
if(a.method==='constant'){if(!/^\d+(?:\.\d{1,6})?$/.test(a.rate))throw Error('Taxa constante escolhida por identificar.');const selected=decimalFraction(a.rate);if(cmp(selected,multiply(maxRate,fraction(1n,2n)))<0n||cmp(selected,maxRate)>0n)throw Error('Taxa constante fora do recorte entre metade da máxima e a máxima;quotas inferiores exigem correspondência própria dos limites de vida útil, artigos5/7.');quota=multiply(taxBasis,multiply(selected,fraction(acceleratedIds.has(a.id)?3n:1n,acceleratedIds.has(a.id)?200n:100n)));if(acceleratedIds.has(a.id))notes.push('CBF16:L7 qualificado,incremento50% da taxa normal;mantidos contabilização e saldo fiscal.');notes.push('Quota constante pela taxa escolhida:'+a.rate+'%;tabela'+entry.table+'/'+entry.code+'.');}
else if(a.method==='declining'){if(!yesno(a.newTangible,'Activo tangível novo adquirido a terceiros/construído')||['building','furniture','software'].includes(entry.kind)||entry.kind==='vehicle'&&a.vehicleException!=='yes')throw Error('Art6:quotas degressivas só nos activos tangíveis novos elegíveis;edifícios,mobiliário/equipamento social,intangíveis e veículos sem excepção ficam separados.');const life=ratio(fraction(100n),maxRate),maxFactor=cmp(life,fraction(5n))<0n?fraction(3n,2n):cmp(life,fraction(6n))<=0n?fraction(2n):fraction(5n,2n);if(!/^\d+(?:\.\d{1,6})?$/.test(a.factor))throw Error('Coeficiente degressivo escolhido em falta.');const factor=decimalFraction(a.factor);if(cmp(factor,zero())<=0n||cmp(factor,maxFactor)>0n)throw Error('Coeficiente degressivo excede o máximo do artigo8:1,5/2/2,5 conforme vida útil mínima.');quota=multiply(remaining,multiply(maxRate,multiply(factor,fraction(1n,100n))));notes.push('Art8:saldo fiscal por amortizar ×taxa máxima'+entry.rate+'% ×coeficiente'+a.factor+';máximo'+fractionPresentation(maxFactor).decimal+'.');
if(yesno(a.switchToStraight,'Opção pela regra de saldo/vida útil restante8(2)')){if(!yesno(a.priorFullQuotas,'Condição das quotas anteriores8(2)')||!a.start.endsWith('-01-01')||period.from!=='2026-01-01'||a.timing!=='annual')throw Error('Art8(2):condição das quotas anteriores por documentar;este recorte da opção exige início emJaneiro e período anual civil.');const remainingYears=subtractFractions(life,fraction(BigInt(2026-Number(a.start.slice(0,4)))));if(cmp(remainingYears,zero())<=0n)throw Error('Art8(2):vida útil mínima esgotada;sem dividir por anos inexistentes.');const linear=ratio(remaining,remainingYears);if(cmp(quota,linear)<0n){quota=linear;notes.push('Art8(2):usado saldo/número de anos de vida útil mínima restantes,com condições anteriores documentadas.');}}
}else throw Error('Método de amortização por identificar.');
+10 -7
View File
@@ -1,3 +1,4 @@
import {irpcSpecialVersion,validateSpecial,calculateSpecial,specialReportRows,specialDependency,type SpecialInput,type SpecialModel} from './irpc-special.ts';
import {normalFormPreparationVersion,validateNormalFormPreparation,prepareNormalForms,normalFormPreparationReport,type NormalFormPreparationInput,type NormalFormPreparationModel} from './irpc-normal-form-preparation.ts';
import {normalPeriodFxVersion,validateNormalPeriodFx,mapNormalPeriodFx,normalPeriodFxDependency,normalPeriodFxReport,type NormalPeriodFxInput,type NormalPeriodFxModel} from './irpc-normal-period-fx.ts';
import {normalDepreciationVersion,validateNormalDepreciation,calculateNormalDepreciation,normalDepreciationReport,type NormalDepreciationInput,type NormalDepreciationModel} from './irpc-normal-depreciation.ts';
@@ -15,8 +16,8 @@ import {addFractions,decimalFraction,fraction,fractionPresentation,type FiscalFr
export const irpcNormalVersion='IRPC-normal-adjustments/1';
export const irpcNormalSource='CIRPC Lei34/2007 art17(1),PDF6/p768(60):resultado e correcções;art36(1)c,PDF10/p768(64):multas e encargos por infracções sem origem contratual,incluindo juros compensatórios. Lei4/2012 PDF2 altera17(6)/acrescenta36(1)o;Lei19/2013 PDF2 mantém36(1)a–o e acrescenta p/q. Lei12/2025 altera3/5/20/62/67/75,adita61-A e revoga39/47/75(2)/76;sem alteração deste recorte36(1)c. Não é apuramento integral nem Modelo20/22.';
export type NormalAdjustment={id:string;sourceCode:string;document:string;line:string;amount:string;rule:''|'noncontractual_fine'|'compensatory_interest'|'other';included:''|'yes'|'no';reference:string};
export type NormalBook={version:1;year:string;mappingReference:string;scope:''|'resident_general'|'ep_general'|'other';scopeReference:string;rows:NormalAdjustment[];profit?:NormalProfitInput;base?:NormalBaseInput;assessment?:NormalAssessmentInput;schedules?:NormalSchedules;costs?:NormalCostsInput;depreciation?:NormalDepreciationInput;forms?:NormalFormPreparationInput;periodsFx?:NormalPeriodFxInput;result?:NormalRecord;history?:NormalRecord[]};
type NormalModel={formPreparation?:NormalFormPreparationModel;periodsFx?:NormalPeriodFxModel;depreciation?:NormalDepreciationModel;costs?:NormalCostsModel;fiscalPeriod?:{kind:string;from:string;to:string;reference:string;pending:string[]};schedules?:SchedulesModel;currentLoss?:FiscalFraction|null;assessment?:NormalAssessmentModel;colecta?:FiscalFraction|null;base?:NormalBaseModel;profit?:NormalProfitModel;version:string;year:string;currency:string;origin:'projected';accountingProfit:FiscalFraction|null;additions:FiscalFraction;partialAdjustedResult:FiscalFraction|null;taxableProfit:FiscalFraction|null;taxableBase:FiscalFraction|null;tax:null;rows:Array<NormalAdjustment&{status:'calculated'|'indeterminate'|'outside_scope';adjustment:FiscalFraction|null;reason:string}>;pending:string[];source:string};
export type NormalBook={version:1;year:string;mappingReference:string;scope:''|'resident_general'|'ep_general'|'other';scopeReference:string;rows:NormalAdjustment[];special?:SpecialInput;profit?:NormalProfitInput;base?:NormalBaseInput;assessment?:NormalAssessmentInput;schedules?:NormalSchedules;costs?:NormalCostsInput;depreciation?:NormalDepreciationInput;forms?:NormalFormPreparationInput;periodsFx?:NormalPeriodFxInput;result?:NormalRecord;history?:NormalRecord[]};
export type NormalModel={special?:SpecialModel;formPreparation?:NormalFormPreparationModel;periodsFx?:NormalPeriodFxModel;depreciation?:NormalDepreciationModel;costs?:NormalCostsModel;fiscalPeriod?:{kind:string;from:string;to:string;reference:string;pending:string[]};schedules?:SchedulesModel;currentLoss?:FiscalFraction|null;assessment?:NormalAssessmentModel;colecta?:FiscalFraction|null;base?:NormalBaseModel;profit?:NormalProfitModel;version:string;year:string;currency:string;origin:'projected';accountingProfit:FiscalFraction|null;additions:FiscalFraction;partialAdjustedResult:FiscalFraction|null;taxableProfit:FiscalFraction|null;taxableBase:FiscalFraction|null;tax:null;rows:Array<NormalAdjustment&{status:'calculated'|'indeterminate'|'outside_scope';adjustment:FiscalFraction|null;reason:string}>;pending:string[];source:string};
type NormalRecord={key:string;createdAt:string;model:NormalModel};
export const newNormalBook=():NormalBook=>({version:1,year:'',mappingReference:'',scope:'',scopeReference:'',rows:[]});
export const newNormalAdjustment=(id:string):NormalAdjustment=>({id,sourceCode:'',document:'',line:'',amount:'',rule:'',included:'',reference:''});
@@ -28,6 +29,7 @@ export function rawNormalFinancialSource(d:Record<string,unknown>){
export function normalFinancialSource(d:Record<string,unknown>){const raw=rawNormalFinancialSource(d),input=normalBook(d).periodsFx;return input?mapNormalPeriodFx(d,input,raw).source:raw;}
function store(d:Record<string,unknown>){const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books||typeof s.books!=='object'||Array.isArray(s.books))throw Error('Livros fiscais não recuperáveis;originais preservados.');return s;}
export function validateNormal(b:NormalBook){
if(b?.special!==undefined)validateSpecial(b.special);
if(b?.forms!==undefined)validateNormalFormPreparation(b.forms);
if(b?.periodsFx!==undefined)validateNormalPeriodFx(b.periodsFx);
if(b?.depreciation!==undefined)validateNormalDepreciation(b.depreciation);
@@ -39,13 +41,13 @@ export function validateNormal(b:NormalBook){
if(!b||b.version!==1||['year','mappingReference','scopeReference'].some(k=>typeof b[k as 'year']!=='string')||!['','resident_general','ep_general','other'].includes(b.scope)||!Array.isArray(b.rows)||b.history!==undefined&&!Array.isArray(b.history))throw Error('Preparação IRPC normal não recuperável.');
for(const r of b.rows)if(!r||Object.keys(newNormalAdjustment('')).some(k=>typeof r[k as keyof NormalAdjustment]!=='string')||!['','noncontractual_fine','compensatory_interest','other'].includes(r.rule)||!['','yes','no'].includes(r.included))throw Error('Ajustamento IRPC não recuperável;original conservado.');
if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('Identificador de ajustamento duplicado.');
for(const r of [b.result,...(b.history??[])])if(r&&(!r.model||![irpcNormalVersion,normalProfitVersion,normalBaseVersion,normalAssessmentVersion,normalSchedulesVersion,normalCostsVersion,normalDepreciationVersion,normalPeriodFxVersion,normalFormPreparationVersion].includes(r.model.version)||typeof r.key!=='string'||typeof r.createdAt!=='string'||!Array.isArray(r.model.rows)||!Array.isArray(r.model.pending)))throw Error('Histórico IRPC normal não recuperável.');
for(const r of [b.result,...(b.history??[])])if(r&&(!r.model||![irpcSpecialVersion,irpcNormalVersion,normalProfitVersion,normalBaseVersion,normalAssessmentVersion,normalSchedulesVersion,normalCostsVersion,normalDepreciationVersion,normalPeriodFxVersion,normalFormPreparationVersion].includes(r.model.version)||typeof r.key!=='string'||typeof r.createdAt!=='string'||!Array.isArray(r.model.rows)||!Array.isArray(r.model.pending)))throw Error('Histórico IRPC normal não recuperável.');
}
export function normalBook(d:Record<string,unknown>):NormalBook{
const m=store(d).irpcNormal;if(m===undefined)return newNormalBook();if(!m||typeof m!=='object'||Array.isArray(m))throw Error('Mapa IRPC normal não recuperável.');const b=m[acquisitionScope(d)]??newNormalBook();validateNormal(b);return b;
}
export function saveNormalBook(d:Record<string,unknown>,b:NormalBook){validateNormal(b);const s=store(d);if(s.irpcNormal!==undefined&&(!s.irpcNormal||typeof s.irpcNormal!=='object'||Array.isArray(s.irpcNormal)))throw Error('Mapa IRPC normal não recuperável.');return JSON.stringify({...s,irpcNormal:{...s.irpcNormal,[acquisitionScope(d)]:b}});}
function key(d:Record<string,unknown>,b:NormalBook){return JSON.stringify([b.forms?normalFormPreparationVersion:b.periodsFx?normalPeriodFxVersion:b.depreciation?normalDepreciationVersion:b.costs?normalCostsVersion:b.schedules?normalSchedulesVersion:b.assessment?normalAssessmentVersion:b.base?normalBaseVersion:b.profit?normalProfitVersion:irpcNormalVersion,acquisitionScope(d),d.business_plan_snapshot??'',b.year,b.mappingReference,b.scope,b.scopeReference,b.rows,...(b.profit?[b.profit]:[]),...(b.base?[b.base]:[]),...(b.assessment?[b.assessment]:[]),...(b.schedules?[b.schedules]:[]),...(b.costs?[b.costs]:[]),...(b.depreciation?[b.depreciation]:[]),...(b.periodsFx?[b.periodsFx,normalPeriodFxDependency(d,b.periodsFx)]:[]),...(b.forms?[b.forms]:[])]);}
function key(d:Record<string,unknown>,b:NormalBook){return JSON.stringify([b.special?irpcSpecialVersion:b.forms?normalFormPreparationVersion:b.periodsFx?normalPeriodFxVersion:b.depreciation?normalDepreciationVersion:b.costs?normalCostsVersion:b.schedules?normalSchedulesVersion:b.assessment?normalAssessmentVersion:b.base?normalBaseVersion:b.profit?normalProfitVersion:irpcNormalVersion,acquisitionScope(d),d.business_plan_snapshot??'',b.year,b.mappingReference,b.scope,b.scopeReference,b.rows,...(b.profit?[b.profit]:[]),...(b.base?[b.base]:[]),...(b.assessment?[b.assessment]:[]),...(b.schedules?[b.schedules]:[]),...(b.costs?[b.costs]:[]),...(b.depreciation?[b.depreciation]:[]),...(b.periodsFx?[b.periodsFx,normalPeriodFxDependency(d,b.periodsFx)]:[]),...(b.forms?[b.forms]:[]),...(b.special?[b.special,specialDependency(d)]:[])]);}
export function normalCurrent(d:Record<string,unknown>,b:NormalBook){return !!b.result&&b.result.key===key(d,b);}
const zero=()=>fraction(0n,1n);
const cents=(s:string)=>{if(!/^-?\d+(?:\.\d{1,2})?$/.test(s))throw Error('Montante MZN inválido;máximo dois decimais.');const v=decimalFraction(s.startsWith('-')?s.slice(1):s);return s.startsWith('-')?fraction(-BigInt(v.numerator),BigInt(v.denominator)):v;};
@@ -94,7 +96,8 @@ export function calculateNormal(d:Record<string,unknown>,b:NormalBook):NormalMod
const assessment=b.assessment?calculateNormalAssessment(b.assessment,base?.taxableBase??null,{year:b.year,scope:b.scope,extended:!!b.schedules||!!periodsFx,short:!!periodsFx?.short,currency:source?.currency??'',...(fiscalPeriod?{period:{from:fiscalPeriod.from,to:fiscalPeriod.to}}:{})}):undefined;
if(assessment)pending.push(...assessment.pending,assessment.finalBalance.reason);
const currentLoss=b.schedules&&profit?.taxableProfit&&BigInt(profit.taxableProfit.numerator)<0n?fraction(-BigInt(profit.taxableProfit.numerator),BigInt(profit.taxableProfit.denominator)):null;
const model:NormalModel={...(periodsFx?{periodsFx}:{}),...(depreciation?{depreciation}:{}),...(costs?{costs}:{}),...(schedules?{schedules,currentLoss,fiscalPeriod}:{}),...(assessment?{assessment,colecta:assessment.colecta}:{}),...(base?{base}:{}),...(profit?{profit}:{}),version:b.forms?normalFormPreparationVersion:b.periodsFx?normalPeriodFxVersion:b.depreciation?normalDepreciationVersion:b.costs?normalCostsVersion:b.schedules?normalSchedulesVersion:b.assessment?normalAssessmentVersion:b.base?normalBaseVersion:b.profit?normalProfitVersion:irpcNormalVersion,year:b.year,currency:source?.currency??'',origin:'projected',accountingProfit,additions,partialAdjustedResult:contextReady&&rows.length>0&&rows.every(r=>r.status==='calculated')&&accountingProfit?addFractions([accountingProfit,additions]):null,taxableProfit:profit?.taxableProfit??null,taxableBase:base?.taxableBase??null,tax:null,rows,pending,source:irpcNormalSource+(periodsFx?' '+periodsFx.rule:b.schedules?' Mapas complementares: CIRPC18/21/22/24/36; Decreto72/2013 art3–7. EP: CIRPC15(1)c, apenas resultado imputável ao estabelecimento no regime geral, anual2026/MZN.':'')};
const model:NormalModel={...(periodsFx?{periodsFx}:{}),...(depreciation?{depreciation}:{}),...(costs?{costs}:{}),...(schedules?{schedules,currentLoss,fiscalPeriod}:{}),...(assessment?{assessment,colecta:assessment.colecta}:{}),...(base?{base}:{}),...(profit?{profit}:{}),version:b.special?irpcSpecialVersion:b.forms?normalFormPreparationVersion:b.periodsFx?normalPeriodFxVersion:b.depreciation?normalDepreciationVersion:b.costs?normalCostsVersion:b.schedules?normalSchedulesVersion:b.assessment?normalAssessmentVersion:b.base?normalBaseVersion:b.profit?normalProfitVersion:irpcNormalVersion,year:b.year,currency:source?.currency??'',origin:'projected',accountingProfit,additions,partialAdjustedResult:contextReady&&rows.length>0&&rows.every(r=>r.status==='calculated')&&accountingProfit?addFractions([accountingProfit,additions]):null,taxableProfit:profit?.taxableProfit??null,taxableBase:base?.taxableBase??null,tax:null,rows,pending,source:irpcNormalSource+(periodsFx?' '+periodsFx.rule:b.schedules?' Mapas complementares: CIRPC18/21/22/24/36; Decreto72/2013 art3–7. EP: CIRPC15(1)c, apenas resultado imputável ao estabelecimento no regime geral, anual2026/MZN.':'')};
if(b.special)model.special=calculateSpecial(d,b,model,source);
if(b.forms){model.formPreparation=prepareNormalForms(b.forms,model,source);model.pending.push(...model.formPreparation.pending.map(p=>'Preparação M20/M22: '+p));}
return model;
}
@@ -102,5 +105,5 @@ export function recordNormal(d:Record<string,unknown>,b:NormalBook,now=new Date(
const money=(v:FiscalFraction|null)=>v?fractionPresentation(v).decimal+' MZN':'Indeterminado';
function normalBaseReport(m:NormalBaseModel):Array<[string,string,string,string]>{return[['IRPC normal — L6','Matéria colectável — deduções/perdas',`Lucro tributável:${money(m.taxableProfit)};perdas propostas elegíveis:${money(m.requestedLosses)};perdas deduzidas nesta simulação:${money(m.deductedLosses)};benefícios deduzidos:${money(m.benefitDeduction)};matéria colectável simulada:${money(m.taxableBase)};imposto a pagar:Indeterminado. ${m.pending.join(' | ')}`,`NUIT:${m.input.subjectNuit};${m.input.identityReference};método:${m.input.method==='direct'?'Declarativo/directo':m.input.method==='indirect'?'Indirecto':'Por identificar'};${m.input.methodReference};perdas:${m.input.lossesState==='absent'?'Ausência declarada':m.input.lossesState==='known'?'Conhecidas':'Informação em falta'};${m.input.lossesReference};objecto:${m.input.unchangedObject==='yes'?'Sem alteração declarada':m.input.unchangedObject==='no'?'Alteração declarada':'Por esclarecer'};${m.input.objectReference};${m.source}`],['IRPC normal — L6','Benefícios à matéria colectável',`Estado:${m.input.benefits.state==='known'?'Valor conhecido':m.input.benefits.state==='absent'?'Ausência declarada':'Informação em falta'};montante original:${m.input.benefits.amount||'Não preenchido'};dedução:${money(m.benefitDeduction)};não inclui créditos à colecta ou retenções sofridas.`,m.input.benefits.reference],...m.losses.map(l=>['IRPC normal — L6','Perda fiscal de '+l.year,`Estado:${l.status==='eligible'?'Elegível neste recorte':l.status==='excluded'?'Excluída neste exercício':'Indeterminada'};apurada:${l.assessed};utilizada antes:${l.previouslyUsed};saldo:${money(l.available)};utilização proposta:${l.requested};dedução nesta simulação:${money(l.deducted)};saldo aritmético posterior:${money(l.remaining)};último exercício ordinário:${l.lastOrdinaryYear??'Por determinar'};${l.reason}`,`NUIT:${l.subjectNuit};${l.reference};sem consumo do saldo oficial nem reporte automático.`]as[string,string,string,string])];}
function normalAssessmentReport(m:NormalAssessmentModel):Array<[string,string,string,string]>{return[['IRPC normal — L6','Colecta — regime normal',`Matéria colectável:${money(m.taxableBase)};taxa:${m.rate?m.rate.numerator+'%':'Por identificar'};colecta simulada:${money(m.colecta)};${m.classification};${m.centExact===false?'Valor racional exacto com fracção de cêntimo;sem arredondamento fiscal aplicado.':''}${m.pending.join(' | ')}`,`Exercício:${m.period.year};período:${m.period.from}–${m.period.to};${m.input.reference};${m.rule};${m.source}`],['IRPC normal — L6','Saldo final — âmbito L8','A pagar:Indeterminado;a recuperar:Indeterminado. '+m.finalBalance.reason,'Sequência:lucro tributável → matéria colectável → colecta → saldo final;retenções,pagamentos e créditos anuais reservados aL8.']];}
export function normalReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=normalBook(d),r=b.result;if(!r)return[];const m=r.model;return[['IRPC normal — L6','Ajustamentos ao resultado financeiro',`Exercício:${m.year};resultado contabilístico antes de imposto:${money(m.accountingProfit)};acréscimos demonstrados:${money(m.additions)};resultado parcial ajustado:${money(m.partialAdjustedResult)};${m.profit?'lucro tributável simulado:'+money(m.taxableProfit)+(m.base?';matéria colectável simulada:'+money(m.taxableBase)+(m.assessment?';colecta simulada:'+money(m.colecta??null)+';saldo final a pagar ou recuperar:Indeterminado (L8).':';imposto a pagar:Indeterminado.'):';matéria colectável/imposto:Indeterminados.'):'lucro tributável/matéria colectável/imposto:Indeterminados.'} Projecção com pressupostos;${m.pending.join(' | ')}`,`${normalCurrent(d,b)?'Entradas correspondentes':'Histórico;origem ou entradas alteradas'};${r.createdAt};${m.source}`],...(m.formPreparation?normalFormPreparationReport(m.formPreparation):[]),...(m.periodsFx?normalPeriodFxReport(m.periodsFx):[]),...(m.depreciation?normalDepreciationReport(m.depreciation):[]),...(m.costs?normalCostReport(m.costs):[]),...(m.base?normalBaseReport(m.base):[]),...(m.assessment?normalAssessmentReport(m.assessment):[]),...(m.schedules?[['IRPC normal — L6','Mapas complementares',`Efeito líquido adicional:${money(m.schedules.net)};perda fiscal do próprio exercício:${money(m.currentLoss??null)};sem reporte ou consumo automático. ${m.schedules.pending.join(' | ')}`,m.schedules.source]as[string,string,string,string],...m.schedules.effects.map(e=>{const i=m.schedules!.input.rows.find(r=>r.id===e.id)!;return['IRPC normal — L6',i.document+'/'+i.line,`Montante:${money(e.amount)};efeito:${money(e.effect)};${e.reason}`,`${i.reference};exercício de imputação:${i.fiscalYear||'Não aplicável'}`]as[string,string,string,string];}),...m.schedules.assets.map(a=>['IRPC normal — L6',a.description,`Depreciação contabilística:${money(a.accountingValue)};quota fiscal:${money(a.fiscal)};acréscimo:${money(a.adjustment)};${a.reason}`,`${a.tableReference};${a.reference}`]as[string,string,string,string]),['IRPC normal — L6','Juros financeiros ordinários',`Remanescente aceite no recorte:${money(m.schedules.interest)};não corresponde a juros de mora/compensatórios L5.`,m.schedules.input.interest.reference]as[string,string,string,string]]:[]),...(m.schedules&&!m.formPreparation?[...normalFormFields(m).fields.map(f=>['IRPC normal — L6','M22 campo '+f.code+' — '+f.label,`${f.display};exacto:${f.value?f.value.numerator+'/'+f.value.denominator:'Indeterminado'};transcrição:${f.transcriptionReady?'Preparação parcial com valor exacto ao cêntimo':'Pendente ou sem aplicação'};${f.reason}`,normalFormFields(m).source]as[string,string,string,string]),...normalFormFields(m).m20.fields.map(f=>['IRPC normal — L6','M20 A.1 — conta '+f.account+' — '+f.label,`${f.display};representação em meticais inteiros:${f.transcriptionReady?'Exacta':'Pendente'};${f.reason}`,`${f.reference};${normalFormFields(m).m20.source}`]as[string,string,string,string]),['IRPC normal — L6','M20 — correspondência',normalFormFields(m).m20.reason,normalFormFields(m).m20.source]as[string,string,string,string]]:[]),...m.rows.map(r=>['IRPC normal — L6',r.document+'/'+r.line,`${({calculated:'Calculado neste recorte',indeterminate:'Indeterminado',outside_scope:'Fora deste recorte'})[r.status]};montante:${r.amount};acréscimo:${money(r.adjustment)};${r.reason}`,`Origem:${({revenue:'Receitas',cost_of_sales:'Custo das vendas',operating_expenses:'Despesas operacionais',personnel:'Pessoal',depreciation:'Depreciação contabilística',interest_expense:'Juros reconhecidos'}as Record<string,string>)[r.sourceCode]??'Por corresponder'};incluído:${r.included==='yes'?'Sim':r.included==='no'?'Não':'Por demonstrar'};suporte:${r.reference}`]as[string,string,string,string]),...(m.profit?[...m.profit.lines.map(l=>['IRPC normal — L6','Conferência: '+l.label,`Estado:${l.state==='known'?'Valor conhecido':l.state==='absent'?'Ausência declarada':'Informação em falta'};origem:${l.amount};acréscimos:${money(l.addedBack)};remanescente:${money(l.remainder)};${l.reason}`,l.reference]as[string,string,string,string]),...m.profit.topics.map(t=>['IRPC normal — L6',t.label,`Estado:${t.state==='known'?'Valor conhecido':t.state==='absent'?'Ausência declarada':'Informação em falta'};montante:${money(t.value)};${t.reason}`,t.reference]as[string,string,string,string]),['IRPC normal — L6','Âmbito da conferência',m.profit.input.completenessReference,m.profit.source]as[string,string,string,string]]:[])];}catch(e){return[['IRPC normal — L6','Não recuperável',String(e),'Originais conservados.']];}}
export function normalHtml(d:Record<string,unknown>){const b=normalBook(d);if(!normalCurrent(d,b))throw Error('Calcule e guarde as entradas actuais antes de exportar.');const esc=(s:string)=>s.replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));return'<!doctype html><html lang="pt"><meta charset="utf-8"><title>IRPC normal — ajustamentos</title><h1>L6 — mapa parcial de ajustamentos</h1><p>Projecção pelos pressupostos do plano. Não é demonstração contabilística,liquidação ou Modelo20/22.</p>'+normalReportRows(d).map(r=>'<section><h2>'+esc(r[1])+'</h2><p>'+esc(r[2])+'</p><small>'+esc(r[3])+'</small></section>').join('')+'</html>';}
export function normalReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=normalBook(d),r=b.result;if(!r)return[];const m=r.model;return[['IRPC normal — L6','Ajustamentos ao resultado financeiro',`Exercício:${m.year};resultado contabilístico antes de imposto:${money(m.accountingProfit)};acréscimos demonstrados:${money(m.additions)};resultado parcial ajustado:${money(m.partialAdjustedResult)};${m.profit?'lucro tributável simulado:'+money(m.taxableProfit)+(m.base?';matéria colectável simulada:'+money(m.taxableBase)+(m.assessment?';colecta simulada:'+money(m.colecta??null)+';saldo final a pagar ou recuperar:Indeterminado (L8).':';imposto a pagar:Indeterminado.'):';matéria colectável/imposto:Indeterminados.'):'lucro tributável/matéria colectável/imposto:Indeterminados.'} Projecção com pressupostos;${m.pending.join(' | ')}`,`${normalCurrent(d,b)?'Entradas correspondentes':'Histórico;origem ou entradas alteradas'};${r.createdAt};${m.source}`],...(m.special?specialReportRows(m.special):[]),...(m.formPreparation?normalFormPreparationReport(m.formPreparation):[]),...(m.periodsFx?normalPeriodFxReport(m.periodsFx):[]),...(m.depreciation?normalDepreciationReport(m.depreciation):[]),...(m.costs?normalCostReport(m.costs):[]),...(m.base?normalBaseReport(m.base):[]),...(m.assessment?normalAssessmentReport(m.assessment):[]),...(m.schedules?[['IRPC normal — L6','Mapas complementares',`Efeito líquido adicional:${money(m.schedules.net)};perda fiscal do próprio exercício:${money(m.currentLoss??null)};sem reporte ou consumo automático. ${m.schedules.pending.join(' | ')}`,m.schedules.source]as[string,string,string,string],...m.schedules.effects.map(e=>{const i=m.schedules!.input.rows.find(r=>r.id===e.id)!;return['IRPC normal — L6',i.document+'/'+i.line,`Montante:${money(e.amount)};efeito:${money(e.effect)};${e.reason}`,`${i.reference};exercício de imputação:${i.fiscalYear||'Não aplicável'}`]as[string,string,string,string];}),...m.schedules.assets.map(a=>['IRPC normal — L6',a.description,`Depreciação contabilística:${money(a.accountingValue)};quota fiscal:${money(a.fiscal)};acréscimo:${money(a.adjustment)};${a.reason}`,`${a.tableReference};${a.reference}`]as[string,string,string,string]),['IRPC normal — L6','Juros financeiros ordinários',`Remanescente aceite no recorte:${money(m.schedules.interest)};não corresponde a juros de mora/compensatórios L5.`,m.schedules.input.interest.reference]as[string,string,string,string]]:[]),...(m.schedules&&!m.formPreparation?[...normalFormFields(m).fields.map(f=>['IRPC normal — L6','M22 campo '+f.code+' — '+f.label,`${f.display};exacto:${f.value?f.value.numerator+'/'+f.value.denominator:'Indeterminado'};transcrição:${f.transcriptionReady?'Preparação parcial com valor exacto ao cêntimo':'Pendente ou sem aplicação'};${f.reason}`,normalFormFields(m).source]as[string,string,string,string]),...normalFormFields(m).m20.fields.map(f=>['IRPC normal — L6','M20 A.1 — conta '+f.account+' — '+f.label,`${f.display};representação em meticais inteiros:${f.transcriptionReady?'Exacta':'Pendente'};${f.reason}`,`${f.reference};${normalFormFields(m).m20.source}`]as[string,string,string,string]),['IRPC normal — L6','M20 — correspondência',normalFormFields(m).m20.reason,normalFormFields(m).m20.source]as[string,string,string,string]]:[]),...m.rows.map(r=>['IRPC normal — L6',r.document+'/'+r.line,`${({calculated:'Calculado neste recorte',indeterminate:'Indeterminado',outside_scope:'Fora deste recorte'})[r.status]};montante:${r.amount};acréscimo:${money(r.adjustment)};${r.reason}`,`Origem:${({revenue:'Receitas',cost_of_sales:'Custo das vendas',operating_expenses:'Despesas operacionais',personnel:'Pessoal',depreciation:'Depreciação contabilística',interest_expense:'Juros reconhecidos'}as Record<string,string>)[r.sourceCode]??'Por corresponder'};incluído:${r.included==='yes'?'Sim':r.included==='no'?'Não':'Por demonstrar'};suporte:${r.reference}`]as[string,string,string,string]),...(m.profit?[...m.profit.lines.map(l=>['IRPC normal — L6','Conferência: '+l.label,`Estado:${l.state==='known'?'Valor conhecido':l.state==='absent'?'Ausência declarada':'Informação em falta'};origem:${l.amount};acréscimos:${money(l.addedBack)};remanescente:${money(l.remainder)};${l.reason}`,l.reference]as[string,string,string,string]),...m.profit.topics.map(t=>['IRPC normal — L6',t.label,`Estado:${t.state==='known'?'Valor conhecido':t.state==='absent'?'Ausência declarada':'Informação em falta'};montante:${money(t.value)};${t.reason}`,t.reference]as[string,string,string,string]),['IRPC normal — L6','Âmbito da conferência',m.profit.input.completenessReference,m.profit.source]as[string,string,string,string]]:[])];}catch(e){return[['IRPC normal — L6','Não recuperável',String(e),'Originais conservados.']];}}
export function normalHtml(d:Record<string,unknown>){const b=normalBook(d);if(!normalCurrent(d,b))throw Error('Calcule e guarde as entradas actuais antes de exportar.');const esc=(s:string)=>s.replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));return'<!doctype html><html lang="pt"><meta charset="utf-8"><title>IRPC normal — ajustamentos</title><h1>IRPC — L6 de referência e tratamentos L7 quando qualificados</h1><p>Projecção pelos pressupostos do plano. Não é demonstração contabilística,liquidação ou Modelo20/22.</p>'+normalReportRows(d).map(r=>'<section><h2>'+esc(r[1])+'</h2><p>'+esc(r[2])+'</p><small>'+esc(r[3])+'</small></section>').join('')+'</html>';}
+125
View File
@@ -0,0 +1,125 @@
import type {NormalBook,NormalModel} from './irpc-normal.ts';
import {calculateNormalBase} from './irpc-normal-base.ts';
import {calculateNormalAssessment} from './irpc-normal-assessment.ts';
import {calculateNormalDepreciation,type NormalDepreciationModel} from './irpc-normal-depreciation.ts';
import {addFractions,subtractFractions,decimalFraction,fraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
export const irpcSpecialVersion='IRPC-normal-adjustments/10';
export const specialSources={expenses:'CIRPC,Lei34/2007,art61(4),PDF17/p768(71):35% sobre encargos não devidamente documentados e despesas confidenciais/ilícitas,sem prejuízo do36(1)g. Exercício2026;sem regra inferida da falta de anexo.',cooperative:'CIRPC,Lei34/2007,art12/14,PDF5/p768(59),e61(1):cooperativas agrárias,de artesanato e culturais;redução de50% da taxa geral32%=16%;rendimentos sujeitos a retenção excluídos deste benefício. Recorte2026,residente,parcela ordinária integralmente elegível.',acceleration:'CBF,Lei4/2009,art3/4/9/10/16;compilação oficial APIEX pp66–68/72:incremento50% das taxas normais nos imóveis novos do projecto e,nas mesmas condições,imóveis reabilitados/máquinas e equipamentos industriais ou agro-industriais. Decreto72/2013 art3(3):limite contabilizado e saldo fiscal. Recorte constante pela taxa normal máxima da tabela,primeiro exercício anual2026,sem cumulação presumida.'}as const;
export type Qualification={state:''|'known'|'absent'|'not_applicable';eligible:''|'yes'|'no';from:string;to:string;reference:string;periodReference:string};
export const newQualification=():Qualification=>({state:'',eligible:'',from:'',to:'',reference:'',periodReference:''});
export type SpecialExpense={id:string;scheduleId:string;nature:''|'undocumented'|'confidential'|'illicit'|'other';eligible:''|'yes'|'no';reference:string};
export type AcceleratedAsset={id:string;assetId:string;kind:''|'new_project_building'|'rehabilitated_project_building'|'industrial_equipment'|'agroindustrial_equipment'|'other';eligible:''|'yes'|'no';reference:string};
export type SpecialInput={version:1;expenses:{qualification:Qualification;rows:SpecialExpense[]};cooperative:{qualification:Qualification;kind:''|'agrarian'|'craft'|'cultural'|'other';allIncomeEligible:''|'yes'|'no';withheldIncome:''|'absent'|'known'};acceleration:{qualification:Qualification;cbfRequirements:''|'yes'|'no';recognition:''|'documented'|'not_required';recognitionReference:string;noOtherBenefits:''|'yes'|'no';rows:AcceleratedAsset[]}};
export const newSpecialInput=():SpecialInput=>({version:1,expenses:{qualification:newQualification(),rows:[]},cooperative:{qualification:newQualification(),kind:'',allIncomeEligible:'',withheldIncome:''},acceleration:{qualification:newQualification(),cbfRequirements:'',recognition:'',recognitionReference:'',noOtherBenefits:'',rows:[]}});
export const newSpecialExpense=(id:string):SpecialExpense=>({id,scheduleId:'',nature:'',eligible:'',reference:''});
export const newAcceleratedAsset=(id:string):AcceleratedAsset=>({id,assetId:'',kind:'',eligible:'',reference:''});
const choices=(v:unknown,allowed:string[])=>typeof v==='string'&&allowed.includes(v);
export function validateSpecial(i:SpecialInput){
if(!i||i.version!==1||!i.expenses||!i.cooperative||!i.acceleration)throw Error('Qualificação L7 não recuperável;originais conservados.');
for(const q of[i.expenses.qualification,i.cooperative.qualification,i.acceleration.qualification])if(!q||!choices(q.state,['','known','absent','not_applicable'])||!choices(q.eligible,['','yes','no'])||['from','to','reference','periodReference'].some(k=>typeof q[k as 'from']!=='string'))throw Error('Condição/período L7 não recuperável.');
for(const [rows,keys]of [[i.expenses.rows,Object.keys(newSpecialExpense(''))],[i.acceleration.rows,Object.keys(newAcceleratedAsset(''))]]as const){if(!Array.isArray(rows)||new Set(rows.map(r=>r.id)).size!==rows.length||rows.some(r=>!r||keys.some(k=>typeof(r as unknown as Record<string,unknown>)[k]!=='string')||!choices(r.eligible,['','yes','no'])))throw Error('Parcela L7 não recuperável ou identificador repetido.');}
if(i.expenses.rows.some(r=>!choices(r.nature,['','undocumented','confidential','illicit','other']))||i.acceleration.rows.some(r=>!choices(r.kind,['','new_project_building','rehabilitated_project_building','industrial_equipment','agroindustrial_equipment','other'])))throw Error('Natureza L7 desconhecida.');
if(!choices(i.cooperative.kind,['','agrarian','craft','cultural','other'])||!choices(i.cooperative.allIncomeEligible,['','yes','no'])||!choices(i.cooperative.withheldIncome,['','absent','known'])||!choices(i.acceleration.cbfRequirements,['','yes','no'])||!choices(i.acceleration.noOtherBenefits,['','yes','no'])||!choices(i.acceleration.recognition,['','documented','not_required'])||typeof i.acceleration.recognitionReference!=='string')throw Error('Parâmetro L7 não recuperável.');
}
type Status='calculated'|'absent'|'not_applicable'|'indeterminate';
type Decision={status:Status;reason:string;pending:string[]};
const z=()=>fraction(0n),mul=(v:FiscalFraction,n:bigint,d=1n)=>fraction(BigInt(v.numerator)*n,BigInt(v.denominator)*d);
const cmp=(a:FiscalFraction,b:FiscalFraction)=>BigInt(a.numerator)*BigInt(b.denominator)-BigInt(b.numerator)*BigInt(a.denominator);
const iso=(s:string)=>/^20\d{2}-\d{2}-\d{2}$/.test(s)&&Number.isFinite(Date.parse(s))&&new Date(s).toISOString().slice(0,10)===s;
export function specialContext(d:Record<string,unknown>){return Object.fromEntries(['context_opportunity','context_opportunity_snapshot','context_applicability_snapshot','context_reuse','sector_id','sector_id_label','province_id','province_id_label','district_id','district_id_label','admin_post_id','admin_post_id_label','locality_id','locality_id_label','selection_labels'].map(k=>[k,d[k]??null]));}
export const specialDependency=(d:Record<string,unknown>)=>JSON.stringify(specialContext(d));
const specialPeriod=(b:NormalBook,m:NormalModel)=>b.periodsFx?{from:b.periodsFx.from,to:b.periodsFx.to,pending:m.periodsFx?.pending??[]}:m.fiscalPeriod??{from:b.year+'-01-01',to:b.year+'-12-31',pending:[]};
function qualify(q:Qualification,b:NormalBook,m:NormalModel):Decision{
const pending:string[]=[],p=specialPeriod(b,m);
if(!q.state)pending.push('Informação em falta:identifique o estado do tratamento.');
if(!q.reference.trim())pending.push('Falta prova/declaração das condições ou da ausência/não aplicação.');
if(!iso(q.from)||!iso(q.to)||q.from!==p.from||q.to!==p.to||!q.periodReference.trim())pending.push('Período do tratamento deve corresponder ao período fiscal documentado,com suporte próprio;sem transpor vigência.');
if(b.year!=='2026'||m.currency!=='MZN'||!b.base?.subjectNuit.match(/^\d{9}$/)||!b.base.identityReference.trim()||!b.scopeReference.trim()||!['resident_general','ep_general'].includes(b.scope)||p.pending.length)pending.push('Sujeito/NUIT,âmbito ou período2026 emMZN por reconciliar na base existente.');
if(q.state==='known'&&!q.eligible)pending.push('Condições documentadas por qualificar:sim,não ou informação em falta.');
if(q.state!=='known'&&q.eligible)pending.push('Estado de ausência/não aplicação/informação em falta não admite condição de benefício preenchida.');
if(pending.length)return{status:'indeterminate',pending,reason:pending.join(' ')};
const status=q.state==='absent'?'absent':q.state==='not_applicable'||q.eligible==='no'?'not_applicable':'calculated';
return{status,pending,reason:status==='absent'?'Ausência declarada com suporte;efeito zero neste período.':status==='not_applicable'?'Tratamento não aplicável/condição negativa documentada;percurso normal conservado.':'Condições e período documentados no recorte.'};
}
function fail(d:Decision,reason:string){d.status='indeterminate';d.pending.push(reason);d.reason=d.pending.join(' ');}
export function calculateSpecial(draft:Record<string,unknown>,b:NormalBook,m:NormalModel,source:Parameters<typeof calculateNormalDepreciation>[1]){
const i=b.special!;validateSpecial(i);const expenseDecision=qualify(i.expenses.qualification,b,m),coopDecision=qualify(i.cooperative.qualification,b,m),accDecision=qualify(i.acceleration.qualification,b,m);
const existing=(b.schedules?.rows??[]).filter(r=>r.rule==='undocumented');
if(expenseDecision.status==='absent'&&(existing.length||i.expenses.rows.length))fail(expenseDecision,'Ausência declarada contradiz despesas36g ou linhasL7 presentes;reconcilie sem apagar o ajustamento.');
if(expenseDecision.status!=='calculated'&&i.expenses.rows.length&&expenseDecision.status!=='indeterminate')fail(expenseDecision,'Estado global e parcelas da despesa contraditórios;qualifique as parcelas ou conserve apenas a declaração.');
const expenses=i.expenses.rows.map(r=>{
const origin=existing.find(x=>x.id===r.scheduleId),effect=m.schedules?.effects.find(x=>x.id===r.scheduleId);let amount:FiscalFraction|null=effect?.amount??null,tax:FiscalFraction|null=null,status:Status='indeterminate';const reasons:string[]=[];
if(expenseDecision.status!=='calculated')reasons.push(expenseDecision.reason);
if(!origin||!effect?.effect||!effect.amount||cmp(effect.effect,effect.amount)!==0n)reasons.push('Seleccione ajustamento36g calculado emL6;imposto autónomo não acrescenta novamente a despesa.');
if(i.expenses.rows.filter(x=>x.scheduleId===r.scheduleId).length!==1)reasons.push('Despesa L6 repetida no imposto autónomo;uma referência por parcela.');
if(!r.reference.trim()||!r.eligible||!r.nature)reasons.push('Falta classificação legal documentada da despesa;ausência de anexo não prova61(4).');
if(!reasons.length){status=r.eligible==='no'?'not_applicable':r.nature==='other'?'indeterminate':'calculated';if(status==='indeterminate')reasons.push('Outra natureza por corresponder;sem taxa provisória.');else tax=status==='calculated'?mul(amount!,35n,100n):z();}
return{...structuredClone(r),document:origin?.document??'',line:origin?.line??'',sourceCode:origin?.sourceCode??'',amount,l6Adjustment:effect?.effect??null,additionalProfitAdjustment:z(),rate:status==='calculated'?fraction(35n):null,tax,status,reason:reasons.join(' ')||(status==='calculated'?'CIRPC61(4):despesa ×35%;ajustamento36g reutilizado,sem duplicação.':'Condição negativa documentada;sem imposto autónomo aplicado à parcela.')};
});
if(expenseDecision.status==='calculated'){
if(!existing.length&&!expenses.length)fail(expenseDecision,'Não há parcelas conhecidas;declare ausência com suporte ou seleccione despesas36g.');
if(existing.some(r=>!expenses.some(x=>x.scheduleId===r.id)))fail(expenseDecision,'Há despesa36g sem qualificação autónoma;corresponda cada parcela ou justifique a sua não aplicação.');
for(const r of expenses)if(r.status==='indeterminate')fail(expenseDecision,(r.document||r.id)+': '+r.reason);
}
const knownAutonomous=addFractions(expenses.flatMap(r=>r.tax?[r.tax]:[])),autonomousTax=expenseDecision.status==='indeterminate'?null:expenseDecision.status==='calculated'?knownAutonomous:z();
if(coopDecision.status==='absent'&&['agrarian','craft','cultural'].includes(i.cooperative.kind)&&i.cooperative.allIncomeEligible==='yes'&&i.cooperative.withheldIncome==='absent')fail(coopDecision,'Ausência de redução contradiz os dados positivos de cooperativa integralmente elegível;reconcilie a declaração sem apagar os dados.');
if(coopDecision.status==='calculated'){
if(b.scope!=='resident_general'||!['agrarian','craft','cultural'].includes(i.cooperative.kind))fail(coopDecision,'Art12:tipo de cooperativa abrangida e residência por documentar;outras entidades têm correspondência própria.');
if(i.cooperative.allIncomeEligible!=='yes'||i.cooperative.withheldIncome!=='absent')fail(coopDecision,'Recorte exige base ordinária integralmente elegível e ausência documentada de rendimentos sujeitos a retenção;parcelas mistas/retidas exigem segregação própria,sem redução global presumida.');
}
if(accDecision.status==='absent'&&i.acceleration.rows.length)fail(accDecision,'Ausência de amortização acelerada contradiz activos seleccionados.');
if(accDecision.status!=='calculated'&&i.acceleration.rows.length&&accDecision.status!=='indeterminate')fail(accDecision,'Não aplicação global com activos seleccionados:reconcilie o estado e as parcelas.');
if(accDecision.status==='calculated'){
if(i.acceleration.cbfRequirements!=='yes'||!i.acceleration.recognition||!i.acceleration.recognitionReference.trim())fail(accDecision,'Falta enquadramentoCBF3/9 e reconhecimento10 quando exigido;excepção deve ter norma/suporte,não localização.');
if(i.acceleration.noOtherBenefits!=='yes')fail(accDecision,'CumulaçãoCBF4 por corresponder;este recorte não combina outros benefícios positivos.');
if(coopDecision.status==='calculated')fail(accDecision,'Combinação de redução cooperativa comCBF16 ainda não correspondida;cada caso independente continua demonstrável.');
if(!b.depreciation||!m.depreciation||!i.acceleration.rows.length)fail(accDecision,'Seleccione activos do mapaL6 já reconciliado;sem criar activo ou quota paralelos.');
}
const accelerations=i.acceleration.rows.map(r=>{
const a=b.depreciation?.assets.find(x=>x.id===r.assetId),base=m.depreciation?.assets.find(x=>x.id===r.assetId),issues:string[]=[];let status:Status='indeterminate';
if(accDecision.status!=='calculated')issues.push(accDecision.reason);
if(!r.reference.trim()||!r.eligible||!r.kind)issues.push('Condição/afectação do activo ao projecto por documentar.');
if(i.acceleration.rows.filter(x=>x.assetId===r.assetId).length!==1)issues.push('Activo repetido no benefício.');
if(!a||base?.status!=='calculated')issues.push('ActivoL6 inexistente ou cálculo de origem indeterminado.');
if(!issues.length&&r.eligible==='no')status='not_applicable';
else if(!issues.length&&a){
const building=r.kind==='new_project_building'&&base?.catalog?.kind==='building',equipment=['industrial_equipment','agroindustrial_equipment'].includes(r.kind)&&['machine','laundry_machine'].includes(a.category);
if(!building&&!equipment)issues.push('Categoria/afectação fora do recorte:imóvel novo ou máquina industrial/agro-industrial correspondida;reabilitados e outras categorias/métodos exigem regra própria.');
if(base?.catalog&&cmp(decimalFraction(a.rate),decimalFraction(base.catalog.rate))!==0n)issues.push('RecorteCBF16 usa a taxa normal máxima da tabela;opção por quota inferior exige correspondência própria,sem a acelerar por aproximação.');
if(a.method!=='constant'||a.timing!=='annual'||a.start!=='2026-01-01'||a.exitState!=='absent'||a.previousAccounting!=='0'||a.previousFiscal!=='0'||a.priorAvailable!=='0'||m.fiscalPeriod?.from!=='2026-01-01'||m.fiscalPeriod?.to!=='2026-12-31')issues.push('CBF16 inicial:primeiro exercício anual civil2026,método constante,sem quotas anteriores ou saída;duodécimos e recuperação anterior continuam separados.');
if(!issues.length)status='calculated';
}
return{...structuredClone(r),assetKey:a?.assetKey??'',description:a?.description??'',status,reason:issues.join(' ')||(status==='calculated'?'Activo existente elegível;taxa normal ×1,5,com limite contabilístico e saldo fiscal.':'Condição negativa documentada;quota normal mantida.')};
});
let acceleratedDepreciation:NormalDepreciationModel|null=null,additionalDeduction:FiscalFraction|null=accDecision.status==='indeterminate'?null:z();
if(accDecision.status==='calculated'){
for(const r of accelerations)if(r.status==='indeterminate')fail(accDecision,(r.description||r.id)+': '+r.reason);
if(accDecision.status==='calculated'&&b.depreciation&&m.depreciation?.adjustment){
acceleratedDepreciation=calculateNormalDepreciation(b.depreciation,source,{from:'2026-01-01',to:'2026-12-31'},[...b.rows,...(b.schedules?.rows??[]),...(b.costs?.rows??[])],false,new Set(accelerations.filter(r=>r.status==='calculated').map(r=>r.assetId)));
if(acceleratedDepreciation.adjustment===null)fail(accDecision,acceleratedDepreciation.pending.join(' '));
else additionalDeduction=subtractFractions(m.depreciation.adjustment,acceleratedDepreciation.adjustment);
}else if(accDecision.status==='calculated')fail(accDecision,'Reconciliação da amortização normal em falta.');
}
if(accDecision.status==='indeterminate')additionalDeduction=null;
const taxableProfit=m.taxableProfit&&additionalDeduction?subtractFractions(m.taxableProfit,additionalDeduction):null;
const base=b.base?calculateNormalBase(b.base,taxableProfit,b.year,b.schedules?.currentLossReference,!!m.periodsFx?.short):null;
const assessmentInput=coopDecision.status==='calculated'?{version:1 as const,treatment:'general_61_1' as const,reference:i.cooperative.qualification.reference}:b.assessment;
const assessment=assessmentInput?calculateNormalAssessment(assessmentInput,base?.taxableBase??null,{year:b.year,scope:b.scope,currency:m.currency,extended:!!b.schedules||!!b.periodsFx,short:!!m.periodsFx?.short,period:specialPeriod(b,m)}):null;
const normalColecta=assessment?.colecta??null,colecta=coopDecision.status==='indeterminate'?null:normalColecta?(coopDecision.status==='calculated'?mul(normalColecta,1n,2n):normalColecta):null;
const combinedColecta=colecta&&autonomousTax?addFractions([colecta,autonomousTax]):null;
return{input:structuredClone(i),context:structuredClone(specialContext(draft)),subjectNuit:b.base?.subjectNuit??'',period:specialPeriod(b,m),expenses:{...expenseDecision,rows:expenses,knownAutonomous,autonomousTax,source:specialSources.expenses},cooperative:{...coopDecision,rate:coopDecision.status==='calculated'?fraction(16n):null,source:specialSources.cooperative},acceleration:{...accDecision,rows:accelerations,additionalDeduction,normal:m.depreciation??null,accelerated:acceleratedDepreciation,source:specialSources.acceleration},reference:{taxableProfit:m.taxableProfit,taxableBase:m.taxableBase,colecta:m.colecta??null},taxableProfit,taxableBase:base?.taxableBase??null,colecta,combinedColecta,pending:[...expenseDecision.pending,...coopDecision.pending,...accDecision.pending,...(base?.pending??[]),...(assessment?.pending??[])],limitations:['L7 parcial:apenas61(4),cooperativa integralmente elegível12 eCBF16 no recorte documentado.','Localização/sector só contextualizam;não activam benefícios.','Parcelas mistas,cumulação,mais-valias e demais famílias têm correspondência própria.','M20/M22 permanecem preparaçãoL6 de referência;efeitosL7 sem campo oficial presumido.','Colecta combinada não é saldo final a pagar/recuperar;retenções,pagamentos e créditos anuais emL8.'],finalBalance:{scope:'L8',payable:null,recoverable:null}};
}
export type SpecialModel=ReturnType<typeof calculateSpecial>;
const money=(v:FiscalFraction|null)=>{if(!v)return'Indeterminado';const p=fractionPresentation(v);return(p.approximate?'≈':'')+p.decimal+' MZN (exacto '+v.numerator+'/'+v.denominator+')';};
const status=(s:Status)=>({calculated:'Calculado no recorte',absent:'Ausência declarada',not_applicable:'Não aplicável',indeterminate:'Informação em falta'})[s];
const contextDescription=(c:Record<string,unknown>)=>[['Sector','sector_id'],['Província','province_id'],['Distrito','district_id'],['Posto administrativo','admin_post_id'],['Localidade','locality_id']].map(([label,key])=>label+': '+String(c[key+'_label']||c[key]||'Por identificar')).join(';');
export function specialReportRows(m:SpecialModel):Array<[string,string,string,string]>{return[
['IRPC — L7','Qualificação comum',`NUIT:${m.subjectNuit};período:${m.period.from}–${m.period.to};${contextDescription(m.context)};${m.limitations.join(' ')}`,'Origem herdada conservada;fonte/condições/período por tratamento;sem concessão pela localização.'],
['IRPC — L7','Despesas — imposto autónomo',`${status(m.expenses.status)};parcela conhecida:${money(m.expenses.knownAutonomous)};imposto autónomo reconciliado:${money(m.expenses.autonomousTax)};${m.expenses.reason}`,`${m.input.expenses.qualification.reference};${m.input.expenses.qualification.periodReference};${m.expenses.source}`],
...m.expenses.rows.map(r=>['IRPC — L7',r.document+'/'+r.line,`${status(r.status)};natureza:${r.nature||'Por identificar'};despesa:${money(r.amount)};ajustamento36g já emL6:${money(r.l6Adjustment)};novo ajustamento ao lucro:${money(r.additionalProfitAdjustment)};taxa:${r.rate?'35%':'Não aplicada'};imposto autónomo:${money(r.tax)};${r.reason}`,r.reference]as[string,string,string,string]),
['IRPC — L7','Cooperativa — redução da taxa',`${status(m.cooperative.status)};tipo:${m.input.cooperative.kind||'Por identificar'};taxa:${m.cooperative.rate?'16%':'Não aplicada'};${m.cooperative.reason}`,`${m.input.cooperative.qualification.reference};${m.input.cooperative.qualification.periodReference};${m.cooperative.source}`],
['IRPC — L7','Amortizações aceleradas',`${status(m.acceleration.status)};dedução adicional conciliada:${money(m.acceleration.additionalDeduction)};${m.acceleration.reason}`,`${m.input.acceleration.qualification.reference};${m.input.acceleration.qualification.periodReference};${m.input.acceleration.recognitionReference};${m.acceleration.source}`],
...m.acceleration.rows.map(r=>{const a=m.acceleration.accelerated?.assets.find(a=>a.id===r.assetId),n=m.acceleration.normal?.assets.find(a=>a.id===r.assetId);return['IRPC — L7',r.description||r.assetId,`${status(r.status)};quota normal:${money(n?.quota??null)};quota acelerada:${money(a?.quota??null)};dedução normal:${money(n?.deduction??null)};dedução acelerada:${money(a?.deduction??null)};${r.reason}`,r.reference]as[string,string,string,string];}),
['IRPC — L7','Efeitos nos totais',`ReferênciaL6:lucro:${money(m.reference.taxableProfit)};base:${money(m.reference.taxableBase)};colecta:${money(m.reference.colecta)}. Após tratamentosL7:lucro:${money(m.taxableProfit)};base:${money(m.taxableBase)};colecta sobre a base:${money(m.colecta)};imposto autónomo:${money(m.expenses.autonomousTax)};colecta combinada:${money(m.combinedColecta)}. Saldo final a pagar/recuperar:Indeterminado (L8).`,m.pending.join(' | ')||'Regras/parcelas reconciliadas neste recorte;simulação,não liquidação final.'],
];}
@@ -178,6 +178,7 @@ const rowSection: Record<string, DossierReportSectionId> = {
ISPC: 'finance',
'IRPC — retenções': 'finance',
'IRPC normal — L6': 'finance',
'IRPC — L7': 'finance',
Equipa: 'team',
'Necessidade de negócio': 'resources',
Financiamento: 'resources',
+64
View File
@@ -0,0 +1,64 @@
<?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 IrpcSpecialDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_special_treatments_recover_with_original_plan_history_and_indeterminate_cases(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);
$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$id=null;$etag=null;
foreach(['expense','missing','negative','absent','cooperative','acceleration'] as $case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l7-common/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$l7=$m['special'];
$this->assertSame('IRPC-normal-adjustments/10',$m['version']);
$this->assertSame($b['special'],$l7['input']);
$this->assertContains('IRPC-normal-adjustments/9',array_column(array_column($b['history'],'model'),'version'));
$this->assertArrayHasKey('irpcWithholdings',$s);$this->assertArrayHasKey('books',$s);
$this->assertNull($m['tax']);$this->assertNull($l7['finalBalance']['payable']);$this->assertNull($l7['finalBalance']['recoverable']);
$this->assertSame('123456789',$l7['subjectNuit']);
$this->assertStringContainsString('61(4)',$l7['expenses']['source']);
if($case==='expense'){
$this->assertSame('35',$l7['expenses']['autonomousTax']['numerator']);
$this->assertSame('100',$l7['expenses']['rows'][0]['l6Adjustment']['numerator']);
$this->assertSame('0',$l7['expenses']['rows'][0]['additionalProfitAdjustment']['numerator']);
$this->assertSame($m['taxableProfit'],$l7['taxableProfit']);
$this->assertSame(['numerator'=>'5343','denominator'=>'5'],$l7['combinedColecta']);
}elseif($case==='missing'){
$this->assertSame('indeterminate',$l7['expenses']['status']);$this->assertNull($l7['expenses']['autonomousTax']);$this->assertNull($l7['combinedColecta']);
$this->assertSame('3230',$m['taxableProfit']['numerator']);
}elseif($case==='negative'){
$this->assertSame('not_applicable',$l7['expenses']['rows'][0]['status']);$this->assertSame('0',$l7['expenses']['autonomousTax']['numerator']);
}elseif($case==='absent'){
$this->assertSame('absent',$l7['expenses']['status']);$this->assertSame('0',$l7['expenses']['autonomousTax']['numerator']);
}elseif($case==='cooperative'){
$this->assertSame('16',$l7['cooperative']['rate']['numerator']);$this->assertSame(['numerator'=>'2504','denominator'=>'5'],$l7['colecta']);
$this->assertSame(['numerator'=>'5008','denominator'=>'5'],$m['colecta']);
}else{
$this->assertSame('20',$l7['acceleration']['additionalDeduction']['numerator']);
$this->assertSame('150',$l7['acceleration']['accelerated']['assets'][0]['quota']['numerator']);
$this->assertSame('120',$l7['acceleration']['accelerated']['assets'][0]['deduction']['numerator']);
$this->assertSame('6240',$l7['taxableProfit']['numerator']);$this->assertSame('6260',$m['taxableProfit']['numerator']);
$this->assertSame($b['depreciation'],$l7['acceleration']['accelerated']['input']);
}
file_put_contents(base_path('outputs/irpc-l7-common/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
File diff suppressed because one or more lines are too long
+32
View File
@@ -0,0 +1,32 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml} from '../../resources/js/lib/irpc-normal.ts';
import {newSpecialInput,newQualification,newSpecialExpense,newAcceleratedAsset} from '../../resources/js/lib/irpc-special.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-l6-values-preserved.json',import.meta.url),'utf8'));
const value=v=>v?Number(v.numerator)/Number(v.denominator):null;
const qualification=(state='absent',from='2026-07-01',to='2026-12-31')=>({...newQualification(),state,eligible:state==='known'?'yes':'',from,to,reference:'QA declaração/prova das condições legais no sujeito e parcela,sem inferência territorial',periodReference:'QA norma/exercício2026 e período documentado'});
function setup(){const d=seed(),b=normalBook(d);b.special=newSpecialInput();for(const part of[b.special.expenses,b.special.cooperative,b.special.acceleration])part.qualification=qualification();return{d,b};}
function expenses(){const{d,b}=setup();b.schedules.rows.push({id:'expense36g',rule:'undocumented',amount:'100',sourceCode:'operating_expenses',document:'QA-despesa',line:'1',reference:'QA despesa100 efectivamente incluída e não documentada36g',fiscalYear:'',conditions:'yes'});b.profit.topics.expenseCorrections={state:'known',amount:'100',reference:'QA acréscimo36g já conferido,sem outra correcção'};b.special.expenses.qualification=qualification('known');b.special.expenses.rows=[{...newSpecialExpense('autonomous1'),scheduleId:'expense36g',nature:'undocumented',eligible:'yes',reference:'QA qualificação legal61(4),não inferida de anexo em falta'}];return{d,b};}
function cooperative(){const{d,b}=setup();b.special.cooperative={qualification:qualification('known'),kind:'agrarian',allIncomeEligible:'yes',withheldIncome:'absent'};return{d,b};}
function acceleration(){const{d,b}=setup();delete b.periodsFx;delete b.forms;b.schedules.interest.amount='240';Object.assign(b.depreciation.assets[0],{category:'machine',description:'Máquina industrial existente QA',cost:'1000',rate:'10',accounting:'120',start:'2026-01-01',timing:'annual'});for(const part of[b.special.expenses,b.special.cooperative,b.special.acceleration])part.qualification=qualification('absent','2026-01-01','2026-12-31');b.special.acceleration={qualification:qualification('known','2026-01-01','2026-12-31'),cbfRequirements:'yes',recognition:'documented',recognitionReference:'QA instrumento do projecto/reconhecimento10,pressupostos3/9 e afectação industrial documentados',noOtherBenefits:'yes',rows:[{...newAcceleratedAsset('accelerated1'),assetId:b.depreciation.assets[0].id,kind:'industrial_equipment',eligible:'yes',reference:'QA factura/afectação da máquina ao projecto industrialCBF16'}]};return{d,b};}
test('no optionalL7 keeps exact accepted9 result and key; installed history is not recalculated on read',()=>{const d=seed(),b=normalBook(d),old=structuredClone(b.result);assert.equal(normalCurrent(d,b),true);assert.deepEqual(calculateNormal(d,b),old.model);assert.deepEqual(normalBook(d).result,old);});
test('61(4) reuses100 addback36g and computes35 autonomously without another profit adjustment',()=>{const{d,b}=expenses(),m=calculateNormal(d,b),s=m.special;assert.equal(m.version,'IRPC-normal-adjustments/10');assert.equal(value(m.taxableProfit),3230);assert.equal(value(m.colecta),1033.6);assert.equal(value(s.taxableProfit),3230);assert.equal(value(s.expenses.rows[0].l6Adjustment),100);assert.equal(value(s.expenses.rows[0].additionalProfitAdjustment),0);assert.equal(value(s.expenses.autonomousTax),35);assert.equal(value(s.combinedColecta),1068.6);assert.equal(s.finalBalance.payable,null);});
test('missing, absent and notapplicable differ; attachment presence alone never classifies an expense',()=>{const{d,b}=setup();b.special.expenses.qualification=qualification('');let s=calculateNormal(d,b).special;assert.equal(s.expenses.status,'indeterminate');assert.equal(s.expenses.autonomousTax,null);b.special.expenses.qualification=qualification('absent');s=calculateNormal(d,b).special;assert.equal(s.expenses.status,'absent');assert.equal(value(s.expenses.autonomousTax),0);b.special.expenses.qualification=qualification('not_applicable');assert.equal(calculateNormal(d,b).special.expenses.status,'not_applicable');const c=expenses();c.b.special.expenses.rows[0].nature='';assert.equal(calculateNormal(c.d,c.b).special.expenses.autonomousTax,null);});
test('false condition keeps existing addback and no autonomous tax; contradictory absence cannot erase expense',()=>{const{d,b}=expenses();b.special.expenses.rows[0].eligible='no';let m=calculateNormal(d,b);assert.equal(m.special.expenses.rows[0].status,'not_applicable');assert.equal(value(m.special.expenses.autonomousTax),0);assert.equal(value(m.taxableProfit),3230);b.special.expenses.rows=[];b.special.expenses.qualification=qualification('absent');m=calculateNormal(d,b);assert.equal(m.special.expenses.status,'indeterminate');assert.equal(value(m.taxableProfit),3230);});
test('duplicate, unlinked or uncalculated36g cannot doublecharge or manufacture tax',()=>{for(const mode of['duplicate','unlinked','uncalculated','missingcoverage']){const{d,b}=expenses();if(mode==='duplicate')b.special.expenses.rows.push({...b.special.expenses.rows[0],id:'duplicate'});if(mode==='unlinked')b.special.expenses.rows[0].scheduleId='not-found';if(mode==='uncalculated')b.schedules.rows[0].conditions='';if(mode==='missingcoverage')b.special.expenses.rows=[];assert.equal(calculateNormal(d,b).special.expenses.autonomousTax,null,mode);}});
test('common qualification rejects wrong period/year or missing identity without blocking independent L6 totals',()=>{for(const mutate of[b=>b.special.expenses.qualification.to='2025-12-31',b=>b.special.expenses.qualification.periodReference='',b=>b.base.subjectNuit='']){const{d,b}=expenses();mutate(b);const m=calculateNormal(d,b);assert.equal(m.special.expenses.autonomousTax,null);if(b.base.subjectNuit)assert.equal(value(m.colecta),1033.6);}});
test('unknown benefit does not suppress separately calculable autonomous expense or normal reference',()=>{const{d,b}=expenses();b.special.acceleration.qualification=qualification('');const m=calculateNormal(d,b);assert.equal(value(m.special.expenses.autonomousTax),35);assert.equal(value(m.special.reference.colecta),1033.6);assert.equal(m.special.combinedColecta,null);});
test('eligible cooperative uses16percent without changing reference32percent or autonomous expense rate',()=>{const{d,b}=cooperative(),m=calculateNormal(d,b);assert.equal(value(m.colecta),1001.6);assert.equal(m.special.cooperative.status,'calculated');assert.equal(value(m.special.cooperative.rate),16);assert.equal(value(m.special.colecta),500.8);assert.equal(value(m.special.taxableBase),3130);assert.equal(value(m.special.combinedColecta),500.8);const x=expenses();x.b.special.cooperative=structuredClone(b.special.cooperative);assert.equal(value(calculateNormal(x.d,x.b).special.combinedColecta),551.8);});
test('mixed, withheld, unclassified or false cooperative conditions never grant blanket reduction',()=>{for(const mutate of[c=>c.allIncomeEligible='no',c=>c.withheldIncome='known',c=>c.kind='other']){const{d,b}=cooperative();mutate(b.special.cooperative);const m=calculateNormal(d,b);assert.equal(m.special.cooperative.status,'indeterminate');assert.equal(m.special.colecta,null);assert.equal(value(m.colecta),1001.6);}const{d,b}=cooperative();b.special.cooperative.qualification.eligible='no';assert.equal(value(calculateNormal(d,b).special.colecta),1001.6);});
test('cooperative treatment does not require mislabelling special rate as L6 general; contradictory absence remains pending',()=>{const{d,b}=cooperative();b.assessment.treatment='other';let m=calculateNormal(d,b);assert.equal(m.colecta,null);assert.equal(value(m.special.colecta),500.8);b.special.cooperative.qualification=qualification('absent');m=calculateNormal(d,b);assert.equal(m.special.cooperative.status,'indeterminate');assert.equal(m.special.colecta,null);});
test('common period follows declared shortperiod even without schedules; calendar dates cannot replace it',()=>{const{d,b}=cooperative();delete b.schedules;b.special.expenses.qualification=qualification('absent','2026-01-01','2026-12-31');const m=calculateNormal(d,b);assert.equal(m.special.period.from,'2026-07-01');assert.equal(m.special.expenses.status,'indeterminate');});
test('CBF16 reuses asset and rate10to15 but respects120 accounted cap; additional deduction20 not50',()=>{const{d,b}=acceleration(),input=JSON.stringify(b.depreciation),m=calculateNormal(d,b),s=m.special;assert.equal(s.acceleration.status,'calculated',s.acceleration.reason);assert.equal(value(m.depreciation.assets[0].quota),100);assert.equal(value(s.acceleration.accelerated.assets[0].quota),150);assert.equal(value(s.acceleration.accelerated.assets[0].deduction),120);assert.equal(value(s.acceleration.additionalDeduction),20);assert.equal(value(m.taxableProfit),6260);assert.equal(value(s.taxableProfit),6240);assert.equal(value(s.taxableBase),6240);assert.equal(value(s.colecta),1996.8);assert.equal(JSON.stringify(b.depreciation),input);});
test('new project building reuses land separation and legal table rate before bounded acceleration',()=>{const{d,b}=acceleration();Object.assign(b.depreciation.assets[0],{category:'commercial_building',cost:'5000',land:'0',rate:'2'});b.special.acceleration.rows[0].kind='new_project_building';const m=calculateNormal(d,b);assert.equal(m.special.acceleration.status,'calculated');assert.equal(value(m.special.acceleration.accelerated.assets[0].quota),150);assert.equal(value(m.special.acceleration.additionalDeduction),20);});
test('lower optional normal quota remains valid L6 but is not treated as legally fixed rate in CBF16',()=>{const{d,b}=acceleration();b.depreciation.assets[0].rate='5';const m=calculateNormal(d,b);assert.equal(m.depreciation.assets[0].status,'calculated');assert.equal(m.special.acceleration.additionalDeduction,null);assert.match(m.special.acceleration.reason,/quota inferior/);});
test('location alone, wrong activity, prior quotas, unsupported periods and benefit combinations cannot accelerate',()=>{for(const mutate of[b=>b.special.acceleration.cbfRequirements='',b=>b.special.acceleration.recognitionReference='',b=>b.special.acceleration.rows[0].kind='other',b=>b.depreciation.assets[0].timing='months',b=>b.depreciation.assets[0].previousAccounting='1',b=>b.special.acceleration.noOtherBenefits='no',b=>{b.special.cooperative={qualification:qualification('known','2026-01-01','2026-12-31'),kind:'agrarian',allIncomeEligible:'yes',withheldIncome:'absent'};}]){const{d,b}=acceleration();d.province_id='ZEE-candidate';mutate(b);const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.taxableProfit,null);assert.equal(m.special.acceleration.status,'indeterminate');}});
test('save/recover/report preserves9history and L5/books; repeated calculation never accumulates tax or asset deduction',()=>{const{d,b}=expenses(),old=structuredClone(b.result),plan=d.business_plan_snapshot;d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T14:00:00+02:00'));let back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history.at(-1),old);assert.equal(d.business_plan_snapshot,plan);assert.ok(JSON.parse(d.modelo_a_snapshot).irpcWithholdings);const html=normalHtml(d);assert.match(html,/imposto autónomo:35/);assert.match(html,/novo ajustamento ao lucro:0/);assert.match(html,/Saldo final a pagar\/recuperar:Indeterminado/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/Despesas — imposto autónomo/);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,back));back=normalBook(d);assert.equal(value(back.result.model.special.expenses.autonomousTax),35);assert.equal(value(back.result.model.taxableProfit),3230);d.province_id='changed';assert.equal(normalCurrent(d,normalBook(d)),false);assert.throws(()=>normalHtml(d),/Calcule/);});
test('new evidence changes key; malformedqualification or duplicate row ID cannot overwrite book',()=>{const{d,b}=expenses();d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));const back=normalBook(d);back.special.expenses.rows[0].reference+=' alterado';assert.equal(normalCurrent(d,back),false);back.special.expenses.rows.push(structuredClone(back.special.expenses.rows[0]));assert.throws(()=>saveNormalBook(d,back),/identificador repetido/);});
test('report presents context labels and retains provenance internally without dumping raw context snapshots',()=>{const{d,b}=setup();d.province_id_label='Província QA';d.context_opportunity_snapshot='{"internal":"QA implementation blob"}';d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));assert.match(normalHtml(d),/Província: Província QA/);assert.doesNotMatch(normalHtml(d),/QA implementation blob/);assert.equal(normalBook(d).result.model.special.context.context_opportunity_snapshot,d.context_opportunity_snapshot);});
test('write only new QA cases for transactional API and recovered reports',()=>{const missing=expenses(),negative=expenses();missing.b.special.expenses.rows[0].nature='';negative.b.special.expenses.rows[0].eligible='no';const cases={expense:expenses(),missing,negative,absent:setup(),cooperative:cooperative(),acceleration:acceleration()};fs.mkdirSync('outputs/irpc-l7-common',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T14:00:00+02:00'));fs.writeFileSync('outputs/irpc-l7-common/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l7-common/'+name+'.html',normalHtml(d));}});
+21
View File
@@ -0,0 +1,21 @@
<?php
$root=dirname(__DIR__);
$operation=$argv[1]??'';
if(!in_array($operation,['routes','test'],true)||basename($root)!=='portal-investidor-irpc-l7-common-qa-20261026')throw new RuntimeException('Explicit L7 QA workspace and routes/test operation required.');
foreach(['storage/framework/views','storage/framework/cache','storage/framework/sessions','bootstrap/cache','storage/logs','outputs/irpc-l7-common']as$dir)if(!is_dir($root.'/'.$dir))mkdir($root.'/'.$dir,0700,true);
foreach(['APP_BASE_PATH'=>$root,'APP_ENV'=>'testing','APP_KEY'=>'base64:'.base64_encode(str_repeat('Q',32)),'DB_CONNECTION'=>'pgsql','DB_HOST'=>'127.0.0.1','DB_PORT'=>'55441','DB_DATABASE'=>'simulator_fiscal_delta_20261021_test','DB_USERNAME'=>'postgres','DB_PASSWORD'=>'','DB_URL'=>'','CACHE_STORE'=>'array','SESSION_DRIVER'=>'array','QUEUE_CONNECTION'=>'sync','MAIL_MAILER'=>'array'] as $k=>$v){putenv($k.'='.$v);$_ENV[$k]=$_SERVER[$k]=$v;}
$loader=require $root.'/vendor/autoload.php';
spl_autoload_register(function($class)use($root){foreach(['App\\'=>'app/','Tests\\'=>'tests/','Database\\Factories\\'=>'database/factories/']as$prefix=>$dir){if(str_starts_with($class,$prefix)){$file=$root.'/'.$dir.str_replace('\\','/',substr($class,strlen($prefix))).'.php';if(is_file($file)){require $file;return;}}}},true,true);
$app=require $root.'/bootstrap/app.php';
(new Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables)->bootstrap($app);
(new Illuminate\Foundation\Bootstrap\LoadConfiguration)->bootstrap($app);
$c=config('database.connections.pgsql');
if($app->configurationIsCached()||$c['host']!=='127.0.0.1'||(int)$c['port']!==55441||$c['database']!=='simulator_fiscal_delta_20261021_test'||!empty($c['url']))throw new RuntimeException('QA guard');
$pdo=new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$c['database'],$c['username'],$c['password']);
if($pdo->query('select current_database()')->fetchColumn()!==$c['database'])throw new RuntimeException('QA identity');
if($operation==='routes'){
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$status=Illuminate\Support\Facades\Artisan::call('wayfinder:generate',['--with-form'=>true]);
echo Illuminate\Support\Facades\Artisan::output();exit($status);
}
$argv=['phpunit','-c','phpunit.investmoz.xml','tests/Feature/IrpcSpecialDraftTest.php'];$argc=count($argv);$_SERVER['argv']=$argv;$_SERVER['argc']=$argc;require $root.'/vendor/phpunit/phpunit/phpunit';