This commit is contained in:
@@ -1,5 +1,32 @@
|
|||||||
# Rastreabilidade, utilização e formação
|
# Rastreabilidade, utilização e formação
|
||||||
|
|
||||||
|
## 10/11/2026 — crédito económico: partilha e associações em QA
|
||||||
|
|
||||||
|
Homologação mantém `c359364d`. Avanço posterior em QA, sem instalação: art64(3) distingue a parcela elegível do rendimento da partilha e o excesso com natureza de mais-valia; art64(4) distingue associação em participação e à quota, exigindo tributação efectiva documentada na participação. Reutiliza cálculo60%, prova do imposto subjacente, limites, prioridades, isolamento e writer privado existentes. Não calcula a mais-valia como crédito nem pressupõe inclusão L6 ou reconferência da colecta. Campos antigos e históricos são preservados; fontes incompletas ficam recuperáveis e indeterminadas.
|
||||||
|
|
||||||
|
Critério demonstrado: calcular → gravar/GET pela API existente no QA55441 com rollback → recuperar exactamente → HTML e relatório do dossier. Partilha sintética: valor atribuído200, entradas100 e rendimento documentado150 → elegível100/excesso50 → quota35,84/crédito12/saldo688 MZN. Associação documentada100/imposto20 → crédito12/saldo688 MZN, sem alterar L6. Provas `outputs/irpc-economic-liquidation-20261110/verification.json` e `outputs/irpc-economic-association-20261110/verification.json`. Não são actos fiscais nem consumo de saldos oficiais.
|
||||||
|
|
||||||
|
Conjunto de fontes compatíveis com quota e inclusão ordinária agrupado em `outputs/entrega-economica-especial-20261110.zip`, base c359364d, aplicação/reversão conferidas em índices temporários. Pacotes anteriores intactos. Inclusão/correcção especial em L6 também demonstrada em QA: reconcilia rendimento total da partilha (incluindo mais-valia) uma só vez, enquanto a quota usa apenas a componente elegível; associações reutilizam a qualificação própria. Prova outputs/irpc-economic-special-inclusion-20261110/verification.json: efeito162, base4562/colecta1459,84, quota35,84/saldo739,84 recuperados na API e relatórios. Casos transitórios, fracções de cêntimo, CDT, benefícios/obrigações e restantes resíduos L1–L7/L9–L16 continuam no saldo. Conferência visual pendente. Próximo trabalho funcional: correspondência CDT automática nos casos suportados pelas fontes existentes; eventual instalação deste conjunto é acção separada. Tipos/lint/Pint/build QA conferidos no conjunto final.
|
||||||
|
|
||||||
|
|
||||||
|
## 10/11/2026 — inclusão/correcção económica L6 em QA
|
||||||
|
|
||||||
|
Instalado: `c359364d`. Quota económica e inclusão/correcção L6 ordinária demonstradas em QA, sem instalação. Pacote anterior `outputs/entrega-economica-l6-l8-20261110.zip` preservado; novo delta posterior agrupado separadamente. Conferência visual pendente no registo existente.
|
||||||
|
|
||||||
|
Escolha explícita da origem L8, reconciliação de lucros/crédito já considerados, prévia da diferença e confirmação precedem o cálculo/gravação existentes. Repetição substitui a parcela; não acumula. Prova: +112 MZN leva lucro tributável a6812, base4512 e colecta1443,84. Montantes já incluídos100/12 dão efeito0; excesso150/20 corrige−58. Origem L8 anterior fica desactualizada; reconferida a nova colecta, quota35,84 e saldo723,84 mantêm-se na API/recuperação/HTML/dossier, com históricos preservados. Provas `outputs/irpc-economic-inclusion-20261110/verification.json` e `outputs/irpc-economic-quota-20261110/verification.json`. Tipos/lint/Pint/build QA aprovados.
|
||||||
|
|
||||||
|
Âmbito ordinário residente geral2026/MZN. Fracções de cêntimo exigem tratamento próprio, sem arredondamento presumido. Casos especiais, CDT e obrigações continuam no saldo. Resíduos L1–L7 e L9–L16 preservados; L6/L8 não integrais. Registos anteriores abaixo são históricos.
|
||||||
|
|
||||||
|
## 10/11/2026 — conjunto Educação/L8 instalado e residual económico avançado
|
||||||
|
|
||||||
|
**Instalado e conferido em c359364de3f65599f2248540a5b040604e8490cc**, por fast-forward normal em main a partir de ff726e57. Árvore058910eb26f566c0486a20c1e7fc12f06bbe06a5 igual ao pacote QA. Executor existente terminou às10:54:50UTC; revisão HTTPS/portal/login/upHTTP200 e14hashes de fontes conferidos às10:55:21UTC. Zero migrations/importações; configuração idêntica; release anterior preservada. Backup fresco em `/var/backups/investmoz-education-l8-20261110/backup`,190tabelas efectivamente restauradas/checksums iguais num contentor sem rede,369ficheiros/configuração restaurados e conferidos em pasta privada. Credencial não guardada; sessão SSH encerrada após conferência. Recibos `outputs/education-l8-delivery-20261110/{backup,restore,files-restore,verification}.json`. Pacotes selados anteriores preservados. Controlos do executor obrigatórios,sem nova ronda manual de provas aceites.
|
||||||
|
|
||||||
|
Ficam utilizáveis no destino os dois recortes: revisão educativa conserva preparação/envio incerto ao consultar histórico; crédito internacional oferece derivação ordinária da fracção nacional. Instalação destes recortes deixa de ser pendência;conferência visual continua separada. Outros critérios dos módulos não são encerrados por esta instalação.
|
||||||
|
|
||||||
|
**Próximo residual executável avançado após a instalação — QA,sem instalação nova:**quota proporcional do crédito económico ordinário,CIRPC64(1–2)após acréscimo53. Fonte aceite Lei34/2007,PDF18/p768-(72),reutilizada;60% do imposto correspondente e qualificação/exclusão40 já validados pelo mecanismo existente. Novo modo calcula `colecta L6 × (lucros incluídos + crédito nominal já acrescido) / matéria colectável L6`,com versão/origem/referência no resultado e relatório. Não altera a matéria colectável,não duplica o acréscimo,não calcula automaticamente a sua inclusão. Modo documentado antigo por omissão;limite anterior conservado,não utilizado no modo derivado. Fonte/base desactualizada,acréscimo incompleto,correspondência ausente e soma de componentes acima da base impedem o saldo dependente. Casos64(3/4),L7 e outras situações conservam o saldo próprio.
|
||||||
|
|
||||||
|
Quatro casos novos frontend,cálculo/recuperação/HTML/dossier;API privada existente,num novo caso/9asserções,DatabaseTransactionsrollback na mesma QAproactive_investment_20261027_test:55441,actor1021existente. Novo exemplo sintético:lucros100+acréscimo60,base4400/colecta1408→quota256/5=51,2MZN;saldo3244/5=648,8MZN.Sem saldo fiscal consumido. Prova `outputs/irpc-economic-quota-20261110/verification.json`. Tipos/lint ePint aprovados;buildQA em log próprio. Nenhuma prova anterior de Educação/fracção nacional reaberta. Estado é QA,ainda não instalado nem visualmente aceite. L8 permanece parcial;resíduosL1–L7 eL9–L16 mantidos. Próximo:inclusão/correcção económica na base com a correspondência própria e restantes ramos já identificados,sem repetir os recortes fechados.
|
||||||
|
|
||||||
## 10/11/2026 — L8: fracção nacional ordinária demonstrada até ao relatório em QA
|
## 10/11/2026 — L8: fracção nacional ordinária demonstrada até ao relatório em QA
|
||||||
|
|
||||||
Pendência funcional encerrada **em QA no recorte ordinário**: opção explícita de derivar a fracção nacional do crédito internacional da colecta L6 corrente e da parcela líquida estrangeira correspondida à matéria colectável. Fonte aceite reutilizada: CIRPC65(1)(a/b),65(2),Lei34/2007,PDF18/p768-(72),e ordem66/RIRPC21. Não se exige nova validação da fonte oficial. A fórmula técnica `colecta antes de créditos × parcela líquida / matéria colectável` fica limitada à colecta normal corrente, base positiva, inclusão documentada, mesmo titular/exercício/país e afectação líquida identificada. O portal não inventa nem reparte custos/perdas/deduções; valores acima da base ou parcelas por país que em conjunto excedam a base impedem o saldo dependente.
|
Pendência funcional encerrada **em QA no recorte ordinário**: opção explícita de derivar a fracção nacional do crédito internacional da colecta L6 corrente e da parcela líquida estrangeira correspondida à matéria colectável. Fonte aceite reutilizada: CIRPC65(1)(a/b),65(2),Lei34/2007,PDF18/p768-(72),e ordem66/RIRPC21. Não se exige nova validação da fonte oficial. A fórmula técnica `colecta antes de créditos × parcela líquida / matéria colectável` fica limitada à colecta normal corrente, base positiva, inclusão documentada, mesmo titular/exercício/país e afectação líquida identificada. O portal não inventa nem reparte custos/perdas/deduções; valores acima da base ou parcelas por país que em conjunto excedam a base impedem o saldo dependente.
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
|
## 10/11/2026 — crédito económico: partilha e associações em QA
|
||||||
|
|
||||||
|
Homologação mantém `c359364d`. Avanço posterior em QA, sem instalação: art64(3) distingue a parcela elegível do rendimento da partilha e o excesso com natureza de mais-valia; art64(4) distingue associação em participação e à quota, exigindo tributação efectiva documentada na participação. Reutiliza cálculo60%, prova do imposto subjacente, limites, prioridades, isolamento e writer privado existentes. Não calcula a mais-valia como crédito nem pressupõe inclusão L6 ou reconferência da colecta. Campos antigos e históricos são preservados; fontes incompletas ficam recuperáveis e indeterminadas.
|
||||||
|
|
||||||
|
Critério demonstrado: calcular → gravar/GET pela API existente no QA55441 com rollback → recuperar exactamente → HTML e relatório do dossier. Partilha sintética: valor atribuído200, entradas100 e rendimento documentado150 → elegível100/excesso50 → quota35,84/crédito12/saldo688 MZN. Associação documentada100/imposto20 → crédito12/saldo688 MZN, sem alterar L6. Provas `outputs/irpc-economic-liquidation-20261110/verification.json` e `outputs/irpc-economic-association-20261110/verification.json`. Não são actos fiscais nem consumo de saldos oficiais.
|
||||||
|
|
||||||
|
Conjunto de fontes compatíveis com quota e inclusão ordinária agrupado em `outputs/entrega-economica-especial-20261110.zip`, base c359364d, aplicação/reversão conferidas em índices temporários. Pacotes anteriores intactos. Inclusão/correcção especial em L6 também demonstrada em QA: reconcilia rendimento total da partilha (incluindo mais-valia) uma só vez, enquanto a quota usa apenas a componente elegível; associações reutilizam a qualificação própria. Prova outputs/irpc-economic-special-inclusion-20261110/verification.json: efeito162, base4562/colecta1459,84, quota35,84/saldo739,84 recuperados na API e relatórios. Casos transitórios, fracções de cêntimo, CDT, benefícios/obrigações e restantes resíduos L1–L7/L9–L16 continuam no saldo. Conferência visual pendente. Próximo trabalho funcional: correspondência CDT automática nos casos suportados pelas fontes existentes; eventual instalação deste conjunto é acção separada. Tipos/lint/Pint/build QA conferidos no conjunto final.
|
||||||
|
|
||||||
|
## 10/11/2026 — inclusão/correcção económica L6 em QA
|
||||||
|
|
||||||
|
Instalado: `c359364d`. Quota económica e inclusão/correcção L6 ordinária demonstradas em QA, sem instalação. Pacote anterior `outputs/entrega-economica-l6-l8-20261110.zip` preservado; novo delta posterior agrupado separadamente. Conferência visual pendente no registo existente.
|
||||||
|
|
||||||
|
Escolha explícita da origem L8, reconciliação de lucros/crédito já considerados, prévia da diferença e confirmação precedem o cálculo/gravação existentes. Repetição substitui a parcela; não acumula. Prova: +112 MZN leva lucro tributável a6812, base4512 e colecta1443,84. Montantes já incluídos100/12 dão efeito0; excesso150/20 corrige−58. Origem L8 anterior fica desactualizada; reconferida a nova colecta, quota35,84 e saldo723,84 mantêm-se na API/recuperação/HTML/dossier, com históricos preservados. Provas `outputs/irpc-economic-inclusion-20261110/verification.json` e `outputs/irpc-economic-quota-20261110/verification.json`. Tipos/lint/Pint/build QA aprovados.
|
||||||
|
|
||||||
|
Âmbito ordinário residente geral2026/MZN. Fracções de cêntimo exigem tratamento próprio, sem arredondamento presumido. Casos especiais, CDT e obrigações continuam no saldo. Resíduos L1–L7 e L9–L16 preservados; L6/L8 não integrais. Registos anteriores abaixo são históricos.
|
||||||
|
|
||||||
## Continuação integral — saldo e execução em 10/11/2026
|
## Continuação integral — saldo e execução em 10/11/2026
|
||||||
|
|
||||||
|
Estado actualizado: conjunto Educação/fracção nacional ordinária instalado e HTTPS verificado em `c359364d`,com backup/restauro190tabelas/369ficheiros,delta14hashes,.env inalterado e zero migrations/importações. Recibos `outputs/education-l8-delivery-20261110/`. As anotações de QA/instalação pendente abaixo ficam históricas neste recorte. Conferência visual permanece separada.
|
||||||
|
|
||||||
|
Após essa instalação,avançado o residual de quota económica ordinária após acréscimo53:novo modo deriva de L6 com origem/versionamento e preservação do limite manual,salva/recupera pela API existente e apresenta HTML/dossier. Quatro casos novos+um API revertido/9asserções;tipos/lint/Pint aprovados. Prova `outputs/irpc-economic-quota-20261110/verification.json`. Este novo delta é QA,ainda não instalado. Inclusão/correcção automática do acréscimo na base e casos especiais não incluídos;L8parcial,resíduosL1–L7 eL9–L16 continuam explícitos. Não reabrir a fracção nacional internacional ou Educação para executar esse saldo.
|
||||||
|
|
||||||
Actualização L8 desta passagem: recorte ordinário de derivação da fracção nacional implementado e demonstrado em QA até gravação/recuperação/relatório pelo writer privado existente. Base4400/colecta1408/parcela100 → fracção32 → saldo simulado676,com limites/provas próprios; caso exacto2/625 e correspondência em falta demonstrados. Nenhuma determinação automática de CDT,repartição de custos/perdas ou consumo oficial. Modo documentado e históricos preservados. Delta agrupável com Educação,ainda sem instalação. Fonte e percurso no início do manual; provas `outputs/irpc-national-fraction-20261110/recovered-reports.json`.
|
Actualização L8 desta passagem: recorte ordinário de derivação da fracção nacional implementado e demonstrado em QA até gravação/recuperação/relatório pelo writer privado existente. Base4400/colecta1408/parcela100 → fracção32 → saldo simulado676,com limites/provas próprios; caso exacto2/625 e correspondência em falta demonstrados. Nenhuma determinação automática de CDT,repartição de custos/perdas ou consumo oficial. Modo documentado e históricos preservados. Delta agrupável com Educação,ainda sem instalação. Fonte e percurso no início do manual; provas `outputs/irpc-national-fraction-20261110/recovered-reports.json`.
|
||||||
|
|
||||||
Correcção do saldo global: preservar explicitamente resíduos L1–L7 e L9–L16 (INSS,IRPS mensal/anual,Selo,direitos,ICE,SISA,IPRA),conforme tabela actualizada na visão existente. Transporte/crédito económico entregues em569a1cd7 não são trabalho ausente. Nenhum lote agregado é fechado por número de testes;fecho comunicado nesta passagem é apenas a fracção nacional ordinária em QA.
|
Correcção do saldo global: preservar explicitamente resíduos L1–L7 e L9–L16 (INSS,IRPS mensal/anual,Selo,direitos,ICE,SISA,IPRA),conforme tabela actualizada na visão existente. Transporte/crédito económico entregues em569a1cd7 não são trabalho ausente. Nenhum lote agregado é fechado por número de testes;fecho comunicado nesta passagem é apenas a fracção nacional ordinária em QA.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,31 @@
|
|||||||
|
## 10/11/2026 — crédito económico: partilha e associações em QA
|
||||||
|
|
||||||
|
Homologação mantém `c359364d`. Avanço posterior em QA, sem instalação: art64(3) distingue a parcela elegível do rendimento da partilha e o excesso com natureza de mais-valia; art64(4) distingue associação em participação e à quota, exigindo tributação efectiva documentada na participação. Reutiliza cálculo60%, prova do imposto subjacente, limites, prioridades, isolamento e writer privado existentes. Não calcula a mais-valia como crédito nem pressupõe inclusão L6 ou reconferência da colecta. Campos antigos e históricos são preservados; fontes incompletas ficam recuperáveis e indeterminadas.
|
||||||
|
|
||||||
|
Critério demonstrado: calcular → gravar/GET pela API existente no QA55441 com rollback → recuperar exactamente → HTML e relatório do dossier. Partilha sintética: valor atribuído200, entradas100 e rendimento documentado150 → elegível100/excesso50 → quota35,84/crédito12/saldo688 MZN. Associação documentada100/imposto20 → crédito12/saldo688 MZN, sem alterar L6. Provas `outputs/irpc-economic-liquidation-20261110/verification.json` e `outputs/irpc-economic-association-20261110/verification.json`. Não são actos fiscais nem consumo de saldos oficiais.
|
||||||
|
|
||||||
|
Conjunto de fontes compatíveis com quota e inclusão ordinária agrupado em `outputs/entrega-economica-especial-20261110.zip`, base c359364d, aplicação/reversão conferidas em índices temporários. Pacotes anteriores intactos. Inclusão/correcção especial em L6 também demonstrada em QA: reconcilia rendimento total da partilha (incluindo mais-valia) uma só vez, enquanto a quota usa apenas a componente elegível; associações reutilizam a qualificação própria. Prova outputs/irpc-economic-special-inclusion-20261110/verification.json: efeito162, base4562/colecta1459,84, quota35,84/saldo739,84 recuperados na API e relatórios. Casos transitórios, fracções de cêntimo, CDT, benefícios/obrigações e restantes resíduos L1–L7/L9–L16 continuam no saldo. Conferência visual pendente. Próximo trabalho funcional: correspondência CDT automática nos casos suportados pelas fontes existentes; eventual instalação deste conjunto é acção separada. Tipos/lint/Pint/build QA conferidos no conjunto final.
|
||||||
|
|
||||||
|
## 10/11/2026 — inclusão/correcção económica L6 em QA
|
||||||
|
|
||||||
|
Instalado: `c359364d`. Quota económica e inclusão/correcção L6 ordinária demonstradas em QA, sem instalação. Pacote anterior `outputs/entrega-economica-l6-l8-20261110.zip` preservado; novo delta posterior agrupado separadamente. Conferência visual pendente no registo existente.
|
||||||
|
|
||||||
|
Escolha explícita da origem L8, reconciliação de lucros/crédito já considerados, prévia da diferença e confirmação precedem o cálculo/gravação existentes. Repetição substitui a parcela; não acumula. Prova: +112 MZN leva lucro tributável a6812, base4512 e colecta1443,84. Montantes já incluídos100/12 dão efeito0; excesso150/20 corrige−58. Origem L8 anterior fica desactualizada; reconferida a nova colecta, quota35,84 e saldo723,84 mantêm-se na API/recuperação/HTML/dossier, com históricos preservados. Provas `outputs/irpc-economic-inclusion-20261110/verification.json` e `outputs/irpc-economic-quota-20261110/verification.json`. Tipos/lint/Pint/build QA aprovados.
|
||||||
|
|
||||||
|
Âmbito ordinário residente geral2026/MZN. Fracções de cêntimo exigem tratamento próprio, sem arredondamento presumido. Casos especiais, CDT e obrigações continuam no saldo. Resíduos L1–L7 e L9–L16 preservados; L6/L8 não integrais. Registos anteriores abaixo são históricos.
|
||||||
|
|
||||||
## Saldo operacional reconciliado — 10/11/2026, após `ff726e57`
|
## Saldo operacional reconciliado — 10/11/2026, após `ff726e57`
|
||||||
|
|
||||||
|
**Estado mais recente:** conjunto Educação + fracção nacional ordinária instalado na homologação em `c359364de3f65599f2248540a5b040604e8490cc`, confirmado por HTTPS às 10:55:21 UTC (12:55 locais). Backup/restauro190tabelas e369ficheiros/configuração conferidos,14fontes do delta idênticas à revisão Git,zero migrations/importações,.env preservado e release ff726e57 mantida para reversão. Recibo `outputs/education-l8-delivery-20261110/verification.json`. As notas anteriores de conjunto «só QA» descrevem o estado anterior à instalação e não são nova pendência.
|
||||||
|
|
||||||
|
| Recorte | Estado actual |
|
||||||
|
| --- | --- |
|
||||||
|
| Educação — continuidade ao paginar histórico | Instalado c359364d; restantes jornadas/retoma durável e conferência visual conservadas. |
|
||||||
|
| L8 — fracção nacional ordinária do crédito internacional | Instalado c359364d; não reabrir cálculo/gravação/recuperação/relatórios aceites. |
|
||||||
|
| L8 — quota proporcional económica ordinária após acréscimo53 | Novo delta QA, ainda não instalado: cálculo, gravação/GET pela API existente, recuperação e relatórios demonstrados; tipos/lint aprovados. Inclusão/correcção automática do acréscimo na base não incluída. |
|
||||||
|
|
||||||
|
Novo residual avançado após a instalação: opção explícita de derivar a quota económica da colecta L6 × (lucros incluídos + crédito nominal já acrescido) / matéria colectável L6. CIRPC64(1–2)/53 e ordem já aceites,sem novo enquadramento jurídico genérico. Exige correspondência documentada à base corrente; origem alterada,acréscimo ausente ou componentes acima da base tornam o caso dependente indeterminado. Não reescreve L6,não acrescenta o crédito duas vezes,não altera o modo manual/históricos. Prova `outputs/irpc-economic-quota-20261110/verification.json`:quota256/5, saldo3244/5 no cenário sintético. L8 continua parcial,com inclusão/correcção económica,art64(3/4),CDT automática,obrigações e restantes casos ainda por implementar conforme saldo;L1–L7 eL9–L16 preservados.
|
||||||
|
|
||||||
### Actualização fiscal: recorte L8 demonstrado em QA; saldo L1–L16 preservado
|
### Actualização fiscal: recorte L8 demonstrado em QA; saldo L1–L16 preservado
|
||||||
|
|
||||||
**Pendência encerrada nesta passagem em QA:** derivação proporcional da fracção nacional do crédito internacional no caso L6 ordinário corrente, com parcela líquida já incluída e correspondida à matéria colectável. Percurso cálculo → gravação/recuperação pela API privada existente → recálculo → HTML/relatório do dossier demonstrado. Não é fecho integral de L8, determinação automática de CDT ou repartição de custos/perdas. Delta agrupável com a continuidade de Educação, ainda não instalado na homologação, que mantém `ff726e57`.
|
**Pendência encerrada nesta passagem em QA:** derivação proporcional da fracção nacional do crédito internacional no caso L6 ordinário corrente, com parcela líquida já incluída e correspondida à matéria colectável. Percurso cálculo → gravação/recuperação pela API privada existente → recálculo → HTML/relatório do dossier demonstrado. Não é fecho integral de L8, determinação automática de CDT ou repartição de custos/perdas. Delta agrupável com a continuidade de Educação, ainda não instalado na homologação, que mantém `ff726e57`.
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { annualBook } from '@/lib/irpc-annual-settlement';
|
||||||
|
import type { NormalBook } from '@/lib/irpc-normal';
|
||||||
|
import {
|
||||||
|
prepareEconomicInclusion,
|
||||||
|
calculateEconomicInclusions,
|
||||||
|
economicInclusionMoney,
|
||||||
|
} from '@/lib/irpc-normal-economic';
|
||||||
|
|
||||||
|
export function SimulationIrpcEconomicInclusion({
|
||||||
|
draft,
|
||||||
|
book,
|
||||||
|
save,
|
||||||
|
}: {
|
||||||
|
draft: Record<string, unknown>;
|
||||||
|
book: NormalBook;
|
||||||
|
save: (b: NormalBook) => void;
|
||||||
|
}) {
|
||||||
|
const [selected, setSelected] = useState('');
|
||||||
|
const [profit, setProfit] = useState('');
|
||||||
|
const [credit, setCredit] = useState('');
|
||||||
|
const [reference, setReference] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [preview, setPreview] = useState<{
|
||||||
|
fingerprint: string;
|
||||||
|
book: NormalBook;
|
||||||
|
net: string;
|
||||||
|
} | null>(null);
|
||||||
|
const sources = annualBook(draft)?.economicCredits?.rows ?? [];
|
||||||
|
if (!sources.length) return null;
|
||||||
|
const fingerprint = JSON.stringify([
|
||||||
|
book,
|
||||||
|
sources,
|
||||||
|
selected,
|
||||||
|
profit,
|
||||||
|
credit,
|
||||||
|
reference,
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="space-y-3 rounded border p-3"
|
||||||
|
aria-label="Incluir ou corrigir lucros e crédito económico em L6"
|
||||||
|
>
|
||||||
|
<h3 className="font-semibold">
|
||||||
|
Incluir ou corrigir lucros e crédito económico em L6
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Escolha a origem preparada em L8. Indique os montantes já
|
||||||
|
considerados na contabilidade e noutros ajustamentos, antes
|
||||||
|
deste mapa; zero deve ser indicado expressamente. A prévia
|
||||||
|
propõe apenas a diferença e conserva o resultado anterior.
|
||||||
|
Depois de confirmar, calcule e guarde L6 pelo percurso
|
||||||
|
existente; L8 exige nova conferência da colecta.
|
||||||
|
</p>
|
||||||
|
<label className="block">
|
||||||
|
Origem dos lucros
|
||||||
|
<select
|
||||||
|
value={selected}
|
||||||
|
onChange={(e) => setSelected(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Escolha a origem</option>
|
||||||
|
{sources.map((r) => (
|
||||||
|
<option key={r.id} value={r.id}>
|
||||||
|
{r.distributionDocument} — {r.distributionParcel}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="block">
|
||||||
|
Rendimentos já considerados — MZN (incluindo eventual mais-valia
|
||||||
|
da partilha)
|
||||||
|
<input
|
||||||
|
value={profit}
|
||||||
|
onChange={(e) => setProfit(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="block">
|
||||||
|
Crédito nominal já acrescido — MZN
|
||||||
|
<input
|
||||||
|
value={credit}
|
||||||
|
onChange={(e) => setCredit(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="block">
|
||||||
|
Suporte da reconciliação
|
||||||
|
<textarea
|
||||||
|
value={reference}
|
||||||
|
onChange={(e) => setReference(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{error && <p role="alert">{error}</p>}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
const source = sources.find((r) => r.id === selected);
|
||||||
|
if (!source)
|
||||||
|
throw Error('Escolha explicitamente uma origem.');
|
||||||
|
const next = prepareEconomicInclusion(book, {
|
||||||
|
source,
|
||||||
|
profitAlreadyIncluded: profit,
|
||||||
|
creditAlreadyIncluded: credit,
|
||||||
|
reference,
|
||||||
|
});
|
||||||
|
const model = calculateEconomicInclusions(
|
||||||
|
next.economicInclusions!,
|
||||||
|
next.base!.subjectNuit,
|
||||||
|
next.year,
|
||||||
|
);
|
||||||
|
setPreview({
|
||||||
|
fingerprint,
|
||||||
|
book: next,
|
||||||
|
net: economicInclusionMoney(model.net),
|
||||||
|
});
|
||||||
|
setError('');
|
||||||
|
} catch (e) {
|
||||||
|
setPreview(null);
|
||||||
|
setError(String(e));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pré-visualizar inclusão/correcção
|
||||||
|
</button>
|
||||||
|
{preview && preview.fingerprint === fingerprint && (
|
||||||
|
<div className="space-y-2 rounded border p-3">
|
||||||
|
<p>
|
||||||
|
Efeito total deste mapa no lucro tributável:{' '}
|
||||||
|
{preview.net} MZN. Substitui as parcelas da mesma
|
||||||
|
origem, sem as acumular.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (preview.fingerprint !== fingerprint) return;
|
||||||
|
save(preview.book);
|
||||||
|
setPreview(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirmar preparação em L6
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setPreview(null)}>
|
||||||
|
Cancelar prévia
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -110,7 +110,10 @@ export function SimulationIrpcEconomic({
|
|||||||
rowChange({ ...r, ...v });
|
rowChange({ ...r, ...v });
|
||||||
const prefix = 'Crédito económico ' + (i + 1) + ' — ';
|
const prefix = 'Crédito económico ' + (i + 1) + ' — ';
|
||||||
const field = (
|
const field = (
|
||||||
key: Exclude<keyof EconomicPosition, 'previousUse'>,
|
key: Exclude<
|
||||||
|
keyof EconomicPosition,
|
||||||
|
'previousUse' | 'liquidation' | 'association'
|
||||||
|
>,
|
||||||
label: string,
|
label: string,
|
||||||
) => (
|
) => (
|
||||||
<label key={key} className="block">
|
<label key={key} className="block">
|
||||||
@@ -145,9 +148,7 @@ export function SimulationIrpcEconomic({
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
const outside =
|
const outside = r.article40 === 'yes';
|
||||||
(!!r.kind && r.kind !== 'ordinary_profit') ||
|
|
||||||
r.article40 === 'yes';
|
|
||||||
const detail = (
|
const detail = (
|
||||||
<>
|
<>
|
||||||
{(
|
{(
|
||||||
@@ -226,7 +227,60 @@ export function SimulationIrpcEconomic({
|
|||||||
'Suporte da quota proporcional da colecta',
|
'Suporte da quota proporcional da colecta',
|
||||||
],
|
],
|
||||||
] as const
|
] as const
|
||||||
).map(([key, label]) => field(key, label))}
|
)
|
||||||
|
.filter(
|
||||||
|
([key]) =>
|
||||||
|
r.nationalMethod !== 'proportional' ||
|
||||||
|
(key !== 'nationalLimit' &&
|
||||||
|
key !== 'nationalLimitReference'),
|
||||||
|
)
|
||||||
|
.map(([key, label]) => field(key, label))}
|
||||||
|
<label className="block">
|
||||||
|
Quota proporcional
|
||||||
|
<select
|
||||||
|
aria-label={prefix + 'Método da quota'}
|
||||||
|
value={r.nationalMethod ?? 'documented'}
|
||||||
|
onChange={(e) =>
|
||||||
|
patch({
|
||||||
|
nationalMethod: e.target
|
||||||
|
.value as EconomicPosition['nationalMethod'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="documented">
|
||||||
|
Limite documentado
|
||||||
|
</option>
|
||||||
|
<option value="proportional">
|
||||||
|
Derivar da colecta L6 ordinária
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{r.nationalMethod === 'proportional' && (
|
||||||
|
<label className="block">
|
||||||
|
Correspondência dos lucros mais acréscimo53 na
|
||||||
|
base corrente
|
||||||
|
<textarea
|
||||||
|
aria-label={
|
||||||
|
prefix + 'Correspondência da quota'
|
||||||
|
}
|
||||||
|
value={r.nationalBasisReference ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
patch({
|
||||||
|
nationalBasisReference:
|
||||||
|
e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="block">
|
||||||
|
Quota = colecta L6 × (lucros incluídos +
|
||||||
|
crédito nominal acrescido) / matéria
|
||||||
|
colectável L6. Não altera a base nem
|
||||||
|
acrescenta o crédito novamente. O limite
|
||||||
|
manual anterior fica conservado, mas não é
|
||||||
|
usado neste modo.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
{choice(
|
{choice(
|
||||||
'incomeIncluded',
|
'incomeIncluded',
|
||||||
'Lucros ilíquidos e acréscimo incluídos na base corrente',
|
'Lucros ilíquidos e acréscimo incluídos na base corrente',
|
||||||
@@ -339,6 +393,155 @@ export function SimulationIrpcEconomic({
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
{r.kind === 'association' && (
|
||||||
|
<fieldset className="space-y-2 rounded border p-3">
|
||||||
|
<legend>Associação — art.64(4)</legend>
|
||||||
|
<label className="block">
|
||||||
|
Modalidade
|
||||||
|
<select
|
||||||
|
aria-label={
|
||||||
|
prefix + 'Modalidade da associação'
|
||||||
|
}
|
||||||
|
value={r.association?.kind ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
patch({
|
||||||
|
association: {
|
||||||
|
kind: e.target.value as
|
||||||
|
| ''
|
||||||
|
| 'participation'
|
||||||
|
| 'quota',
|
||||||
|
effectivelyTaxed:
|
||||||
|
r.association
|
||||||
|
?.effectivelyTaxed ??
|
||||||
|
'',
|
||||||
|
reference:
|
||||||
|
r.association
|
||||||
|
?.reference ?? '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">
|
||||||
|
Escolha a modalidade
|
||||||
|
</option>
|
||||||
|
<option value="participation">
|
||||||
|
Em participação
|
||||||
|
</option>
|
||||||
|
<option value="quota">À quota</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{r.association?.kind === 'participation' && (
|
||||||
|
<label className="block">
|
||||||
|
Rendimentos distribuídos efectivamente
|
||||||
|
tributados
|
||||||
|
<select
|
||||||
|
aria-label={
|
||||||
|
prefix +
|
||||||
|
'Tributação efectiva da participação'
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
r.association.effectivelyTaxed
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
patch({
|
||||||
|
association: {
|
||||||
|
...r.association!,
|
||||||
|
effectivelyTaxed: 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>
|
||||||
|
)}
|
||||||
|
<label className="block">
|
||||||
|
Suporte do contrato, rendimentos e adaptação
|
||||||
|
do crédito
|
||||||
|
<input
|
||||||
|
aria-label={
|
||||||
|
prefix + 'Suporte da associação'
|
||||||
|
}
|
||||||
|
value={r.association?.reference ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
patch({
|
||||||
|
association: {
|
||||||
|
kind: '',
|
||||||
|
effectivelyTaxed: '',
|
||||||
|
...r.association,
|
||||||
|
reference: e.target.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
|
{r.kind === 'liquidation' && (
|
||||||
|
<fieldset className="space-y-2 rounded border p-3">
|
||||||
|
<legend>
|
||||||
|
Partilha em liquidação — art.64(3)
|
||||||
|
</legend>
|
||||||
|
<p>
|
||||||
|
No rendimento abaixo indique a diferença
|
||||||
|
positiva classificada documentalmente como
|
||||||
|
aplicação de capitais. O crédito limita-se
|
||||||
|
também ao valor atribuído menos entradas
|
||||||
|
efectivas para capital. O excesso mantém
|
||||||
|
tratamento próprio de mais-valia; não
|
||||||
|
integra a quota económica. Confirme a
|
||||||
|
inclusão L6 e o IRPC correspondente apenas à
|
||||||
|
parcela elegível.
|
||||||
|
</p>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'attributed',
|
||||||
|
'Valor atribuído na partilha — MZN',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'paidInCapital',
|
||||||
|
'Entradas efectivas para capital na contabilidade da liquidada — MZN',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'reference',
|
||||||
|
'Suporte da partilha e classificação do rendimento',
|
||||||
|
],
|
||||||
|
] as const
|
||||||
|
).map(([key, label]) => (
|
||||||
|
<label key={key} className="block">
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
aria-label={prefix + label}
|
||||||
|
value={r.liquidation?.[key] ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
patch({
|
||||||
|
liquidation: {
|
||||||
|
attributed: '',
|
||||||
|
paidInCapital: '',
|
||||||
|
reference: '',
|
||||||
|
...r.liquidation,
|
||||||
|
[key]: e.target.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
{residentConfirmed && r.recipientResident === 'yes' ? (
|
{residentConfirmed && r.recipientResident === 'yes' ? (
|
||||||
<p>
|
<p>
|
||||||
Residência do beneficiário reutilizada da
|
Residência do beneficiário reutilizada da
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { SimulationIrpcPeriodFx } from '@/components/simulation-irpc-period-fx';
|
|||||||
import { SimulationIrpcDepreciation } from '@/components/simulation-irpc-depreciation';
|
import { SimulationIrpcDepreciation } from '@/components/simulation-irpc-depreciation';
|
||||||
import { SimulationIrpcCosts } from '@/components/simulation-irpc-costs';
|
import { SimulationIrpcCosts } from '@/components/simulation-irpc-costs';
|
||||||
import { SimulationIrpcSchedules } from '@/components/simulation-irpc-schedules';
|
import { SimulationIrpcSchedules } from '@/components/simulation-irpc-schedules';
|
||||||
|
import { SimulationIrpcEconomicInclusion } from '@/components/simulation-irpc-economic-inclusion';
|
||||||
import { SimulationIrpcAssessment } from '@/components/simulation-irpc-assessment';
|
import { SimulationIrpcAssessment } from '@/components/simulation-irpc-assessment';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { SimulationIrpcTaxableBase } from '@/components/simulation-irpc-taxable-base';
|
import { SimulationIrpcTaxableBase } from '@/components/simulation-irpc-taxable-base';
|
||||||
@@ -438,6 +439,16 @@ export function SimulationIrpcNormal({
|
|||||||
)}
|
)}
|
||||||
<SimulationIrpcPeriodFx draft={draft} book={b} save={save} />
|
<SimulationIrpcPeriodFx draft={draft} book={b} save={save} />
|
||||||
<SimulationIrpcSchedules book={b} save={save} />
|
<SimulationIrpcSchedules book={b} save={save} />
|
||||||
|
<SimulationIrpcEconomicInclusion
|
||||||
|
key={JSON.stringify([
|
||||||
|
draft.context_opportunity,
|
||||||
|
b.base?.subjectNuit,
|
||||||
|
b.year,
|
||||||
|
])}
|
||||||
|
draft={draft}
|
||||||
|
book={b}
|
||||||
|
save={save}
|
||||||
|
/>
|
||||||
<SimulationIrpcDepreciation
|
<SimulationIrpcDepreciation
|
||||||
draft={draft}
|
draft={draft}
|
||||||
book={b}
|
book={b}
|
||||||
|
|||||||
@@ -1193,6 +1193,13 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
|||||||
b.assessmentKey,
|
b.assessmentKey,
|
||||||
colecta,
|
colecta,
|
||||||
issues,
|
issues,
|
||||||
|
{
|
||||||
|
ordinary:
|
||||||
|
assessment?.kind === 'normal' &&
|
||||||
|
assessment.current &&
|
||||||
|
assessment.key === b.assessmentKey,
|
||||||
|
taxableBase: assessment?.record?.model.taxableBase ?? null,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
pending.push(
|
pending.push(
|
||||||
...m.pending.map(
|
...m.pending.map(
|
||||||
@@ -1202,6 +1209,22 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
|||||||
return m;
|
return m;
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
if (b.economicCredits) {
|
if (b.economicCredits) {
|
||||||
|
if (
|
||||||
|
currentBase &&
|
||||||
|
cmp(
|
||||||
|
addFractions(
|
||||||
|
economicRows.flatMap((r) =>
|
||||||
|
r.nationalDerivation
|
||||||
|
? [r.nationalDerivation.included]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
currentBase,
|
||||||
|
) > 0n
|
||||||
|
)
|
||||||
|
pending.push(
|
||||||
|
'As parcelas económicas com quotas derivadas excedem em conjunto a matéria colectável; reconcilie os lucros e acréscimos por origem.',
|
||||||
|
);
|
||||||
if (b.economicCredits.rows.length && assessment?.kind !== 'normal')
|
if (b.economicCredits.rows.length && assessment?.kind !== 'normal')
|
||||||
pending.push(
|
pending.push(
|
||||||
'Crédito económico delimitado à colecta normal corrente;efeitos L7 exigem correspondência própria.',
|
'Crédito económico delimitado à colecta normal corrente;efeitos L7 exigem correspondência própria.',
|
||||||
@@ -1390,7 +1413,7 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
|||||||
limitations: [
|
limitations: [
|
||||||
...(b.economicCredits
|
...(b.economicCredits
|
||||||
? [
|
? [
|
||||||
'Crédito económico ordinário:IRPC correspondente aos lucros,acréscimo53 já incluído e quota proporcional documentados. Sem cálculo automático da inclusão ou da exclusão40;liquidação64(3),associações64(4),benefícios e regras especiais separados. Sem reembolso ou transporte económico presumidos.',
|
'Crédito económico: IRPC correspondente, inclusão/acréscimo53 e quota proporcional reconciliados com a colecta corrente. Partilha64(3) separa rendimento elegível e excesso com natureza de mais-valia; associações64(4) têm modalidade/adaptação próprias. Preparação L6 é explícita; exclusão40, benefícios e restantes regras mantêm âmbito próprio. Sem reembolso ou transporte económico presumidos.',
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(b.creditCarry
|
...(b.creditCarry
|
||||||
@@ -1598,6 +1621,51 @@ export function annualReportRows(
|
|||||||
r.input.grossUpIncluded +
|
r.input.grossUpIncluded +
|
||||||
' MZN;quota proporcional: ' +
|
' MZN;quota proporcional: ' +
|
||||||
show(r.nationalLimit) +
|
show(r.nationalLimit) +
|
||||||
|
(r.nationalDerivation
|
||||||
|
? '; derivação L6: colecta ' +
|
||||||
|
show(r.nationalDerivation.colecta) +
|
||||||
|
' × lucros mais acréscimo53 ' +
|
||||||
|
show(
|
||||||
|
r.nationalDerivation.included,
|
||||||
|
) +
|
||||||
|
' / matéria colectável ' +
|
||||||
|
show(
|
||||||
|
r.nationalDerivation
|
||||||
|
.taxableBase,
|
||||||
|
) +
|
||||||
|
'; exacta ' +
|
||||||
|
r.nationalLimit?.numerator +
|
||||||
|
'/' +
|
||||||
|
r.nationalLimit?.denominator
|
||||||
|
: '') +
|
||||||
|
(r.liquidation
|
||||||
|
? '; partilha64(3): valor atribuído ' +
|
||||||
|
show(r.liquidation.attributed) +
|
||||||
|
'; entradas efectivas ' +
|
||||||
|
show(r.liquidation.paidInCapital) +
|
||||||
|
'; limite ' +
|
||||||
|
show(r.liquidation.ceiling) +
|
||||||
|
'; rendimento elegível ' +
|
||||||
|
show(r.liquidation.eligibleIncome) +
|
||||||
|
'; excesso com natureza de mais-valia ' +
|
||||||
|
show(
|
||||||
|
r.liquidation.excessCapitalGain,
|
||||||
|
) +
|
||||||
|
'; ' +
|
||||||
|
r.liquidation.version +
|
||||||
|
'; ' +
|
||||||
|
r.liquidation.reference
|
||||||
|
: '') +
|
||||||
|
(r.association
|
||||||
|
? '; ' +
|
||||||
|
r.association.version +
|
||||||
|
'; modalidade ' +
|
||||||
|
r.association.kind +
|
||||||
|
'; tributação efectiva ' +
|
||||||
|
r.association.effectivelyTaxed +
|
||||||
|
'; ' +
|
||||||
|
r.association.reference
|
||||||
|
: '') +
|
||||||
';elegível: ' +
|
';elegível: ' +
|
||||||
show(r.eligible) +
|
show(r.eligible) +
|
||||||
';uso efectivo anterior: ' +
|
';uso efectivo anterior: ' +
|
||||||
@@ -1616,7 +1684,12 @@ export function annualReportRows(
|
|||||||
';53: ' +
|
';53: ' +
|
||||||
r.input.grossUpReference +
|
r.input.grossUpReference +
|
||||||
';quota: ' +
|
';quota: ' +
|
||||||
r.input.nationalLimitReference +
|
(r.nationalDerivation
|
||||||
|
? r.nationalDerivation.version +
|
||||||
|
'; correspondência: ' +
|
||||||
|
r.nationalDerivation.reference +
|
||||||
|
'; limite manual anterior conservado, não utilizado'
|
||||||
|
: r.input.nationalLimitReference) +
|
||||||
';uso: ' +
|
';uso: ' +
|
||||||
r.input.previousUse.reference,
|
r.input.previousUse.reference,
|
||||||
] as [string, string, string, string],
|
] as [string, string, string, string],
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ export type EconomicPosition = {
|
|||||||
grossUpReference: string;
|
grossUpReference: string;
|
||||||
nationalLimit: string;
|
nationalLimit: string;
|
||||||
nationalLimitReference: string;
|
nationalLimitReference: string;
|
||||||
|
association?: {
|
||||||
|
kind: '' | 'participation' | 'quota';
|
||||||
|
effectivelyTaxed: Choice;
|
||||||
|
reference: string;
|
||||||
|
};
|
||||||
|
liquidation?: {
|
||||||
|
attributed: string;
|
||||||
|
paidInCapital: string;
|
||||||
|
reference: string;
|
||||||
|
};
|
||||||
|
nationalMethod?: 'documented' | 'proportional';
|
||||||
|
nationalBasisReference?: string;
|
||||||
previousUse: CreditEvidence;
|
previousUse: CreditEvidence;
|
||||||
};
|
};
|
||||||
export type EconomicCredits = {
|
export type EconomicCredits = {
|
||||||
@@ -113,12 +125,37 @@ export function validateEconomic(c: EconomicCredits) {
|
|||||||
throw Error(
|
throw Error(
|
||||||
'Crédito económico não recuperável; originais conservados.',
|
'Crédito económico não recuperável; originais conservados.',
|
||||||
);
|
);
|
||||||
|
for (const r of c.rows)
|
||||||
|
if (
|
||||||
|
r?.liquidation !== undefined &&
|
||||||
|
(!r.liquidation ||
|
||||||
|
['attributed', 'paidInCapital', 'reference'].some(
|
||||||
|
(k) =>
|
||||||
|
typeof (
|
||||||
|
r.liquidation as unknown as Record<string, unknown>
|
||||||
|
)[k] !== 'string',
|
||||||
|
))
|
||||||
|
)
|
||||||
|
throw Error('Partilha não recuperável; originais conservados.');
|
||||||
|
for (const r of c.rows)
|
||||||
|
if (
|
||||||
|
r?.association !== undefined &&
|
||||||
|
(!r.association ||
|
||||||
|
!['', 'participation', 'quota'].includes(r.association.kind) ||
|
||||||
|
!['', 'yes', 'no'].includes(r.association.effectivelyTaxed) ||
|
||||||
|
typeof r.association.reference !== 'string')
|
||||||
|
)
|
||||||
|
throw Error('Associação não recuperável; originais conservados.');
|
||||||
const keys = Object.keys(newEconomicPosition('')).filter(
|
const keys = Object.keys(newEconomicPosition('')).filter(
|
||||||
(k) => k !== 'previousUse',
|
(k) => k !== 'previousUse',
|
||||||
);
|
);
|
||||||
for (const r of c.rows)
|
for (const r of c.rows)
|
||||||
if (
|
if (
|
||||||
!r ||
|
!r ||
|
||||||
|
(r.nationalMethod !== undefined &&
|
||||||
|
!['documented', 'proportional'].includes(r.nationalMethod)) ||
|
||||||
|
(r.nationalBasisReference !== undefined &&
|
||||||
|
typeof r.nationalBasisReference !== 'string') ||
|
||||||
keys.some(
|
keys.some(
|
||||||
(k) =>
|
(k) =>
|
||||||
typeof (r as unknown as Record<string, unknown>)[k] !==
|
typeof (r as unknown as Record<string, unknown>)[k] !==
|
||||||
@@ -201,6 +238,57 @@ export function economicGroupPending(c: EconomicCredits) {
|
|||||||
);
|
);
|
||||||
return pending;
|
return pending;
|
||||||
}
|
}
|
||||||
|
export function economicIncome(r: EconomicPosition) {
|
||||||
|
const income = creditMoney(r.income);
|
||||||
|
if (r.kind === 'ordinary_profit') return { income };
|
||||||
|
if (r.kind === 'association') {
|
||||||
|
const a = r.association;
|
||||||
|
if (!a?.kind || !a.reference.trim())
|
||||||
|
throw Error(
|
||||||
|
'Identifique associação em participação ou à quota e o suporte da adaptação64(4).',
|
||||||
|
);
|
||||||
|
if (a.kind === 'participation' && a.effectivelyTaxed !== 'yes')
|
||||||
|
throw Error(
|
||||||
|
'Associação em participação exige rendimentos distribuídos efectivamente tributados; desconhecido não significa sim.',
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
income,
|
||||||
|
association: {
|
||||||
|
version: 'IRPC-economic-association/1',
|
||||||
|
...structuredClone(a),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (r.kind !== 'liquidation')
|
||||||
|
throw Error('Natureza económica por identificar.');
|
||||||
|
if (!r.liquidation?.reference.trim())
|
||||||
|
throw Error(
|
||||||
|
'Partilha64(3): identifique o valor atribuído, entradas efectivas para capital e classificação documental do rendimento.',
|
||||||
|
);
|
||||||
|
const attributed = creditMoney(r.liquidation.attributed),
|
||||||
|
paidInCapital = creditMoney(r.liquidation.paidInCapital);
|
||||||
|
if (creditCompare(income, attributed) > 0n)
|
||||||
|
throw Error(
|
||||||
|
'Rendimento da partilha não pode exceder o valor atribuído.',
|
||||||
|
);
|
||||||
|
const difference = subtractFractions(attributed, paidInCapital);
|
||||||
|
const ceiling =
|
||||||
|
creditCompare(difference, zero()) > 0n ? difference : zero();
|
||||||
|
const eligibleIncome = minimum(income, ceiling);
|
||||||
|
return {
|
||||||
|
income: eligibleIncome,
|
||||||
|
liquidation: {
|
||||||
|
version: 'IRPC-economic-liquidation/1',
|
||||||
|
attributed,
|
||||||
|
paidInCapital,
|
||||||
|
capitalIncome: income,
|
||||||
|
ceiling,
|
||||||
|
eligibleIncome,
|
||||||
|
excessCapitalGain: subtractFractions(income, eligibleIncome),
|
||||||
|
reference: r.liquidation.reference,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
export function economicPosition(
|
export function economicPosition(
|
||||||
r: EconomicPosition,
|
r: EconomicPosition,
|
||||||
subjectNuit: string,
|
subjectNuit: string,
|
||||||
@@ -208,6 +296,7 @@ export function economicPosition(
|
|||||||
assessmentKey: string,
|
assessmentKey: string,
|
||||||
colecta: FiscalFraction | null,
|
colecta: FiscalFraction | null,
|
||||||
proofIssues: string[],
|
proofIssues: string[],
|
||||||
|
basis?: { ordinary: boolean; taxableBase: FiscalFraction | null },
|
||||||
) {
|
) {
|
||||||
const pending = [...proofIssues];
|
const pending = [...proofIssues];
|
||||||
let nominal: FiscalFraction | null = null,
|
let nominal: FiscalFraction | null = null,
|
||||||
@@ -216,6 +305,15 @@ export function economicPosition(
|
|||||||
previousUse: FiscalFraction | null = null,
|
previousUse: FiscalFraction | null = null,
|
||||||
available: FiscalFraction | null = null,
|
available: FiscalFraction | null = null,
|
||||||
included: FiscalFraction | null = null;
|
included: FiscalFraction | null = null;
|
||||||
|
let liquidation: ReturnType<typeof economicIncome>['liquidation'];
|
||||||
|
let association: ReturnType<typeof economicIncome>['association'];
|
||||||
|
let nationalDerivation: {
|
||||||
|
version: string;
|
||||||
|
included: FiscalFraction;
|
||||||
|
taxableBase: FiscalFraction;
|
||||||
|
colecta: FiscalFraction;
|
||||||
|
reference: string;
|
||||||
|
} | null = null;
|
||||||
let status: 'qualified' | 'outside_scope' | 'indeterminate' =
|
let status: 'qualified' | 'outside_scope' | 'indeterminate' =
|
||||||
'indeterminate';
|
'indeterminate';
|
||||||
try {
|
try {
|
||||||
@@ -230,10 +328,12 @@ export function economicPosition(
|
|||||||
throw Error(
|
throw Error(
|
||||||
'Titular, entidade distribuidora, exercício2026 e MZN por corresponder.',
|
'Titular, entidade distribuidora, exercício2026 e MZN por corresponder.',
|
||||||
);
|
);
|
||||||
if (r.kind !== 'ordinary_profit') {
|
if (
|
||||||
|
!['ordinary_profit', 'liquidation', 'association'].includes(r.kind)
|
||||||
|
) {
|
||||||
status = r.kind ? 'outside_scope' : 'indeterminate';
|
status = r.kind ? 'outside_scope' : 'indeterminate';
|
||||||
throw Error(
|
throw Error(
|
||||||
'Lucros ordinários64(1–2) por identificar; liquidação64(3) e associações64(4) exigem correspondência específica, sem converter a parcela em crédito internacional.',
|
'Identifique lucros ordinários64(1–2), partilha64(3) ou associação64(4), com correspondência própria; não converter a parcela em crédito internacional.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -272,8 +372,11 @@ export function economicPosition(
|
|||||||
throw Error(
|
throw Error(
|
||||||
'Confirme a inclusão dos lucros ilíquidos de retenção e a correspondência com a colecta corrente; a origem pode ter mudado.',
|
'Confirme a inclusão dos lucros ilíquidos de retenção e a correspondência com a colecta corrente; a origem pode ter mudado.',
|
||||||
);
|
);
|
||||||
|
const qualified = economicIncome(r);
|
||||||
|
liquidation = qualified.liquidation;
|
||||||
|
association = qualified.association;
|
||||||
const tax = economicTaxProof(r),
|
const tax = economicTaxProof(r),
|
||||||
income = creditMoney(r.income);
|
income = qualified.income;
|
||||||
nominal = fraction(
|
nominal = fraction(
|
||||||
BigInt(tax.value.numerator) * 3n,
|
BigInt(tax.value.numerator) * 3n,
|
||||||
BigInt(tax.value.denominator) * 5n,
|
BigInt(tax.value.denominator) * 5n,
|
||||||
@@ -293,9 +396,41 @@ export function economicPosition(
|
|||||||
'Acréscimo53(1)a incluído na base deve corresponder ao crédito nominal60%; reconcilie L6 sem alterar a colecta ou o histórico automaticamente.',
|
'Acréscimo53(1)a incluído na base deve corresponder ao crédito nominal60%; reconcilie L6 sem alterar a colecta ou o histórico automaticamente.',
|
||||||
);
|
);
|
||||||
included = addFractions([income, nominal]);
|
included = addFractions([income, nominal]);
|
||||||
nationalLimit = creditLimit(r.nationalLimit);
|
if (r.nationalMethod === 'proportional') {
|
||||||
|
if (
|
||||||
|
!basis?.ordinary ||
|
||||||
|
!basis.taxableBase ||
|
||||||
|
creditCompare(basis.taxableBase, zero()) <= 0n ||
|
||||||
|
!colecta ||
|
||||||
|
creditCompare(colecta, zero()) < 0n ||
|
||||||
|
!r.nationalBasisReference?.trim()
|
||||||
|
)
|
||||||
|
throw Error(
|
||||||
|
'Quota económica derivada exige colecta L6 ordinária corrente, matéria colectável positiva e correspondência dos lucros mais acréscimo53 já incluídos.',
|
||||||
|
);
|
||||||
|
if (creditCompare(included, basis.taxableBase) > 0n)
|
||||||
|
throw Error(
|
||||||
|
'Lucros mais acréscimo53 excedem a matéria colectável; reconcilie a inclusão antes de derivar a quota.',
|
||||||
|
);
|
||||||
|
nationalLimit = fraction(
|
||||||
|
BigInt(colecta.numerator) *
|
||||||
|
BigInt(included.numerator) *
|
||||||
|
BigInt(basis.taxableBase.denominator),
|
||||||
|
BigInt(colecta.denominator) *
|
||||||
|
BigInt(included.denominator) *
|
||||||
|
BigInt(basis.taxableBase.numerator),
|
||||||
|
);
|
||||||
|
nationalDerivation = {
|
||||||
|
version: 'IRPC-economic-quota/1',
|
||||||
|
included,
|
||||||
|
taxableBase: structuredClone(basis.taxableBase),
|
||||||
|
colecta: structuredClone(colecta),
|
||||||
|
reference: r.nationalBasisReference,
|
||||||
|
};
|
||||||
|
} else nationalLimit = creditLimit(r.nationalLimit);
|
||||||
if (
|
if (
|
||||||
!r.nationalLimitReference.trim() ||
|
(r.nationalMethod !== 'proportional' &&
|
||||||
|
!r.nationalLimitReference.trim()) ||
|
||||||
!colecta ||
|
!colecta ||
|
||||||
creditCompare(nationalLimit, colecta) > 0n ||
|
creditCompare(nationalLimit, colecta) > 0n ||
|
||||||
(creditCompare(included, zero()) === 0n &&
|
(creditCompare(included, zero()) === 0n &&
|
||||||
@@ -327,6 +462,9 @@ export function economicPosition(
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
nominal,
|
nominal,
|
||||||
nationalLimit,
|
nationalLimit,
|
||||||
|
...(nationalDerivation ? { nationalDerivation } : {}),
|
||||||
|
...(liquidation ? { liquidation } : {}),
|
||||||
|
...(association ? { association } : {}),
|
||||||
eligible,
|
eligible,
|
||||||
previousUse,
|
previousUse,
|
||||||
available,
|
available,
|
||||||
@@ -354,7 +492,17 @@ export function economicDeduction(
|
|||||||
deducted,
|
deducted,
|
||||||
afterCredit:
|
afterCredit:
|
||||||
deducted && colecta ? subtractFractions(colecta, deducted) : null,
|
deducted && colecta ? subtractFractions(colecta, deducted) : null,
|
||||||
source: economicSource,
|
source:
|
||||||
|
economicSource +
|
||||||
|
(rows.some((r) => r.association)
|
||||||
|
? ' IRPC-economic-association/1:CIRPC64(4), associação em participação com tributação efectiva documentada ou associação à quota, com adaptação, inclusão L6 e imposto correspondente expressamente suportados.'
|
||||||
|
: '') +
|
||||||
|
(rows.some((r) => r.liquidation)
|
||||||
|
? ' IRPC-economic-liquidation/1:CIRPC64(3); rendimento de capitais documentado limitado à diferença positiva entre valor atribuído e entradas efectivas para capital. Excesso separado como mais-valia, sem crédito económico sobre essa parcela; inclusão L6 e imposto subjacente exigem correspondência expressa.'
|
||||||
|
: '') +
|
||||||
|
(rows.some((r) => r.nationalDerivation)
|
||||||
|
? ' IRPC-economic-quota/1:quota ordinária derivada da colecta e matéria colectável L6 com lucros mais acréscimo53 já correspondidos;sem alterar a base ou o acréscimo.'
|
||||||
|
: ''),
|
||||||
officialBalancesConsumed: false,
|
officialBalancesConsumed: false,
|
||||||
carryCreated: false,
|
carryCreated: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
import { canonicalParcel } from './fiscal-proof-parcel.ts';
|
||||||
|
import type { NormalBook } from './irpc-normal.ts';
|
||||||
|
import {
|
||||||
|
economicTaxProof,
|
||||||
|
economicDistributionIdentity,
|
||||||
|
economicIncome,
|
||||||
|
validateEconomic,
|
||||||
|
type EconomicPosition,
|
||||||
|
} from './irpc-economic-credit.ts';
|
||||||
|
import {
|
||||||
|
creditMoney,
|
||||||
|
creditLimit,
|
||||||
|
creditCompare,
|
||||||
|
} from './irpc-international-credit.ts';
|
||||||
|
import {
|
||||||
|
addFractions,
|
||||||
|
subtractFractions,
|
||||||
|
fraction,
|
||||||
|
type FiscalFraction,
|
||||||
|
} from './fiscal-rational.ts';
|
||||||
|
|
||||||
|
export type EconomicInclusion = {
|
||||||
|
source: EconomicPosition;
|
||||||
|
profitAlreadyIncluded: string;
|
||||||
|
creditAlreadyIncluded: string;
|
||||||
|
reference: string;
|
||||||
|
};
|
||||||
|
export type EconomicInclusions = { version: 1; rows: EconomicInclusion[] };
|
||||||
|
export const economicInclusionVersion = 'IRPC-economic-inclusion/1';
|
||||||
|
export function validateEconomicInclusions(input: EconomicInclusions) {
|
||||||
|
if (
|
||||||
|
input?.version !== 1 ||
|
||||||
|
!Array.isArray(input.rows) ||
|
||||||
|
input.rows.some(
|
||||||
|
(r) =>
|
||||||
|
!r ||
|
||||||
|
typeof r.profitAlreadyIncluded !== 'string' ||
|
||||||
|
typeof r.creditAlreadyIncluded !== 'string' ||
|
||||||
|
typeof r.reference !== 'string',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw Error('Inclusões económicas não recuperáveis.');
|
||||||
|
validateEconomic({
|
||||||
|
version: 1,
|
||||||
|
state: 'known',
|
||||||
|
reference: economicInclusionVersion,
|
||||||
|
rows: input.rows.map((r) => r.source),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function economicInclusionMoney(value: FiscalFraction): string {
|
||||||
|
const n = BigInt(value.numerator) * 100n,
|
||||||
|
d = BigInt(value.denominator);
|
||||||
|
if (n % d !== 0n)
|
||||||
|
throw Error(
|
||||||
|
'Correcção com fracção de cêntimo exige tratamento próprio; não arredondar por presunção.',
|
||||||
|
);
|
||||||
|
const cents = n / d,
|
||||||
|
absolute = cents < 0n ? -cents : cents;
|
||||||
|
return (
|
||||||
|
(cents < 0n ? '-' : '') +
|
||||||
|
absolute / 100n +
|
||||||
|
'.' +
|
||||||
|
(absolute % 100n).toString().padStart(2, '0')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function calculateEconomicInclusions(
|
||||||
|
input: EconomicInclusions,
|
||||||
|
subject: string,
|
||||||
|
year: string,
|
||||||
|
) {
|
||||||
|
validateEconomicInclusions(input);
|
||||||
|
const pending: string[] = [];
|
||||||
|
const effects: {
|
||||||
|
id: string;
|
||||||
|
topic: string;
|
||||||
|
amount: FiscalFraction | null;
|
||||||
|
effect: FiscalFraction | null;
|
||||||
|
reason: string;
|
||||||
|
}[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const row of input.rows) {
|
||||||
|
const r = row.source;
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!r ||
|
||||||
|
!['ordinary_profit', 'liquidation', 'association'].includes(
|
||||||
|
r.kind,
|
||||||
|
) ||
|
||||||
|
!r.id.trim() ||
|
||||||
|
!/^\d{9}$/.test(r.payerNuit) ||
|
||||||
|
r.subjectNuit !== subject ||
|
||||||
|
r.year !== year ||
|
||||||
|
year !== '2026' ||
|
||||||
|
r.recipientResident !== 'yes' ||
|
||||||
|
r.payerResident !== 'yes' ||
|
||||||
|
r.payerIrpc !== 'yes' ||
|
||||||
|
r.payerExempt !== 'no' ||
|
||||||
|
r.article40 !== 'no' ||
|
||||||
|
!r.qualificationReference?.trim() ||
|
||||||
|
!r.article40Reference?.trim() ||
|
||||||
|
!row.reference?.trim() ||
|
||||||
|
!r.distributionDocument?.trim() ||
|
||||||
|
!r.distributionParcel?.trim() ||
|
||||||
|
!/^2026-\d{2}-\d{2}$/.test(r.distributionDate) ||
|
||||||
|
new Date(r.distributionDate).toISOString().slice(0, 10) !==
|
||||||
|
r.distributionDate
|
||||||
|
)
|
||||||
|
throw Error(
|
||||||
|
'Falta qualificação, origem, titular/exercício ou correspondência da inclusão; art40 e outros âmbitos separados.',
|
||||||
|
);
|
||||||
|
const identity =
|
||||||
|
r.distributionDocument.trim().toUpperCase() +
|
||||||
|
'|' +
|
||||||
|
r.distributionParcel.trim().toUpperCase();
|
||||||
|
const taxIdentity = JSON.stringify([
|
||||||
|
canonicalParcel(r.taxIssuer),
|
||||||
|
canonicalParcel(r.taxDocument),
|
||||||
|
canonicalParcel(r.taxParcel),
|
||||||
|
]);
|
||||||
|
const distributionIdentity = economicDistributionIdentity(r);
|
||||||
|
if (
|
||||||
|
seen.has(distributionIdentity) ||
|
||||||
|
seen.has('tax|' + taxIdentity)
|
||||||
|
)
|
||||||
|
throw Error(
|
||||||
|
'Origem/parcela económica repetida; consolide antes de corrigir a base.',
|
||||||
|
);
|
||||||
|
seen.add(distributionIdentity);
|
||||||
|
seen.add('tax|' + taxIdentity);
|
||||||
|
const qualification = economicIncome(r);
|
||||||
|
const tax = economicTaxProof(r).value;
|
||||||
|
const nominal = fraction(
|
||||||
|
BigInt(tax.numerator) * 3n,
|
||||||
|
BigInt(tax.denominator) * 5n,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
creditCompare(qualification.income, fraction(0n)) === 0n &&
|
||||||
|
creditCompare(nominal, fraction(0n)) > 0n
|
||||||
|
)
|
||||||
|
throw Error(
|
||||||
|
'Imposto subjacente positivo sem rendimento elegível para crédito económico.',
|
||||||
|
);
|
||||||
|
const specialReference = qualification.liquidation
|
||||||
|
? `${qualification.liquidation.version}; rendimento elegível ${economicInclusionMoney(qualification.liquidation.eligibleIncome)}; excesso com natureza de mais-valia ${economicInclusionMoney(qualification.liquidation.excessCapitalGain)} incluído no rendimento total a reconciliar. ${qualification.liquidation.reference}`
|
||||||
|
: qualification.association
|
||||||
|
? `${qualification.association.version}; ${qualification.association.kind}; ${qualification.association.reference}`
|
||||||
|
: '';
|
||||||
|
const targets = [creditMoney(r.income), nominal];
|
||||||
|
const included = [
|
||||||
|
creditMoney(row.profitAlreadyIncluded),
|
||||||
|
creditMoney(row.creditAlreadyIncluded),
|
||||||
|
];
|
||||||
|
if (
|
||||||
|
r.incomeIncluded === 'yes' &&
|
||||||
|
creditCompare(included[0], targets[0]) < 0n
|
||||||
|
)
|
||||||
|
throw Error(
|
||||||
|
'A origem L8 declara os lucros já incluídos; o montante anterior indicado contradiz essa declaração. Compare e corrija a origem antes de acrescentar novamente.',
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
r.grossUpIncluded.trim() &&
|
||||||
|
creditCompare(creditLimit(r.grossUpIncluded), included[1]) !==
|
||||||
|
0n
|
||||||
|
)
|
||||||
|
throw Error(
|
||||||
|
'O crédito já acrescido indicado não corresponde à origem L8; reconcilie a diferença antes de alterar a base.',
|
||||||
|
);
|
||||||
|
targets.forEach((target, index) => {
|
||||||
|
const effect = subtractFractions(target, included[index]);
|
||||||
|
economicInclusionMoney(effect);
|
||||||
|
effects.push({
|
||||||
|
id: identity + ':' + index,
|
||||||
|
topic: 'incomeCorrections',
|
||||||
|
amount: fraction(
|
||||||
|
BigInt(effect.numerator) < 0n
|
||||||
|
? -BigInt(effect.numerator)
|
||||||
|
: BigInt(effect.numerator),
|
||||||
|
BigInt(effect.denominator),
|
||||||
|
),
|
||||||
|
effect,
|
||||||
|
reason: `${economicInclusionVersion}; CIRPC64(1–2)/53(1)a; ${index ? 'acréscimo nominal' : 'lucros'}: alvo ${economicInclusionMoney(target)}; já considerado ${economicInclusionMoney(included[index])}; diferença ${economicInclusionMoney(effect)}. ${row.reference}${specialReference ? ' ' + specialReference : ''}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
pending.push(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: economicInclusionVersion,
|
||||||
|
input: structuredClone(input),
|
||||||
|
effects,
|
||||||
|
pending,
|
||||||
|
net: addFractions(effects.flatMap((e) => (e.effect ? [e.effect] : []))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function prepareEconomicInclusion(
|
||||||
|
book: NormalBook,
|
||||||
|
row: EconomicInclusion,
|
||||||
|
): NormalBook {
|
||||||
|
if (
|
||||||
|
book.special ||
|
||||||
|
book.periodsFx ||
|
||||||
|
(book.schedules && book.schedules.period.kind !== 'calendar') ||
|
||||||
|
book.scope !== 'resident_general' ||
|
||||||
|
book.year !== '2026' ||
|
||||||
|
!book.base ||
|
||||||
|
!book.profit
|
||||||
|
)
|
||||||
|
throw Error(
|
||||||
|
'Prepare primeiro o lucro e a base L6 ordinária do sujeito/ano civil2026/MZN; períodos especiais e conversões têm correspondência própria.',
|
||||||
|
);
|
||||||
|
const old = book.economicInclusions ?? { version: 1 as const, rows: [] };
|
||||||
|
const rows = old.rows.filter(
|
||||||
|
(p) =>
|
||||||
|
economicDistributionIdentity(p.source) !==
|
||||||
|
economicDistributionIdentity(row.source),
|
||||||
|
);
|
||||||
|
const next: EconomicInclusions = {
|
||||||
|
version: 1,
|
||||||
|
rows: [...rows, structuredClone(row)],
|
||||||
|
};
|
||||||
|
const previous = calculateEconomicInclusions(
|
||||||
|
old,
|
||||||
|
book.base.subjectNuit,
|
||||||
|
book.year,
|
||||||
|
),
|
||||||
|
prepared = calculateEconomicInclusions(
|
||||||
|
next,
|
||||||
|
book.base.subjectNuit,
|
||||||
|
book.year,
|
||||||
|
);
|
||||||
|
if (previous.pending.length || prepared.pending.length)
|
||||||
|
throw Error([...previous.pending, ...prepared.pending].join(' '));
|
||||||
|
const topic = book.profit.topics.incomeCorrections;
|
||||||
|
if (
|
||||||
|
!topic.reference.trim() ||
|
||||||
|
!['known', 'absent'].includes(topic.state) ||
|
||||||
|
(topic.state === 'absent' && topic.amount.trim())
|
||||||
|
)
|
||||||
|
throw Error(
|
||||||
|
'Reconcilie as outras correcções aos proveitos antes de substituir parcelas.',
|
||||||
|
);
|
||||||
|
const current =
|
||||||
|
topic.state === 'absent' ? fraction(0n) : creditMoney(topic.amount);
|
||||||
|
const remaining = subtractFractions(
|
||||||
|
current,
|
||||||
|
addFractions(
|
||||||
|
previous.effects.flatMap((e) => (e.amount ? [e.amount] : [])),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (BigInt(remaining.numerator) < 0n)
|
||||||
|
throw Error(
|
||||||
|
'Montante de correcções inferior às parcelas anteriores; compare antes de substituir.',
|
||||||
|
);
|
||||||
|
const amount = addFractions([
|
||||||
|
remaining,
|
||||||
|
addFractions(
|
||||||
|
prepared.effects.flatMap((e) => (e.amount ? [e.amount] : [])),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
...book,
|
||||||
|
economicInclusions: next,
|
||||||
|
profit: {
|
||||||
|
...book.profit,
|
||||||
|
topics: {
|
||||||
|
...book.profit.topics,
|
||||||
|
incomeCorrections: {
|
||||||
|
state: 'known',
|
||||||
|
amount: economicInclusionMoney(amount),
|
||||||
|
reference: topic.reference.includes(row.reference)
|
||||||
|
? topic.reference
|
||||||
|
: topic.reference + '\n' + row.reference,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -155,6 +155,17 @@ export function closeNormalProfit(
|
|||||||
schedules?: SchedulesModel,
|
schedules?: SchedulesModel,
|
||||||
costs?: NormalCostsModel,
|
costs?: NormalCostsModel,
|
||||||
depreciation?: NormalDepreciationModel,
|
depreciation?: NormalDepreciationModel,
|
||||||
|
economic?: {
|
||||||
|
effects: {
|
||||||
|
id: string;
|
||||||
|
topic: string;
|
||||||
|
amount: FiscalFraction | null;
|
||||||
|
effect: FiscalFraction | null;
|
||||||
|
reason: string;
|
||||||
|
}[];
|
||||||
|
net: FiscalFraction;
|
||||||
|
pending: string[];
|
||||||
|
},
|
||||||
): NormalProfitModel {
|
): NormalProfitModel {
|
||||||
validateNormalProfit(input);
|
validateNormalProfit(input);
|
||||||
const pending: string[] = [],
|
const pending: string[] = [],
|
||||||
@@ -291,9 +302,10 @@ export function closeNormalProfit(
|
|||||||
const mapped = [
|
const mapped = [
|
||||||
...(schedules?.effects ?? []),
|
...(schedules?.effects ?? []),
|
||||||
...(costs?.effects ?? []),
|
...(costs?.effects ?? []),
|
||||||
|
...(economic?.effects ?? []),
|
||||||
].filter((e) => e.topic === key);
|
].filter((e) => e.topic === key);
|
||||||
const covered =
|
const covered =
|
||||||
(schedules || costs) &&
|
(schedules || costs || economic) &&
|
||||||
mapped.length > 0 &&
|
mapped.length > 0 &&
|
||||||
mapped.every((e) => e.effect !== null) &&
|
mapped.every((e) => e.effect !== null) &&
|
||||||
value !== null &&
|
value !== null &&
|
||||||
@@ -324,6 +336,7 @@ export function closeNormalProfit(
|
|||||||
}
|
}
|
||||||
if (schedules) pending.push(...schedules.pending);
|
if (schedules) pending.push(...schedules.pending);
|
||||||
if (costs) pending.push(...costs.pending);
|
if (costs) pending.push(...costs.pending);
|
||||||
|
if (economic) pending.push(...economic.pending);
|
||||||
if (depreciation && !schedules) pending.push(...depreciation.pending);
|
if (depreciation && !schedules) pending.push(...depreciation.pending);
|
||||||
return {
|
return {
|
||||||
input: structuredClone(input),
|
input: structuredClone(input),
|
||||||
@@ -335,6 +348,7 @@ export function closeNormalProfit(
|
|||||||
? addFractions([
|
? addFractions([
|
||||||
accounting,
|
accounting,
|
||||||
additions,
|
additions,
|
||||||
|
...(economic ? [economic.net] : []),
|
||||||
...(schedules ? [schedules.net] : []),
|
...(schedules ? [schedules.net] : []),
|
||||||
...(costs ? [costs.net] : []),
|
...(costs ? [costs.net] : []),
|
||||||
...(depreciation && !schedules && depreciation.adjustment
|
...(depreciation && !schedules && depreciation.adjustment
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { inputText } from './input-text.ts';
|
import { inputText } from './input-text.ts';
|
||||||
|
import {
|
||||||
|
calculateEconomicInclusions,
|
||||||
|
validateEconomicInclusions,
|
||||||
|
type EconomicInclusions,
|
||||||
|
} from './irpc-normal-economic.ts';
|
||||||
import { irpcSpecialCombinationsVersion } from './irpc-special-combinations.ts';
|
import { irpcSpecialCombinationsVersion } from './irpc-special-combinations.ts';
|
||||||
import {
|
import {
|
||||||
irpcSpecialVersion,
|
irpcSpecialVersion,
|
||||||
@@ -104,6 +109,7 @@ export type NormalAdjustment = {
|
|||||||
reference: string;
|
reference: string;
|
||||||
};
|
};
|
||||||
export type NormalBook = {
|
export type NormalBook = {
|
||||||
|
economicInclusions?: EconomicInclusions;
|
||||||
version: 1;
|
version: 1;
|
||||||
year: string;
|
year: string;
|
||||||
mappingReference: string;
|
mappingReference: string;
|
||||||
@@ -123,6 +129,7 @@ export type NormalBook = {
|
|||||||
history?: NormalRecord[];
|
history?: NormalRecord[];
|
||||||
};
|
};
|
||||||
export type NormalModel = {
|
export type NormalModel = {
|
||||||
|
economicInclusions?: ReturnType<typeof calculateEconomicInclusions>;
|
||||||
special?: SpecialModel;
|
special?: SpecialModel;
|
||||||
formPreparation?: NormalFormPreparationModel;
|
formPreparation?: NormalFormPreparationModel;
|
||||||
periodsFx?: NormalPeriodFxModel;
|
periodsFx?: NormalPeriodFxModel;
|
||||||
@@ -213,6 +220,8 @@ function store(d: Record<string, unknown>) {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
export function validateNormal(b: NormalBook) {
|
export function validateNormal(b: NormalBook) {
|
||||||
|
if (b?.economicInclusions !== undefined)
|
||||||
|
validateEconomicInclusions(b.economicInclusions);
|
||||||
if (b?.special !== undefined) validateSpecial(b.special);
|
if (b?.special !== undefined) validateSpecial(b.special);
|
||||||
if (b?.forms !== undefined) validateNormalFormPreparation(b.forms);
|
if (b?.forms !== undefined) validateNormalFormPreparation(b.forms);
|
||||||
if (b?.periodsFx !== undefined) validateNormalPeriodFx(b.periodsFx);
|
if (b?.periodsFx !== undefined) validateNormalPeriodFx(b.periodsFx);
|
||||||
@@ -333,6 +342,7 @@ function key(d: Record<string, unknown>, b: NormalBook) {
|
|||||||
b.scopeReference,
|
b.scopeReference,
|
||||||
b.rows,
|
b.rows,
|
||||||
...(b.profit ? [b.profit] : []),
|
...(b.profit ? [b.profit] : []),
|
||||||
|
...(b.economicInclusions ? [b.economicInclusions] : []),
|
||||||
...(b.base ? [b.base] : []),
|
...(b.base ? [b.base] : []),
|
||||||
...(b.assessment ? [b.assessment] : []),
|
...(b.assessment ? [b.assessment] : []),
|
||||||
...(b.schedules ? [b.schedules] : []),
|
...(b.schedules ? [b.schedules] : []),
|
||||||
@@ -569,6 +579,23 @@ export function calculateNormal(
|
|||||||
!!periodsFx?.short,
|
!!periodsFx?.short,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const economicInclusions = b.economicInclusions
|
||||||
|
? calculateEconomicInclusions(
|
||||||
|
b.economicInclusions,
|
||||||
|
b.base?.subjectNuit ?? '',
|
||||||
|
b.year,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
if (
|
||||||
|
economicInclusions &&
|
||||||
|
(b.special ||
|
||||||
|
b.periodsFx ||
|
||||||
|
(b.schedules && b.schedules.period.kind !== 'calendar') ||
|
||||||
|
b.scope !== 'resident_general')
|
||||||
|
)
|
||||||
|
economicInclusions.pending.push(
|
||||||
|
'Inclusão económica automática delimitada à base ordinária residente/ano civil2026/MZN; este âmbito exige correspondência própria.',
|
||||||
|
);
|
||||||
const profit = b.profit
|
const profit = b.profit
|
||||||
? closeNormalProfit(
|
? closeNormalProfit(
|
||||||
b.profit,
|
b.profit,
|
||||||
@@ -580,6 +607,7 @@ export function calculateNormal(
|
|||||||
schedules,
|
schedules,
|
||||||
costs,
|
costs,
|
||||||
depreciation,
|
depreciation,
|
||||||
|
economicInclusions,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
if (profit && periodsFx)
|
if (profit && periodsFx)
|
||||||
@@ -677,6 +705,7 @@ export function calculateNormal(
|
|||||||
? addFractions([accountingProfit, additions])
|
? addFractions([accountingProfit, additions])
|
||||||
: null,
|
: null,
|
||||||
taxableProfit: profit?.taxableProfit ?? null,
|
taxableProfit: profit?.taxableProfit ?? null,
|
||||||
|
...(economicInclusions ? { economicInclusions } : {}),
|
||||||
taxableBase: base?.taxableBase ?? null,
|
taxableBase: base?.taxableBase ?? null,
|
||||||
tax: null,
|
tax: null,
|
||||||
rows,
|
rows,
|
||||||
@@ -774,6 +803,18 @@ export function normalReportRows(
|
|||||||
if (!r) return [];
|
if (!r) return [];
|
||||||
const m = r.model;
|
const m = r.model;
|
||||||
return [
|
return [
|
||||||
|
...(m.economicInclusions?.effects.map(
|
||||||
|
(e) =>
|
||||||
|
[
|
||||||
|
'IRPC normal — L6',
|
||||||
|
'Inclusão/correcção económica — ' + e.id,
|
||||||
|
'Parcela reconciliada: ' +
|
||||||
|
money(e.amount) +
|
||||||
|
'; efeito com sinal: ' +
|
||||||
|
money(e.effect),
|
||||||
|
e.reason,
|
||||||
|
] as [string, string, string, string],
|
||||||
|
) ?? []),
|
||||||
[
|
[
|
||||||
'IRPC normal — L6',
|
'IRPC normal — L6',
|
||||||
'Ajustamentos ao resultado financeiro',
|
'Ajustamentos ao resultado financeiro',
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?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 IrpcEconomicAssociationDraftTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function test_economic_association_uses_the_existing_private_draft_writer(): void
|
||||||
|
{
|
||||||
|
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||||
|
$this->assertSame('proactive_investment_20261027_test', $target->name);
|
||||||
|
$this->assertSame(55441, (int) $target->port);
|
||||||
|
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||||
|
$this->actingAs(User::findOrFail(1021));
|
||||||
|
$d = json_decode(file_get_contents(base_path('outputs/irpc-economic-association-20261110/fixture.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
$personal = ['person_type' => $d['person_type']];
|
||||||
|
unset($d['person_type']);
|
||||||
|
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => $personal, 'economic_snapshot' => $d], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||||
|
$back = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
|
||||||
|
$this->assertSame($d['modelo_a_snapshot'], $back['modelo_a_snapshot']);
|
||||||
|
$this->assertSame($d['business_plan_snapshot'], $back['business_plan_snapshot']);
|
||||||
|
$b = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$d['context_opportunity']];
|
||||||
|
$this->assertSame('proportional', $b['economicCredits']['rows'][0]['nationalMethod']);
|
||||||
|
$this->assertSame(['numerator' => '896', 'denominator' => '25'], $b['result']['model']['economicCredit']['rows'][0]['nationalLimit']);
|
||||||
|
$this->assertSame(['numerator' => '688', 'denominator' => '1'], $b['result']['model']['arithmeticBalance']);
|
||||||
|
$this->assertSame('participation', $b['result']['model']['economicCredit']['rows'][0]['association']['kind']);
|
||||||
|
file_put_contents(base_path('outputs/irpc-economic-association-20261110/recovered.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?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 IrpcEconomicInclusionDraftTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function test_economic_inclusion_uses_the_existing_private_draft_writer(): void
|
||||||
|
{
|
||||||
|
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||||
|
$this->assertSame('proactive_investment_20261027_test', $target->name);
|
||||||
|
$this->assertSame(55441, (int) $target->port);
|
||||||
|
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||||
|
$this->actingAs(User::findOrFail(1021));
|
||||||
|
$d = json_decode(file_get_contents(base_path('outputs/irpc-economic-inclusion-20261110/fixture.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
$personal = ['person_type' => $d['person_type']];
|
||||||
|
unset($d['person_type']);
|
||||||
|
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => $personal, 'economic_snapshot' => $d], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||||
|
$back = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
|
||||||
|
$this->assertSame($d['modelo_a_snapshot'], $back['modelo_a_snapshot']);
|
||||||
|
$this->assertSame($d['business_plan_snapshot'], $back['business_plan_snapshot']);
|
||||||
|
$b = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcNormal'][$d['context_opportunity']];
|
||||||
|
$this->assertSame('IRPC-economic-inclusion/1', $b['result']['model']['economicInclusions']['version']);
|
||||||
|
$this->assertSame(['numerator' => '112', 'denominator' => '1'], $b['result']['model']['economicInclusions']['net']);
|
||||||
|
$this->assertSame(['numerator' => '36096', 'denominator' => '25'], $b['result']['model']['colecta']);
|
||||||
|
$this->assertNotEmpty($b['history']);
|
||||||
|
file_put_contents(base_path('outputs/irpc-economic-inclusion-20261110/recovered.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?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 IrpcEconomicLiquidationDraftTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function test_economic_liquidation_uses_the_existing_private_draft_writer(): void
|
||||||
|
{
|
||||||
|
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||||
|
$this->assertSame('proactive_investment_20261027_test', $target->name);
|
||||||
|
$this->assertSame(55441, (int) $target->port);
|
||||||
|
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||||
|
$this->actingAs(User::findOrFail(1021));
|
||||||
|
$d = json_decode(file_get_contents(base_path('outputs/irpc-economic-liquidation-20261110/fixture.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
$personal = ['person_type' => $d['person_type']];
|
||||||
|
unset($d['person_type']);
|
||||||
|
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => $personal, 'economic_snapshot' => $d], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||||
|
$back = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
|
||||||
|
$this->assertSame($d['modelo_a_snapshot'], $back['modelo_a_snapshot']);
|
||||||
|
$this->assertSame($d['business_plan_snapshot'], $back['business_plan_snapshot']);
|
||||||
|
$b = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$d['context_opportunity']];
|
||||||
|
$this->assertSame('proportional', $b['economicCredits']['rows'][0]['nationalMethod']);
|
||||||
|
$this->assertSame(['numerator' => '896', 'denominator' => '25'], $b['result']['model']['economicCredit']['rows'][0]['nationalLimit']);
|
||||||
|
$this->assertSame(['numerator' => '688', 'denominator' => '1'], $b['result']['model']['arithmeticBalance']);
|
||||||
|
$this->assertSame(['numerator' => '50', 'denominator' => '1'], $b['result']['model']['economicCredit']['rows'][0]['liquidation']['excessCapitalGain']);
|
||||||
|
file_put_contents(base_path('outputs/irpc-economic-liquidation-20261110/recovered.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?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 IrpcEconomicQuotaDraftTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function test_derived_economic_quota_uses_the_existing_private_draft_writer(): void
|
||||||
|
{
|
||||||
|
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||||
|
$this->assertSame('proactive_investment_20261027_test', $target->name);
|
||||||
|
$this->assertSame(55441, (int) $target->port);
|
||||||
|
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||||
|
$this->actingAs(User::findOrFail(1021));
|
||||||
|
$d = json_decode(file_get_contents(base_path('outputs/irpc-economic-quota-20261110/fixture.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
$personal = ['person_type' => $d['person_type']];
|
||||||
|
unset($d['person_type']);
|
||||||
|
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => $personal, 'economic_snapshot' => $d], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||||
|
$back = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
|
||||||
|
$this->assertSame($d['modelo_a_snapshot'], $back['modelo_a_snapshot']);
|
||||||
|
$this->assertSame($d['business_plan_snapshot'], $back['business_plan_snapshot']);
|
||||||
|
$b = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$d['context_opportunity']];
|
||||||
|
$this->assertSame('proportional', $b['economicCredits']['rows'][0]['nationalMethod']);
|
||||||
|
$this->assertSame(['numerator' => '256', 'denominator' => '5'], $b['result']['model']['economicCredit']['rows'][0]['nationalLimit']);
|
||||||
|
$this->assertSame(['numerator' => '3244', 'denominator' => '5'], $b['result']['model']['arithmeticBalance']);
|
||||||
|
file_put_contents(base_path('outputs/irpc-economic-quota-20261110/recovered.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?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 IrpcEconomicSpecialInclusionDraftTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function test_economic_special_inclusion_uses_the_existing_private_draft_writer(): void
|
||||||
|
{
|
||||||
|
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||||
|
$this->assertSame('proactive_investment_20261027_test', $target->name);
|
||||||
|
$this->assertSame(55441, (int) $target->port);
|
||||||
|
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||||
|
$this->actingAs(User::findOrFail(1021));
|
||||||
|
$d = json_decode(file_get_contents(base_path('outputs/irpc-economic-special-inclusion-20261110/fixture.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
$personal = ['person_type' => $d['person_type']];
|
||||||
|
unset($d['person_type']);
|
||||||
|
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => $personal, 'economic_snapshot' => $d], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||||
|
$back = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
|
||||||
|
$this->assertSame($d['modelo_a_snapshot'], $back['modelo_a_snapshot']);
|
||||||
|
$this->assertSame($d['business_plan_snapshot'], $back['business_plan_snapshot']);
|
||||||
|
$b = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcNormal'][$d['context_opportunity']];
|
||||||
|
$this->assertSame('IRPC-economic-inclusion/1', $b['result']['model']['economicInclusions']['version']);
|
||||||
|
$this->assertSame(['numerator' => '162', 'denominator' => '1'], $b['result']['model']['economicInclusions']['net']);
|
||||||
|
$this->assertSame(['numerator' => '36496', 'denominator' => '25'], $b['result']['model']['colecta']);
|
||||||
|
$this->assertNotEmpty($b['history']);
|
||||||
|
file_put_contents(base_path('outputs/irpc-economic-special-inclusion-20261110/recovered.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { economicCase } from './helpers/irpc-economic-case.mjs';
|
||||||
|
import {
|
||||||
|
calculateAnnual,
|
||||||
|
recordAnnual,
|
||||||
|
saveAnnualBook,
|
||||||
|
annualBook,
|
||||||
|
annualCurrent,
|
||||||
|
annualHtml,
|
||||||
|
} from '../../resources/js/lib/irpc-annual-settlement.ts';
|
||||||
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||||
|
function associationCase(kind = 'participation', effectivelyTaxed = 'yes') {
|
||||||
|
const c = economicCase();
|
||||||
|
Object.assign(c.b.economicCredits.rows[0], {
|
||||||
|
kind: 'association',
|
||||||
|
association: {
|
||||||
|
kind,
|
||||||
|
effectivelyTaxed,
|
||||||
|
reference:
|
||||||
|
'SYNTHETIC QA contract and distributed income; documented adaptation64(4)',
|
||||||
|
},
|
||||||
|
nationalMethod: 'proportional',
|
||||||
|
nationalBasisReference:
|
||||||
|
'SYNTHETIC included100 plus nominal12 in current base4400',
|
||||||
|
});
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
void test('participation and quota use only the documented economic tax and preserve L6 and other deductions', () => {
|
||||||
|
for (const kind of ['participation', 'quota']) {
|
||||||
|
const c = associationCase(kind, kind === 'quota' ? '' : 'yes'),
|
||||||
|
before = c.d.modelo_a_snapshot,
|
||||||
|
m = calculateAnnual(c.d, c.b);
|
||||||
|
assert.deepEqual(m.pending, []);
|
||||||
|
assert.deepEqual(m.economicCredit.deducted, {
|
||||||
|
numerator: '12',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
assert.deepEqual(m.arithmeticBalance, {
|
||||||
|
numerator: '688',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
assert.equal(c.d.modelo_a_snapshot, before);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
void test('participation cannot infer effective taxation and unknown contract stays recoverable but indeterminate', () => {
|
||||||
|
for (const patch of [
|
||||||
|
{ kind: 'participation', effectivelyTaxed: '' },
|
||||||
|
{ kind: 'participation', effectivelyTaxed: 'no' },
|
||||||
|
{ kind: '', effectivelyTaxed: 'yes' },
|
||||||
|
{ reference: '' },
|
||||||
|
]) {
|
||||||
|
const c = associationCase();
|
||||||
|
Object.assign(c.b.economicCredits.rows[0].association, patch);
|
||||||
|
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
void test('association adaptation survives recovery and both report consumers without resetting old inputs', () => {
|
||||||
|
const c = associationCase(),
|
||||||
|
b = recordAnnual(c.d, c.b, '2026-11-10T16:10:00+02:00'),
|
||||||
|
d = JSON.parse(
|
||||||
|
JSON.stringify({
|
||||||
|
...c.d,
|
||||||
|
modelo_a_snapshot: saveAnnualBook(c.d, b),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
back = annualBook(d);
|
||||||
|
assert.equal(annualCurrent(d, back), true);
|
||||||
|
assert.deepEqual(
|
||||||
|
calculateAnnual(d, back).economicCredit,
|
||||||
|
back.result.model.economicCredit,
|
||||||
|
);
|
||||||
|
assert.match(annualHtml(d), /IRPC-economic-association\/1/);
|
||||||
|
assert.match(
|
||||||
|
JSON.stringify(
|
||||||
|
createProjectDossierReport(d, {
|
||||||
|
selectedSections: new Set(['finance']),
|
||||||
|
generatedAt: '2026-11-10T16:10:00+02:00',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
/IRPC-economic-association\/1/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -79,11 +79,7 @@ void test('article40 exclusion, payer exemption/foreign residence and special di
|
|||||||
const m = pending(c);
|
const m = pending(c);
|
||||||
assert.equal(m.internationalCredit.positions.length, 1);
|
assert.equal(m.internationalCredit.positions.length, 1);
|
||||||
assert.equal(number(m.internationalCredit.positions[0].eligible), 8);
|
assert.equal(number(m.internationalCredit.positions[0].eligible), 8);
|
||||||
if (
|
if (patch.article40 === 'yes')
|
||||||
patch.article40 === 'yes' ||
|
|
||||||
patch.kind === 'liquidation' ||
|
|
||||||
patch.kind === 'association'
|
|
||||||
)
|
|
||||||
assert.equal(m.economicCredit.rows[0].status, 'outside_scope');
|
assert.equal(m.economicCredit.rows[0].status, 'outside_scope');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { economicCase } from './helpers/irpc-economic-case.mjs';
|
||||||
|
import {
|
||||||
|
normalBook,
|
||||||
|
calculateNormal,
|
||||||
|
recordNormal,
|
||||||
|
saveNormalBook,
|
||||||
|
normalCurrent,
|
||||||
|
normalHtml,
|
||||||
|
} from '../../resources/js/lib/irpc-normal.ts';
|
||||||
|
import { calculateAnnual } from '../../resources/js/lib/irpc-annual-settlement.ts';
|
||||||
|
import {
|
||||||
|
prepareEconomicInclusion,
|
||||||
|
calculateEconomicInclusions,
|
||||||
|
} from '../../resources/js/lib/irpc-normal-economic.ts';
|
||||||
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||||
|
|
||||||
|
function caseInput() {
|
||||||
|
const c = economicCase();
|
||||||
|
const source = structuredClone(c.b.economicCredits.rows[0]);
|
||||||
|
Object.assign(source, {
|
||||||
|
incomeIncluded: 'no',
|
||||||
|
incomeReference: 'SYNTHETIC not yet included',
|
||||||
|
grossUpIncluded: '0',
|
||||||
|
grossUpReference: 'SYNTHETIC no gross-up yet',
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
normal: normalBook(c.d),
|
||||||
|
inclusion: {
|
||||||
|
source,
|
||||||
|
profitAlreadyIncluded: '0',
|
||||||
|
creditAlreadyIncluded: '0',
|
||||||
|
reference:
|
||||||
|
'SYNTHETIC QA complete reconciliation before this map, not a real accounting assertion',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
void test('economic inclusion adds only the missing profits and derived60percent to current L6', () => {
|
||||||
|
const c = caseInput(),
|
||||||
|
old = structuredClone(c.normal.result);
|
||||||
|
const b = prepareEconomicInclusion(c.normal, c.inclusion),
|
||||||
|
m = calculateNormal(c.d, b);
|
||||||
|
assert.deepEqual(m.economicInclusions.pending, []);
|
||||||
|
assert.deepEqual(m.economicInclusions.net, {
|
||||||
|
numerator: '112',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
assert.deepEqual(m.taxableProfit, { numerator: '6812', denominator: '1' });
|
||||||
|
assert.deepEqual(m.taxableBase, { numerator: '4512', denominator: '1' });
|
||||||
|
assert.deepEqual(m.colecta, { numerator: '36096', denominator: '25' });
|
||||||
|
assert.deepEqual(c.normal.result, old);
|
||||||
|
});
|
||||||
|
void test('economic inclusion is repeatable and replaces the same parcel instead of accumulating corrections', () => {
|
||||||
|
const c = caseInput(),
|
||||||
|
once = prepareEconomicInclusion(c.normal, c.inclusion);
|
||||||
|
assert.deepEqual(prepareEconomicInclusion(once, c.inclusion), once);
|
||||||
|
const alias = structuredClone(c.inclusion);
|
||||||
|
alias.source.distributionDocument =
|
||||||
|
' ' + alias.source.distributionDocument.toLowerCase() + ' ';
|
||||||
|
assert.equal(
|
||||||
|
prepareEconomicInclusion(once, alias).economicInclusions.rows.length,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const changed = structuredClone(c.inclusion);
|
||||||
|
changed.source.income = '200';
|
||||||
|
const next = prepareEconomicInclusion(once, changed);
|
||||||
|
assert.equal(next.economicInclusions.rows.length, 1);
|
||||||
|
assert.equal(next.profit.topics.incomeCorrections.amount, '212.00');
|
||||||
|
assert.deepEqual(calculateNormal(c.d, next).taxableProfit, {
|
||||||
|
numerator: '6912',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
void test('economic inclusion does not add amounts already considered and can correct documented over-inclusion', () => {
|
||||||
|
const c = economicCase(),
|
||||||
|
source = c.b.economicCredits.rows[0],
|
||||||
|
b = normalBook(c.d);
|
||||||
|
const row = {
|
||||||
|
source,
|
||||||
|
profitAlreadyIncluded: '100',
|
||||||
|
creditAlreadyIncluded: '12',
|
||||||
|
reference: 'SYNTHETIC already included once',
|
||||||
|
};
|
||||||
|
assert.deepEqual(
|
||||||
|
calculateNormal(c.d, prepareEconomicInclusion(b, row)).colecta,
|
||||||
|
b.result.model.colecta,
|
||||||
|
);
|
||||||
|
const correction = structuredClone(row);
|
||||||
|
correction.profitAlreadyIncluded = '150';
|
||||||
|
correction.creditAlreadyIncluded = '20';
|
||||||
|
correction.source.grossUpIncluded = '20';
|
||||||
|
const m = calculateNormal(c.d, prepareEconomicInclusion(b, correction));
|
||||||
|
assert.deepEqual(m.economicInclusions.net, {
|
||||||
|
numerator: '-58',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
assert.deepEqual(m.taxableProfit, { numerator: '6642', denominator: '1' });
|
||||||
|
});
|
||||||
|
void test('economic inclusion rejects contradictory prior inclusion, duplicate origins, different taxpayers and unsupported fractional cents', () => {
|
||||||
|
const c = caseInput();
|
||||||
|
const contradiction = structuredClone(c.inclusion);
|
||||||
|
contradiction.source.incomeIncluded = 'yes';
|
||||||
|
assert.throws(
|
||||||
|
() => prepareEconomicInclusion(c.normal, contradiction),
|
||||||
|
/declara os lucros/,
|
||||||
|
);
|
||||||
|
const duplicate = calculateEconomicInclusions(
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
rows: [
|
||||||
|
c.inclusion,
|
||||||
|
{
|
||||||
|
...c.inclusion,
|
||||||
|
source: { ...c.inclusion.source, id: 'another-id' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
c.normal.base.subjectNuit,
|
||||||
|
'2026',
|
||||||
|
);
|
||||||
|
assert.ok(duplicate.pending.some((e) => e.includes('repetida')));
|
||||||
|
const other = structuredClone(c.inclusion);
|
||||||
|
other.source.subjectNuit = '987654321';
|
||||||
|
assert.throws(() => prepareEconomicInclusion(c.normal, other));
|
||||||
|
const partial = structuredClone(c.inclusion);
|
||||||
|
partial.source.taxAmount = '20.01';
|
||||||
|
partial.source.proofAmount = '20.01';
|
||||||
|
assert.throws(() => prepareEconomicInclusion(c.normal, partial), /cêntimo/);
|
||||||
|
});
|
||||||
|
void test('economic inclusion keeps recovery, previous result and reports while making the old L8 origin stale', () => {
|
||||||
|
const c = caseInput(),
|
||||||
|
previous = structuredClone(c.normal.result);
|
||||||
|
const b = recordNormal(
|
||||||
|
c.d,
|
||||||
|
prepareEconomicInclusion(c.normal, c.inclusion),
|
||||||
|
'2026-11-10T15:00:00+02:00',
|
||||||
|
);
|
||||||
|
const d = JSON.parse(
|
||||||
|
JSON.stringify({ ...c.d, modelo_a_snapshot: saveNormalBook(c.d, b) }),
|
||||||
|
);
|
||||||
|
const recovered = normalBook(d);
|
||||||
|
assert.equal(normalCurrent(d, recovered), true);
|
||||||
|
assert.deepEqual(recovered.history.at(-1), previous);
|
||||||
|
assert.deepEqual(
|
||||||
|
calculateNormal(d, recovered).economicInclusions,
|
||||||
|
recovered.result.model.economicInclusions,
|
||||||
|
);
|
||||||
|
assert.match(normalHtml(d), /IRPC-economic-inclusion\/1/);
|
||||||
|
assert.match(
|
||||||
|
JSON.stringify(
|
||||||
|
createProjectDossierReport(d, {
|
||||||
|
selectedSections: new Set(['finance']),
|
||||||
|
generatedAt: '2026-11-10T15:00:00+02:00',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
/IRPC-economic-inclusion\/1/,
|
||||||
|
);
|
||||||
|
assert.equal(calculateAnnual(d, c.b).arithmeticBalance, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('special inclusion reconciles the whole taxable partilha once but keeps only its eligible component in the credit', () => {
|
||||||
|
const c = caseInput();
|
||||||
|
Object.assign(c.inclusion.source, {
|
||||||
|
kind: 'liquidation',
|
||||||
|
income: '150',
|
||||||
|
liquidation: {
|
||||||
|
attributed: '200',
|
||||||
|
paidInCapital: '100',
|
||||||
|
reference:
|
||||||
|
'SYNTHETIC capital component100 and gain50 included in taxable income150',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const b = prepareEconomicInclusion(c.normal, c.inclusion),
|
||||||
|
m = calculateNormal(c.d, b);
|
||||||
|
assert.deepEqual(m.economicInclusions.net, {
|
||||||
|
numerator: '162',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
assert.deepEqual(m.taxableBase, { numerator: '4562', denominator: '1' });
|
||||||
|
assert.match(m.economicInclusions.effects[0].reason, /mais-valia 50.00/);
|
||||||
|
assert.deepEqual(prepareEconomicInclusion(b, c.inclusion), b);
|
||||||
|
const already = structuredClone(c.inclusion);
|
||||||
|
Object.assign(already, {
|
||||||
|
profitAlreadyIncluded: '150',
|
||||||
|
creditAlreadyIncluded: '12',
|
||||||
|
});
|
||||||
|
Object.assign(already.source, {
|
||||||
|
incomeIncluded: 'yes',
|
||||||
|
grossUpIncluded: '12',
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
calculateNormal(c.d, prepareEconomicInclusion(c.normal, already))
|
||||||
|
.colecta,
|
||||||
|
c.normal.result.model.colecta,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
void test('association inclusion requires its specific evidence and does not infer effective taxation', () => {
|
||||||
|
const c = caseInput();
|
||||||
|
Object.assign(c.inclusion.source, {
|
||||||
|
kind: 'association',
|
||||||
|
association: {
|
||||||
|
kind: 'participation',
|
||||||
|
effectivelyTaxed: '',
|
||||||
|
reference: 'SYNTHETIC contract',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.throws(
|
||||||
|
() => prepareEconomicInclusion(c.normal, c.inclusion),
|
||||||
|
/efectivamente tributados/,
|
||||||
|
);
|
||||||
|
c.inclusion.source.association.effectivelyTaxed = 'yes';
|
||||||
|
assert.deepEqual(
|
||||||
|
calculateNormal(c.d, prepareEconomicInclusion(c.normal, c.inclusion))
|
||||||
|
.economicInclusions.net,
|
||||||
|
{ numerator: '112', denominator: '1' },
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { economicCase } from './helpers/irpc-economic-case.mjs';
|
||||||
|
import { economicIncome } from '../../resources/js/lib/irpc-economic-credit.ts';
|
||||||
|
import {
|
||||||
|
calculateAnnual,
|
||||||
|
recordAnnual,
|
||||||
|
saveAnnualBook,
|
||||||
|
annualBook,
|
||||||
|
annualCurrent,
|
||||||
|
annualHtml,
|
||||||
|
} from '../../resources/js/lib/irpc-annual-settlement.ts';
|
||||||
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||||
|
export function liquidationCase() {
|
||||||
|
const c = economicCase();
|
||||||
|
Object.assign(c.b.economicCredits.rows[0], {
|
||||||
|
kind: 'liquidation',
|
||||||
|
income: '150',
|
||||||
|
liquidation: {
|
||||||
|
attributed: '200',
|
||||||
|
paidInCapital: '100',
|
||||||
|
reference:
|
||||||
|
'SYNTHETIC QA partilha: capital income150 documented; eligible100 and excess50 separately classified',
|
||||||
|
},
|
||||||
|
nationalMethod: 'proportional',
|
||||||
|
nationalBasisReference:
|
||||||
|
'SYNTHETIC eligible100 plus nominal12 in ordinary base4400',
|
||||||
|
incomeReference:
|
||||||
|
'SYNTHETIC150 included in current L6, eligible100 and capital gain50 distinguished',
|
||||||
|
});
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
void test('liquidation caps the economic-credit component and separates capital gain without changing L6', () => {
|
||||||
|
const c = liquidationCase(),
|
||||||
|
before = c.d.modelo_a_snapshot,
|
||||||
|
m = calculateAnnual(c.d, c.b),
|
||||||
|
r = m.economicCredit.rows[0];
|
||||||
|
assert.deepEqual(m.pending, []);
|
||||||
|
assert.deepEqual(r.liquidation.eligibleIncome, {
|
||||||
|
numerator: '100',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
assert.deepEqual(r.liquidation.excessCapitalGain, {
|
||||||
|
numerator: '50',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
assert.deepEqual(r.nationalLimit, { numerator: '896', denominator: '25' });
|
||||||
|
assert.deepEqual(r.nominal, { numerator: '12', denominator: '1' });
|
||||||
|
assert.equal(c.d.modelo_a_snapshot, before);
|
||||||
|
});
|
||||||
|
void test('liquidation handles documented lower capital income and nonpositive capital ceiling', () => {
|
||||||
|
const c = liquidationCase(),
|
||||||
|
r = c.b.economicCredits.rows[0];
|
||||||
|
r.income = '40';
|
||||||
|
assert.deepEqual(economicIncome(r).income, {
|
||||||
|
numerator: '40',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
r.liquidation.paidInCapital = '250';
|
||||||
|
assert.deepEqual(economicIncome(r).income, {
|
||||||
|
numerator: '0',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
|
||||||
|
});
|
||||||
|
void test('liquidation rejects unknown evidence, attribution contradictions and stale collection', () => {
|
||||||
|
for (const patch of [
|
||||||
|
{ liquidation: undefined },
|
||||||
|
{
|
||||||
|
liquidation: {
|
||||||
|
attributed: '',
|
||||||
|
paidInCapital: '0',
|
||||||
|
reference: 'QA',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
liquidation: {
|
||||||
|
attributed: '100',
|
||||||
|
paidInCapital: '0',
|
||||||
|
reference: 'QA',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ assessmentKey: 'stale' },
|
||||||
|
]) {
|
||||||
|
const c = liquidationCase();
|
||||||
|
Object.assign(c.b.economicCredits.rows[0], patch);
|
||||||
|
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
void test('liquidation recovers the exact cap and excess in both report consumers, retaining old history', () => {
|
||||||
|
const c = liquidationCase(),
|
||||||
|
book = recordAnnual(c.d, c.b, '2026-11-10T16:00:00+02:00');
|
||||||
|
const d = JSON.parse(
|
||||||
|
JSON.stringify({
|
||||||
|
...c.d,
|
||||||
|
modelo_a_snapshot: saveAnnualBook(c.d, book),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
b = annualBook(d);
|
||||||
|
assert.equal(annualCurrent(d, b), true);
|
||||||
|
assert.deepEqual(
|
||||||
|
calculateAnnual(d, b).economicCredit,
|
||||||
|
b.result.model.economicCredit,
|
||||||
|
);
|
||||||
|
assert.match(annualHtml(d), /IRPC-economic-liquidation\/1/);
|
||||||
|
const report = JSON.stringify(
|
||||||
|
createProjectDossierReport(d, {
|
||||||
|
selectedSections: new Set(['finance']),
|
||||||
|
generatedAt: '2026-11-10T16:00:00+02:00',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.match(report, /IRPC-economic-liquidation\/1/);
|
||||||
|
assert.match(report, /mais-valia/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { economicCase } from './helpers/irpc-economic-case.mjs';
|
||||||
|
import {
|
||||||
|
calculateAnnual,
|
||||||
|
recordAnnual,
|
||||||
|
saveAnnualBook,
|
||||||
|
annualBook,
|
||||||
|
annualCurrent,
|
||||||
|
annualHtml,
|
||||||
|
} from '../../resources/js/lib/irpc-annual-settlement.ts';
|
||||||
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||||
|
|
||||||
|
function quotaCase() {
|
||||||
|
const c = economicCase();
|
||||||
|
Object.assign(c.b.economicCredits.rows[0], {
|
||||||
|
nationalMethod: 'proportional',
|
||||||
|
nationalBasisReference:
|
||||||
|
'SYNTHETIC QA: profits100 plus nominal60 already included and reconciled in current ordinary base4400',
|
||||||
|
taxAmount: '100',
|
||||||
|
proofAmount: '100',
|
||||||
|
grossUpIncluded: '60',
|
||||||
|
grossUpReference:
|
||||||
|
'SYNTHETIC QA: nominal60 already included; no base change',
|
||||||
|
});
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
void test('economic quota derives from profits plus the existing gross-up, without changing L6', () => {
|
||||||
|
const c = quotaCase(),
|
||||||
|
before = c.d.modelo_a_snapshot;
|
||||||
|
const m = calculateAnnual(c.d, c.b);
|
||||||
|
assert.deepEqual(m.pending, []);
|
||||||
|
assert.deepEqual(m.economicCredit.rows[0].nationalLimit, {
|
||||||
|
numerator: '256',
|
||||||
|
denominator: '5',
|
||||||
|
});
|
||||||
|
assert.deepEqual(m.economicCredit.deducted, {
|
||||||
|
numerator: '256',
|
||||||
|
denominator: '5',
|
||||||
|
});
|
||||||
|
assert.deepEqual(m.payable, { numerator: '3244', denominator: '5' });
|
||||||
|
assert.equal(c.d.modelo_a_snapshot, before);
|
||||||
|
assert.equal(m.economicCredit.rows[0].input.nationalLimit, '32');
|
||||||
|
});
|
||||||
|
void test('economic quota survives saving and recovery into both report consumers with its origin', () => {
|
||||||
|
const c = quotaCase();
|
||||||
|
const book = recordAnnual(c.d, c.b, '2026-11-10T14:00:00+02:00');
|
||||||
|
const d = JSON.parse(
|
||||||
|
JSON.stringify({
|
||||||
|
...c.d,
|
||||||
|
modelo_a_snapshot: saveAnnualBook(c.d, book),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const recovered = annualBook(d);
|
||||||
|
assert.equal(annualCurrent(d, recovered), true);
|
||||||
|
assert.deepEqual(
|
||||||
|
calculateAnnual(d, recovered).economicCredit,
|
||||||
|
recovered.result.model.economicCredit,
|
||||||
|
);
|
||||||
|
assert.match(annualHtml(d), /IRPC-economic-quota\/1/);
|
||||||
|
assert.match(
|
||||||
|
JSON.stringify(
|
||||||
|
createProjectDossierReport(d, {
|
||||||
|
selectedSections: new Set(['finance']),
|
||||||
|
generatedAt: '2026-11-10T14:00:00+02:00',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
/256\/5/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
void test('economic quota requires support, positive matched base and a still-current assessment', () => {
|
||||||
|
for (const patch of [
|
||||||
|
{ nationalBasisReference: '' },
|
||||||
|
{ income: '4500' },
|
||||||
|
{ assessmentKey: 'obsolete' },
|
||||||
|
{ grossUpIncluded: '0' },
|
||||||
|
]) {
|
||||||
|
const c = quotaCase();
|
||||||
|
Object.assign(c.b.economicCredits.rows[0], patch);
|
||||||
|
const m = calculateAnnual(c.d, c.b);
|
||||||
|
assert.equal(m.status, 'indeterminate');
|
||||||
|
assert.equal(m.arithmeticBalance, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
void test('economic quota defaults to the accepted documented path for old books', () => {
|
||||||
|
const c = economicCase();
|
||||||
|
const m = calculateAnnual(c.d, c.b);
|
||||||
|
assert.equal(m.economicCredit.rows[0].nationalDerivation, undefined);
|
||||||
|
assert.deepEqual(m.economicCredit.rows[0].nationalLimit, {
|
||||||
|
numerator: '32',
|
||||||
|
denominator: '1',
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import { economicCase } from '../tests/frontend/helpers/irpc-economic-case.mjs';
|
||||||
|
import { recordAnnual, saveAnnualBook, annualBook, calculateAnnual, annualHtml } from '../resources/js/lib/irpc-annual-settlement.ts';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
const out = new URL('../outputs/irpc-economic-association-20261110/', import.meta.url);
|
||||||
|
fs.mkdirSync(out, { recursive: true });
|
||||||
|
if (process.argv[2] === 'recovered') {
|
||||||
|
const d = JSON.parse(fs.readFileSync(new URL('recovered.json', out), 'utf8'));
|
||||||
|
const b = annualBook(d), m = b.result.model;
|
||||||
|
assert.deepEqual(calculateAnnual(d, b).economicCredit, m.economicCredit);
|
||||||
|
const html = annualHtml(d);assert.match(html, /IRPC-economic-association\/1/);
|
||||||
|
fs.writeFileSync(new URL('report.html', out), html);
|
||||||
|
fs.writeFileSync(new URL('verification.json', out), JSON.stringify({ synthetic: true, apiRecovered: true, currentRecalculation: true, html: true, association: m.economicCredit.rows[0].association, quota: m.economicCredit.rows[0].nationalLimit, balance: m.arithmeticBalance, activeChanged: false }, null, 2));
|
||||||
|
} else {
|
||||||
|
const c = economicCase();
|
||||||
|
Object.assign(c.b.economicCredits.rows[0], {kind:'association',association:{kind:'participation',effectivelyTaxed:'yes',reference:'SYNTHETIC QA contract and effective taxation of distributed income; documented adaptation64(4)'},nationalMethod:'proportional',nationalBasisReference:'SYNTHETIC current base includes100 plus nominal12'});
|
||||||
|
const b = recordAnnual(c.d, c.b, '2026-11-10T14:00:00+02:00');
|
||||||
|
fs.writeFileSync(new URL('fixture.json', out), JSON.stringify({ ...c.d, modelo_a_snapshot: saveAnnualBook(c.d, b) }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { reuseAnnualAssessment, calculateAnnual, recordAnnual, saveAnnualBook, annualBook, annualCurrent, annualHtml } from '../resources/js/lib/irpc-annual-settlement.ts';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { economicCase } from '../tests/frontend/helpers/irpc-economic-case.mjs';
|
||||||
|
import { normalBook, calculateNormal, recordNormal, saveNormalBook, normalCurrent, normalHtml } from '../resources/js/lib/irpc-normal.ts';
|
||||||
|
import { prepareEconomicInclusion } from '../resources/js/lib/irpc-normal-economic.ts';
|
||||||
|
import { createProjectDossierReport } from '../resources/js/lib/project-dossier-report.ts';
|
||||||
|
const special = process.argv.includes('special');
|
||||||
|
const out = new URL(special ? '../outputs/irpc-economic-special-inclusion-20261110/' : '../outputs/irpc-economic-inclusion-20261110/', import.meta.url);
|
||||||
|
fs.mkdirSync(out, { recursive: true });
|
||||||
|
if (process.argv[2] === 'recovered') {
|
||||||
|
const d = JSON.parse(fs.readFileSync(new URL('recovered.json', out), 'utf8'));
|
||||||
|
const b = normalBook(d), m = b.result.model;
|
||||||
|
assert.equal(normalCurrent(d,b),true);
|
||||||
|
assert.deepEqual(calculateNormal(d,b).economicInclusions,m.economicInclusions);
|
||||||
|
assert.equal(b.history.length > 0,true);
|
||||||
|
const html = normalHtml(d), report = createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-11-10T15:00:00+02:00'});
|
||||||
|
assert.match(html,/IRPC-economic-inclusion\/1/);
|
||||||
|
assert.match(JSON.stringify(report),/IRPC-economic-inclusion\/1/);
|
||||||
|
const annual=annualBook(d), final=calculateAnnual(d,annual);
|
||||||
|
assert.equal(annualCurrent(d,annual),true);
|
||||||
|
assert.deepEqual(final.pending,[]);
|
||||||
|
assert.deepEqual(final.economicCredit,annual.result.model.economicCredit);
|
||||||
|
assert.match(annualHtml(d),/IRPC-economic-quota\/1/);
|
||||||
|
fs.writeFileSync(new URL('annual-report.html',out),annualHtml(d));
|
||||||
|
fs.writeFileSync(new URL('report.html',out),html);
|
||||||
|
fs.writeFileSync(new URL('verification.json',out),JSON.stringify({synthetic:true,apiRecovered:true,currentRecalculation:true,historyPreserved:true,html:true,dossierReport:true,effect:m.economicInclusions.net,taxableProfit:m.taxableProfit,taxableBase:m.taxableBase,colecta:m.colecta,annualContinued:true,quota:final.economicCredit.rows[0].nationalLimit,balance:final.arithmeticBalance,activeChanged:false},null,2));
|
||||||
|
} else {
|
||||||
|
const c=economicCase(), source=structuredClone(c.b.economicCredits.rows[0]);
|
||||||
|
Object.assign(source,{incomeIncluded:'no',incomeReference:'SYNTHETIC not yet included',grossUpIncluded:'0',grossUpReference:'SYNTHETIC no gross-up yet'});
|
||||||
|
if (special) Object.assign(source,{kind:'liquidation',income:'150',liquidation:{attributed:'200',paidInCapital:'100',reference:'SYNTHETIC QA capital100/gain50 in total taxable150; exact reconciliation'}});
|
||||||
|
const b=recordNormal(c.d,prepareEconomicInclusion(normalBook(c.d),{source,profitAlreadyIncluded:'0',creditAlreadyIncluded:'0',reference:'SYNTHETIC QA explicit reconciliation before this map'}),'2026-11-10T15:00:00+02:00');
|
||||||
|
const d={...c.d,modelo_a_snapshot:saveNormalBook(c.d,b)};
|
||||||
|
let annual=reuseAnnualAssessment(d,c.b);
|
||||||
|
annual.internationalCredits.positions.forEach(p=>{p.assessmentKey=annual.assessmentKey;p.incomeReference='SYNTHETIC reconfirmed foreign component in new L6';});
|
||||||
|
Object.assign(annual.economicCredits.rows[0],source,{assessmentKey:annual.assessmentKey,incomeIncluded:'yes',incomeReference:'SYNTHETIC reconciled inclusion map in new L6',grossUpIncluded:'12',grossUpReference:'SYNTHETIC reconciled nominal12 in new L6',nationalMethod:'proportional',nationalBasisReference:'SYNTHETIC same112 component in new base4512'});
|
||||||
|
assert.deepEqual(calculateAnnual(d,annual).pending,[]);
|
||||||
|
annual=recordAnnual(d,annual,'2026-11-10T15:01:00+02:00');
|
||||||
|
d.modelo_a_snapshot=saveAnnualBook(d,annual);
|
||||||
|
fs.writeFileSync(new URL('fixture.json',out),JSON.stringify(d));
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import { economicCase } from '../tests/frontend/helpers/irpc-economic-case.mjs';
|
||||||
|
import { recordAnnual, saveAnnualBook, annualBook, calculateAnnual, annualHtml } from '../resources/js/lib/irpc-annual-settlement.ts';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
const out = new URL('../outputs/irpc-economic-liquidation-20261110/', import.meta.url);
|
||||||
|
fs.mkdirSync(out, { recursive: true });
|
||||||
|
if (process.argv[2] === 'recovered') {
|
||||||
|
const d = JSON.parse(fs.readFileSync(new URL('recovered.json', out), 'utf8'));
|
||||||
|
const b = annualBook(d), m = b.result.model;
|
||||||
|
assert.deepEqual(calculateAnnual(d, b).economicCredit, m.economicCredit);
|
||||||
|
const html = annualHtml(d);assert.match(html, /IRPC-economic-liquidation\/1/);
|
||||||
|
fs.writeFileSync(new URL('report.html', out), html);
|
||||||
|
fs.writeFileSync(new URL('verification.json', out), JSON.stringify({ synthetic: true, apiRecovered: true, currentRecalculation: true, html: true, liquidation: m.economicCredit.rows[0].liquidation, quota: m.economicCredit.rows[0].nationalLimit, balance: m.arithmeticBalance, activeChanged: false }, null, 2));
|
||||||
|
} else {
|
||||||
|
const c = economicCase();
|
||||||
|
Object.assign(c.b.economicCredits.rows[0], {kind:'liquidation',income:'150',liquidation:{attributed:'200',paidInCapital:'100',reference:'SYNTHETIC QA partilha; documented capital-income150, eligible100 and gain50; L6 already reconciled'},nationalMethod:'proportional',nationalBasisReference:'SYNTHETIC current base includes eligible100 plus nominal12'});
|
||||||
|
const b = recordAnnual(c.d, c.b, '2026-11-10T14:00:00+02:00');
|
||||||
|
fs.writeFileSync(new URL('fixture.json', out), JSON.stringify({ ...c.d, modelo_a_snapshot: saveAnnualBook(c.d, b) }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$root = dirname(__DIR__);
|
||||||
|
if (($argv[1] ?? '') !== 'test' || basename($root) !== 'portal-investidor') {
|
||||||
|
throw new RuntimeException('Explicit local test operation required.');
|
||||||
|
}
|
||||||
|
foreach (['APP_ENV' => 'testing', 'APP_KEY' => 'base64:'.base64_encode(str_repeat('Q', 32)),
|
||||||
|
'APP_CONFIG_CACHE' => $root.'/outputs/irpc-national-fraction-20261110/no-config.php',
|
||||||
|
'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441',
|
||||||
|
'DB_DATABASE' => 'proactive_investment_20261027_test', 'DB_USERNAME' => 'investmoz_test', 'DB_PASSWORD' => '',
|
||||||
|
'DB_URL' => '', 'DATABASE_URL' => '', 'CACHE_STORE' => 'array', 'SESSION_DRIVER' => 'array',
|
||||||
|
'QUEUE_CONNECTION' => 'sync', 'MAIL_MAILER' => 'array', 'SUITECRM_ENABLED' => 'false'] as $key => $value) {
|
||||||
|
putenv($key.'='.$value);
|
||||||
|
$_ENV[$key] = $_SERVER[$key] = $value;
|
||||||
|
}
|
||||||
|
$pdo = new PDO('pgsql:host=127.0.0.1;port=55441;dbname=proactive_investment_20261027_test', 'investmoz_test', '');
|
||||||
|
$identity = $pdo->query('select current_database() as name,inet_server_port() as port')->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($identity['name'] !== 'proactive_investment_20261027_test' || (int) $identity['port'] !== 55441) {
|
||||||
|
throw new RuntimeException('Isolated QA identity required.');
|
||||||
|
}
|
||||||
|
$case = ['quota' => 'IrpcEconomicQuotaDraftTest', 'inclusion' => 'IrpcEconomicInclusionDraftTest', 'liquidation' => 'IrpcEconomicLiquidationDraftTest', 'association' => 'IrpcEconomicAssociationDraftTest', 'special-inclusion' => 'IrpcEconomicSpecialInclusionDraftTest'][$argv[2] ?? ''] ?? throw new RuntimeException('Choose quota, inclusion, liquidation or association explicitly.');
|
||||||
|
$argv = ['phpunit', '-c', 'phpunit.investmoz.xml', '--do-not-cache-result', 'tests/Feature/'.$case.'.php'];
|
||||||
|
$argc = count($argv);
|
||||||
|
$_SERVER['argv'] = $argv;
|
||||||
|
$_SERVER['argc'] = $argc;
|
||||||
|
require $root.'/vendor/autoload.php';
|
||||||
|
require $root.'/vendor/phpunit/phpunit/phpunit';
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import { economicCase } from '../tests/frontend/helpers/irpc-economic-case.mjs';
|
||||||
|
import { recordAnnual, saveAnnualBook, annualBook, calculateAnnual, annualHtml } from '../resources/js/lib/irpc-annual-settlement.ts';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
const out = new URL('../outputs/irpc-economic-quota-20261110/', import.meta.url);
|
||||||
|
fs.mkdirSync(out, { recursive: true });
|
||||||
|
if (process.argv[2] === 'recovered') {
|
||||||
|
const d = JSON.parse(fs.readFileSync(new URL('recovered.json', out), 'utf8'));
|
||||||
|
const b = annualBook(d), m = b.result.model;
|
||||||
|
assert.deepEqual(calculateAnnual(d, b).economicCredit, m.economicCredit);
|
||||||
|
const html = annualHtml(d);assert.match(html, /IRPC-economic-quota\/1/);
|
||||||
|
fs.writeFileSync(new URL('report.html', out), html);
|
||||||
|
fs.writeFileSync(new URL('verification.json', out), JSON.stringify({ synthetic: true, apiRecovered: true, currentRecalculation: true, html: true, quota: m.economicCredit.rows[0].nationalLimit, balance: m.arithmeticBalance, activeChanged: false }, null, 2));
|
||||||
|
} else {
|
||||||
|
const c = economicCase();
|
||||||
|
Object.assign(c.b.economicCredits.rows[0], { nationalMethod: 'proportional', nationalBasisReference: 'SYNTHETIC QA: profits100 plus nominal60 already reconciled in base4400', taxAmount: '100', proofAmount: '100', grossUpIncluded: '60', grossUpReference: 'SYNTHETIC QA: nominal60 already included, no automatic base change' });
|
||||||
|
const b = recordAnnual(c.d, c.b, '2026-11-10T14:00:00+02:00');
|
||||||
|
fs.writeFileSync(new URL('fixture.json', out), JSON.stringify({ ...c.d, modelo_a_snapshot: saveAnnualBook(c.d, b) }));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user