This commit is contained in:
@@ -1,5 +1,32 @@
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
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`.
|
||||
|
||||
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`
|
||||
|
||||
**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
|
||||
|
||||
**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 });
|
||||
const prefix = 'Crédito económico ' + (i + 1) + ' — ';
|
||||
const field = (
|
||||
key: Exclude<keyof EconomicPosition, 'previousUse'>,
|
||||
key: Exclude<
|
||||
keyof EconomicPosition,
|
||||
'previousUse' | 'liquidation' | 'association'
|
||||
>,
|
||||
label: string,
|
||||
) => (
|
||||
<label key={key} className="block">
|
||||
@@ -145,9 +148,7 @@ export function SimulationIrpcEconomic({
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
const outside =
|
||||
(!!r.kind && r.kind !== 'ordinary_profit') ||
|
||||
r.article40 === 'yes';
|
||||
const outside = r.article40 === 'yes';
|
||||
const detail = (
|
||||
<>
|
||||
{(
|
||||
@@ -226,7 +227,60 @@ export function SimulationIrpcEconomic({
|
||||
'Suporte da quota proporcional da colecta',
|
||||
],
|
||||
] 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(
|
||||
'incomeIncluded',
|
||||
'Lucros ilíquidos e acréscimo incluídos na base corrente',
|
||||
@@ -339,6 +393,155 @@ export function SimulationIrpcEconomic({
|
||||
</option>
|
||||
</select>
|
||||
</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' ? (
|
||||
<p>
|
||||
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 { SimulationIrpcCosts } from '@/components/simulation-irpc-costs';
|
||||
import { SimulationIrpcSchedules } from '@/components/simulation-irpc-schedules';
|
||||
import { SimulationIrpcEconomicInclusion } from '@/components/simulation-irpc-economic-inclusion';
|
||||
import { SimulationIrpcAssessment } from '@/components/simulation-irpc-assessment';
|
||||
import { useState } from 'react';
|
||||
import { SimulationIrpcTaxableBase } from '@/components/simulation-irpc-taxable-base';
|
||||
@@ -438,6 +439,16 @@ export function SimulationIrpcNormal({
|
||||
)}
|
||||
<SimulationIrpcPeriodFx draft={draft} 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
|
||||
draft={draft}
|
||||
book={b}
|
||||
|
||||
@@ -1193,6 +1193,13 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
||||
b.assessmentKey,
|
||||
colecta,
|
||||
issues,
|
||||
{
|
||||
ordinary:
|
||||
assessment?.kind === 'normal' &&
|
||||
assessment.current &&
|
||||
assessment.key === b.assessmentKey,
|
||||
taxableBase: assessment?.record?.model.taxableBase ?? null,
|
||||
},
|
||||
);
|
||||
pending.push(
|
||||
...m.pending.map(
|
||||
@@ -1202,6 +1209,22 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
||||
return m;
|
||||
}) ?? [];
|
||||
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')
|
||||
pending.push(
|
||||
'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: [
|
||||
...(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
|
||||
@@ -1598,6 +1621,51 @@ export function annualReportRows(
|
||||
r.input.grossUpIncluded +
|
||||
' MZN;quota proporcional: ' +
|
||||
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: ' +
|
||||
show(r.eligible) +
|
||||
';uso efectivo anterior: ' +
|
||||
@@ -1616,7 +1684,12 @@ export function annualReportRows(
|
||||
';53: ' +
|
||||
r.input.grossUpReference +
|
||||
';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: ' +
|
||||
r.input.previousUse.reference,
|
||||
] as [string, string, string, string],
|
||||
|
||||
@@ -51,6 +51,18 @@ export type EconomicPosition = {
|
||||
grossUpReference: string;
|
||||
nationalLimit: 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;
|
||||
};
|
||||
export type EconomicCredits = {
|
||||
@@ -113,12 +125,37 @@ export function validateEconomic(c: EconomicCredits) {
|
||||
throw Error(
|
||||
'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(
|
||||
(k) => k !== 'previousUse',
|
||||
);
|
||||
for (const r of c.rows)
|
||||
if (
|
||||
!r ||
|
||||
(r.nationalMethod !== undefined &&
|
||||
!['documented', 'proportional'].includes(r.nationalMethod)) ||
|
||||
(r.nationalBasisReference !== undefined &&
|
||||
typeof r.nationalBasisReference !== 'string') ||
|
||||
keys.some(
|
||||
(k) =>
|
||||
typeof (r as unknown as Record<string, unknown>)[k] !==
|
||||
@@ -201,6 +238,57 @@ export function economicGroupPending(c: EconomicCredits) {
|
||||
);
|
||||
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(
|
||||
r: EconomicPosition,
|
||||
subjectNuit: string,
|
||||
@@ -208,6 +296,7 @@ export function economicPosition(
|
||||
assessmentKey: string,
|
||||
colecta: FiscalFraction | null,
|
||||
proofIssues: string[],
|
||||
basis?: { ordinary: boolean; taxableBase: FiscalFraction | null },
|
||||
) {
|
||||
const pending = [...proofIssues];
|
||||
let nominal: FiscalFraction | null = null,
|
||||
@@ -216,6 +305,15 @@ export function economicPosition(
|
||||
previousUse: FiscalFraction | null = null,
|
||||
available: 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' =
|
||||
'indeterminate';
|
||||
try {
|
||||
@@ -230,10 +328,12 @@ export function economicPosition(
|
||||
throw Error(
|
||||
'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';
|
||||
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 (
|
||||
@@ -272,8 +372,11 @@ export function economicPosition(
|
||||
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.',
|
||||
);
|
||||
const qualified = economicIncome(r);
|
||||
liquidation = qualified.liquidation;
|
||||
association = qualified.association;
|
||||
const tax = economicTaxProof(r),
|
||||
income = creditMoney(r.income);
|
||||
income = qualified.income;
|
||||
nominal = fraction(
|
||||
BigInt(tax.value.numerator) * 3n,
|
||||
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.',
|
||||
);
|
||||
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 (
|
||||
!r.nationalLimitReference.trim() ||
|
||||
(r.nationalMethod !== 'proportional' &&
|
||||
!r.nationalLimitReference.trim()) ||
|
||||
!colecta ||
|
||||
creditCompare(nationalLimit, colecta) > 0n ||
|
||||
(creditCompare(included, zero()) === 0n &&
|
||||
@@ -327,6 +462,9 @@ export function economicPosition(
|
||||
id: r.id,
|
||||
nominal,
|
||||
nationalLimit,
|
||||
...(nationalDerivation ? { nationalDerivation } : {}),
|
||||
...(liquidation ? { liquidation } : {}),
|
||||
...(association ? { association } : {}),
|
||||
eligible,
|
||||
previousUse,
|
||||
available,
|
||||
@@ -354,7 +492,17 @@ export function economicDeduction(
|
||||
deducted,
|
||||
afterCredit:
|
||||
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,
|
||||
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,
|
||||
costs?: NormalCostsModel,
|
||||
depreciation?: NormalDepreciationModel,
|
||||
economic?: {
|
||||
effects: {
|
||||
id: string;
|
||||
topic: string;
|
||||
amount: FiscalFraction | null;
|
||||
effect: FiscalFraction | null;
|
||||
reason: string;
|
||||
}[];
|
||||
net: FiscalFraction;
|
||||
pending: string[];
|
||||
},
|
||||
): NormalProfitModel {
|
||||
validateNormalProfit(input);
|
||||
const pending: string[] = [],
|
||||
@@ -291,9 +302,10 @@ export function closeNormalProfit(
|
||||
const mapped = [
|
||||
...(schedules?.effects ?? []),
|
||||
...(costs?.effects ?? []),
|
||||
...(economic?.effects ?? []),
|
||||
].filter((e) => e.topic === key);
|
||||
const covered =
|
||||
(schedules || costs) &&
|
||||
(schedules || costs || economic) &&
|
||||
mapped.length > 0 &&
|
||||
mapped.every((e) => e.effect !== null) &&
|
||||
value !== null &&
|
||||
@@ -324,6 +336,7 @@ export function closeNormalProfit(
|
||||
}
|
||||
if (schedules) pending.push(...schedules.pending);
|
||||
if (costs) pending.push(...costs.pending);
|
||||
if (economic) pending.push(...economic.pending);
|
||||
if (depreciation && !schedules) pending.push(...depreciation.pending);
|
||||
return {
|
||||
input: structuredClone(input),
|
||||
@@ -335,6 +348,7 @@ export function closeNormalProfit(
|
||||
? addFractions([
|
||||
accounting,
|
||||
additions,
|
||||
...(economic ? [economic.net] : []),
|
||||
...(schedules ? [schedules.net] : []),
|
||||
...(costs ? [costs.net] : []),
|
||||
...(depreciation && !schedules && depreciation.adjustment
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { inputText } from './input-text.ts';
|
||||
import {
|
||||
calculateEconomicInclusions,
|
||||
validateEconomicInclusions,
|
||||
type EconomicInclusions,
|
||||
} from './irpc-normal-economic.ts';
|
||||
import { irpcSpecialCombinationsVersion } from './irpc-special-combinations.ts';
|
||||
import {
|
||||
irpcSpecialVersion,
|
||||
@@ -104,6 +109,7 @@ export type NormalAdjustment = {
|
||||
reference: string;
|
||||
};
|
||||
export type NormalBook = {
|
||||
economicInclusions?: EconomicInclusions;
|
||||
version: 1;
|
||||
year: string;
|
||||
mappingReference: string;
|
||||
@@ -123,6 +129,7 @@ export type NormalBook = {
|
||||
history?: NormalRecord[];
|
||||
};
|
||||
export type NormalModel = {
|
||||
economicInclusions?: ReturnType<typeof calculateEconomicInclusions>;
|
||||
special?: SpecialModel;
|
||||
formPreparation?: NormalFormPreparationModel;
|
||||
periodsFx?: NormalPeriodFxModel;
|
||||
@@ -213,6 +220,8 @@ function store(d: Record<string, unknown>) {
|
||||
return s;
|
||||
}
|
||||
export function validateNormal(b: NormalBook) {
|
||||
if (b?.economicInclusions !== undefined)
|
||||
validateEconomicInclusions(b.economicInclusions);
|
||||
if (b?.special !== undefined) validateSpecial(b.special);
|
||||
if (b?.forms !== undefined) validateNormalFormPreparation(b.forms);
|
||||
if (b?.periodsFx !== undefined) validateNormalPeriodFx(b.periodsFx);
|
||||
@@ -333,6 +342,7 @@ function key(d: Record<string, unknown>, b: NormalBook) {
|
||||
b.scopeReference,
|
||||
b.rows,
|
||||
...(b.profit ? [b.profit] : []),
|
||||
...(b.economicInclusions ? [b.economicInclusions] : []),
|
||||
...(b.base ? [b.base] : []),
|
||||
...(b.assessment ? [b.assessment] : []),
|
||||
...(b.schedules ? [b.schedules] : []),
|
||||
@@ -569,6 +579,23 @@ export function calculateNormal(
|
||||
!!periodsFx?.short,
|
||||
)
|
||||
: 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
|
||||
? closeNormalProfit(
|
||||
b.profit,
|
||||
@@ -580,6 +607,7 @@ export function calculateNormal(
|
||||
schedules,
|
||||
costs,
|
||||
depreciation,
|
||||
economicInclusions,
|
||||
)
|
||||
: undefined;
|
||||
if (profit && periodsFx)
|
||||
@@ -677,6 +705,7 @@ export function calculateNormal(
|
||||
? addFractions([accountingProfit, additions])
|
||||
: null,
|
||||
taxableProfit: profit?.taxableProfit ?? null,
|
||||
...(economicInclusions ? { economicInclusions } : {}),
|
||||
taxableBase: base?.taxableBase ?? null,
|
||||
tax: null,
|
||||
rows,
|
||||
@@ -774,6 +803,18 @@ export function normalReportRows(
|
||||
if (!r) return [];
|
||||
const m = r.model;
|
||||
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',
|
||||
'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);
|
||||
assert.equal(m.internationalCredit.positions.length, 1);
|
||||
assert.equal(number(m.internationalCredit.positions[0].eligible), 8);
|
||||
if (
|
||||
patch.article40 === 'yes' ||
|
||||
patch.kind === 'liquidation' ||
|
||||
patch.kind === 'association'
|
||||
)
|
||||
if (patch.article40 === 'yes')
|
||||
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