Clarify viability simulation purpose and preserve PEC cash-flow hypotheses
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-11 10:37:33 +02:00
parent aae79c712d
commit a708f079b3
24 changed files with 754 additions and 42 deletions
+4
View File
@@ -1,3 +1,7 @@
## Simulação de viabilidade — clarificação expressa do proprietário,11/11/2026
O InvesteMoz simula impostos para avaliar a viabilidade do negócio; não actua como entidade que executa pagamentos. Apresentar PPC/PEC e distribuição por meses como estimativas fiscais e projecções de tesouraria,com fontes e pressupostos. Montantes já pagos,quando fornecidos,são dados documentais para o cálculo. Não confundir uma hipótese de caixa com pagamento efectuado,declaração submetida ou cumprimento fiscal certificado. Preservar os cálculos,históricos e dados aceites; não criar serviços de cobrança/execução de pagamentos como requisito de conclusão deste simulador.
# Instruções permanentes do projeto InvesteMoz
## Encerramento e reabertura do dossier — decisão do proprietário, 03/11/2026
@@ -1,3 +1,13 @@
## 11/11/2026 — finalidade do simulador e projecção de tesouraria PEC
**Clarificação expressa do proprietário:** o portal simula impostos para avaliar a viabilidade do negócio; não faz pagamentos. A apresentação anterior de «obrigação» e «plano de pagamento» criava uma expectativa errada de execução. Ecrãs PPC/PEC passam a **Simular PPC/PEC**,estimativa fiscal e projecção de tesouraria. Fontes legais e dados documentais já pagos continuam a informar o cálculo; não são operações executadas pelo portal. A execução de pagamentos não é pendência deste simulador.
**Implementado em QA:** hipótese PEC distribuída por Junho/Agosto/Outubro com montantes explícitos cuja soma corresponde exactamente ao total,ou hipótese concentrada num mês escolhido. Não inventa igualdade/arredondamento ou validação de prazo para a opção única. Valores desconhecidos,divergência da soma,total alterado e ausência de estimativa positiva invalidam a projecção dependente,sem bloquear o cálculo fiscal. Trocar hipótese conserva edições;resumo com Alterar. Rascunho privado,recuperação,histórico e relatório do mesmo dossier reutilizados. As estimativas não alimentam os comprovativos pagos nem alteram deduções anuais.
Cinco verificações novas de cálculo/interface e35afectadas aprovadas;percurso sintéticoAPI/recuperação/relatório em quatro cenários(distribuído,concentrado,divergente,total alterado),com saldo anual138MZN conservado. Provas `outputs/irpc-pec-plan-20261111/*-verification.json`. Sem migração,base,papel ou permissão nova. Tipos/lint/compilação aprovados;uma prova nativa/38asserções com rollback. Pacote Git e reversão conferidos no fecho. Isto não é conferência visual.
**Estado:** QA,ainda sem instalação;homologação permanece `bcbbe51c` e270instituições publicadas. Restam instalação/visual e recortes fiscais de base29(5),rectificação29(3),PPC excedente,períodos/regimes e outras regras de simulação;prazo legal da hipótese única não é certificado. L1–L7/restanteL8/L9–L16 e demais módulos permanecem no saldo,sem transformar o portal em sistema de execução fiscal. Próximo trabalho:primeiro residual de simulação executável com fontes aceites.
## 11/11/2026 — total e calendário PEC instalados embcbbe51c
Instalação expressamente autorizada,executada pelo processo Git existente e conferida às10:28:18Africa/Johannesburg(08:28:18UTC): `bcbbe51c2fc279d5b2d85412f7bcb6ca2423a9b6`. Utilizável no simulador fiscal,fecho anual L8: **Preparar obrigação PEC**,com total ordinário2026 baseado no volume documental2025,limites antes dos PPC pagos anteriores,mesesJunho/Agosto/Outubro e dispensa no ano de início efectivo. Guardar/recuperar pelo rascunho privado e apresentar no relatório do mesmo dossier. Não cria pagamento,dedução anual ou reembolso.
@@ -1,3 +1,13 @@
## 11/11/2026 — finalidade do simulador e projecção de tesouraria PEC
**Clarificação expressa do proprietário:** o portal simula impostos para avaliar a viabilidade do negócio; não faz pagamentos. A apresentação anterior de «obrigação» e «plano de pagamento» criava uma expectativa errada de execução. Ecrãs PPC/PEC passam a **Simular PPC/PEC**,estimativa fiscal e projecção de tesouraria. Fontes legais e dados documentais já pagos continuam a informar o cálculo; não são operações executadas pelo portal. A execução de pagamentos não é pendência deste simulador.
**Implementado em QA:** hipótese PEC distribuída por Junho/Agosto/Outubro com montantes explícitos cuja soma corresponde exactamente ao total,ou hipótese concentrada num mês escolhido. Não inventa igualdade/arredondamento ou validação de prazo para a opção única. Valores desconhecidos,divergência da soma,total alterado e ausência de estimativa positiva invalidam a projecção dependente,sem bloquear o cálculo fiscal. Trocar hipótese conserva edições;resumo com Alterar. Rascunho privado,recuperação,histórico e relatório do mesmo dossier reutilizados. As estimativas não alimentam os comprovativos pagos nem alteram deduções anuais.
Cinco verificações novas de cálculo/interface e35afectadas aprovadas;percurso sintéticoAPI/recuperação/relatório em quatro cenários(distribuído,concentrado,divergente,total alterado),com saldo anual138MZN conservado. Provas `outputs/irpc-pec-plan-20261111/*-verification.json`. Sem migração,base,papel ou permissão nova. Tipos/lint/compilação aprovados;uma prova nativa/38asserções com rollback. Pacote Git e reversão conferidos no fecho. Isto não é conferência visual.
**Estado:** QA,ainda sem instalação;homologação permanece `bcbbe51c` e270instituições publicadas. Restam instalação/visual e recortes fiscais de base29(5),rectificação29(3),PPC excedente,períodos/regimes e outras regras de simulação;prazo legal da hipótese única não é certificado. L1–L7/restanteL8/L9–L16 e demais módulos permanecem no saldo,sem transformar o portal em sistema de execução fiscal. Próximo trabalho:primeiro residual de simulação executável com fontes aceites.
## 11/11/2026 — total e calendário PEC instalados embcbbe51c
Instalação expressamente autorizada,executada pelo processo Git existente e conferida às10:28:18Africa/Johannesburg(08:28:18UTC): `bcbbe51c2fc279d5b2d85412f7bcb6ca2423a9b6`. Utilizável no simulador fiscal,fecho anual L8: **Preparar obrigação PEC**,com total ordinário2026 baseado no volume documental2025,limites antes dos PPC pagos anteriores,mesesJunho/Agosto/Outubro e dispensa no ano de início efectivo. Guardar/recuperar pelo rascunho privado e apresentar no relatório do mesmo dossier. Não cria pagamento,dedução anual ou reembolso.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,13 @@
## 11/11/2026 — finalidade do simulador e projecção de tesouraria PEC
**Clarificação expressa do proprietário:** o portal simula impostos para avaliar a viabilidade do negócio; não faz pagamentos. A apresentação anterior de «obrigação» e «plano de pagamento» criava uma expectativa errada de execução. Ecrãs PPC/PEC passam a **Simular PPC/PEC**,estimativa fiscal e projecção de tesouraria. Fontes legais e dados documentais já pagos continuam a informar o cálculo; não são operações executadas pelo portal. A execução de pagamentos não é pendência deste simulador.
**Implementado em QA:** hipótese PEC distribuída por Junho/Agosto/Outubro com montantes explícitos cuja soma corresponde exactamente ao total,ou hipótese concentrada num mês escolhido. Não inventa igualdade/arredondamento ou validação de prazo para a opção única. Valores desconhecidos,divergência da soma,total alterado e ausência de estimativa positiva invalidam a projecção dependente,sem bloquear o cálculo fiscal. Trocar hipótese conserva edições;resumo com Alterar. Rascunho privado,recuperação,histórico e relatório do mesmo dossier reutilizados. As estimativas não alimentam os comprovativos pagos nem alteram deduções anuais.
Cinco verificações novas de cálculo/interface e35afectadas aprovadas;percurso sintéticoAPI/recuperação/relatório em quatro cenários(distribuído,concentrado,divergente,total alterado),com saldo anual138MZN conservado. Provas `outputs/irpc-pec-plan-20261111/*-verification.json`. Sem migração,base,papel ou permissão nova. Tipos/lint/compilação aprovados;uma prova nativa/38asserções com rollback. Pacote Git e reversão conferidos no fecho. Isto não é conferência visual.
**Estado:** QA,ainda sem instalação;homologação permanece `bcbbe51c` e270instituições publicadas. Restam instalação/visual e recortes fiscais de base29(5),rectificação29(3),PPC excedente,períodos/regimes e outras regras de simulação;prazo legal da hipótese única não é certificado. L1–L7/restanteL8/L9–L16 e demais módulos permanecem no saldo,sem transformar o portal em sistema de execução fiscal. Próximo trabalho:primeiro residual de simulação executável com fontes aceites.
## 11/11/2026 — total e calendário PEC instalados embcbbe51c
Instalação expressamente autorizada,executada pelo processo Git existente e conferida às10:28:18Africa/Johannesburg(08:28:18UTC): `bcbbe51c2fc279d5b2d85412f7bcb6ca2423a9b6`. Utilizável no simulador fiscal,fecho anual L8: **Preparar obrigação PEC**,com total ordinário2026 baseado no volume documental2025,limites antes dos PPC pagos anteriores,mesesJunho/Agosto/Outubro e dispensa no ano de início efectivo. Guardar/recuperar pelo rascunho privado e apresentar no relatório do mesmo dossier. Não cria pagamento,dedução anual ou reembolso.
@@ -342,6 +342,7 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>PPC efectivamente pagos</legend>
<p>Simulação fiscal para avaliar a viabilidade do negócio e as necessidades de tesouraria. Os valores e meses previstos são estimativas; o portal não executa pagamentos nem submete declarações fiscais.</p>
<SimulationIrpcPecObligation book={b} onChange={pecObligation=>patch({pecObligation})}/>
<SimulationIrpcPpcObligation book={b} onChange={ppcObligation=>patch({ppcObligation})}/>
{group('ppc', 'PPC efectuados')}
@@ -1,3 +1,4 @@
import {SimulationIrpcPecPlan} from './simulation-irpc-pec-plan';
import {
newPecObligation,
calculatePecObligation,
@@ -17,7 +18,7 @@ export function SimulationIrpcPecObligation({
if (!p)
return (
<section className="rounded border p-3">
<h5>Obrigação PEC</h5>
<h5>Estimativa PEC</h5>
<p>
Preparar o total e os meses previstos conserva os
comprovativos pagos em separado.
@@ -28,7 +29,7 @@ export function SimulationIrpcPecObligation({
onChange(newPecObligation(book.subjectNuit, book.year))
}
>
Preparar obrigação PEC
Simular PEC
</button>
</section>
);
@@ -41,7 +42,7 @@ export function SimulationIrpcPecObligation({
patch = (changes: Partial<PecObligation>) =>
onChange({ ...p, ...changes });
const field = (
key: Exclude<keyof PecObligation, 'version' | 'scope'>,
key: Exclude<keyof PecObligation, 'version' | 'scope' | 'plan'>,
label: string,
) => (
<label className="block">
@@ -55,10 +56,10 @@ export function SimulationIrpcPecObligation({
);
return (
<section
aria-label="Preparação da obrigação PEC"
aria-label="Preparação da estimativa PEC"
className="rounded border p-3"
>
<h5>Obrigação PEC — preparação</h5>
<h5>Estimativa PEC — preparação</h5>
<label className="block">
Enquadramento PEC
<select
@@ -82,8 +83,8 @@ export function SimulationIrpcPecObligation({
</label>
{p.scope === 'ordinary_resident' && (
<>
{field('subjectNuit', 'NUIT — obrigação PEC')}
{field('year', 'Exercício — obrigação PEC')}
{field('subjectNuit', 'NUIT — estimativa PEC')}
{field('year', 'Exercício — estimativa PEC')}
{field(
'activityStartYear',
'Ano de início efectivo da actividade — PEC',
@@ -122,7 +123,7 @@ export function SimulationIrpcPecObligation({
) : (
<>
<p>
Obrigação total:{' '}
Total estimado:{' '}
{fractionPresentation(m.total!).decimal} MZN.
</p>
<ul>
@@ -130,13 +131,10 @@ export function SimulationIrpcPecObligation({
<li key={month}>{month}</li>
))}
</ul>
<p>
Repartição por prestação e opção de pagamento único por
preparar; não se presume dia limite nem arredondamento
do PPC para o PEC.
</p>
</>
)}
<SimulationIrpcPecPlan plan={p.plan} total={m.total} notApplicable={m.notApplicable} year={book.year} onChange={plan=>patch({plan})}/>
<p>
Guardar o rascunho conserva esta preparação. Calcular o fecho
anual inclui-a no relatório. Não regista pagamentos,cria
@@ -0,0 +1,135 @@
import {
newPecPlan,
calculatePecPlan,
pecPlanSource,
type PecPlan,
} from '@/lib/irpc-pec-plan';
import {
fractionPresentation,
type FiscalFraction,
} from '@/lib/fiscal-rational';
export function SimulationIrpcPecPlan({
plan,
total,
notApplicable,
year,
onChange,
}: {
plan?: PecPlan;
total: FiscalFraction | null;
notApplicable: boolean;
year: string;
onChange: (p: PecPlan) => void;
}) {
if (!plan)
return (
<div className="rounded border p-3">
<p>
Prepare os montantes por prestação ou a opção de pagamento
único,sem registar pagamento.
</p>
<button
type="button"
disabled={
!total || notApplicable || BigInt(total.numerator) <= 0n
}
onClick={() => onChange(newPecPlan())}
>
Simular tesouraria PEC
</button>
</div>
);
const m = calculatePecPlan(plan, total, notApplicable, year),
patch = (p: Partial<PecPlan>) => onChange({ ...plan, ...p });
const field = (
key: 'reference' | 'june' | 'august' | 'october',
label: string,
) => (
<label className="block">
{label}
<input
aria-label={label}
value={plan[key]}
onChange={(e) => patch({ [key]: e.target.value })}
/>
</label>
);
return (
<section
aria-label="Projecção de tesouraria PEC"
className="rounded border p-3"
>
<h6>Projecção PEC</h6>
{m.pending.length ? (
<p role="status">{m.pending.join(' ')}</p>
) : (
<p>
Soma reconciliada:{' '}
{m.rows
.map(
(r) =>
r.month +
': ' +
fractionPresentation(r.amount).decimal +
' MZN',
)
.join('; ')}
. Prazo legal não validado.
</p>
)}
<details open={m.pending.length > 0}>
<summary>Alterar projecção PEC</summary>
<label className="block">
Opção de preparação PEC
<select
aria-label="Opção de preparação PEC"
value={plan.mode}
onChange={(e) =>
patch({ mode: e.target.value as PecPlan['mode'] })
}
>
<option value="">Por escolher</option>
<option value="three">
Distribuir a estimativa por três meses
</option>
<option value="single">
Concentrar a estimativa num mês
</option>
</select>
</label>
{field('reference', 'Fundamento do projecção PEC')}
{plan.mode === 'three' && (
<>
{field('june', 'PEC Junho — MZN')}
{field('august', 'PEC Agosto — MZN')}
{field('october', 'PEC Outubro — MZN')}
</>
)}
{plan.mode === 'single' && (
<label className="block">
Mês previsto do PEC único
<input
aria-label="Mês previsto do PEC único"
type="month"
value={plan.singleMonth}
onChange={(e) =>
patch({ singleMonth: e.target.value })
}
/>
</label>
)}
</details>
<p>
O mês escolhido é uma hipótese de tesouraria para avaliar a
viabilidade. A reconciliação da soma não valida os prazos
legais,emite guia ou regista pagamento. Alterar a estimativa
exige reconferir o plano. Guarde pelo rascunho e recalcule o
fecho para actualizar o relatório.
</p>
<details>
<summary>Fonte do projecção PEC</summary>
<p>{pecPlanSource}</p>
</details>
</section>
);
}
@@ -17,7 +17,7 @@ export function SimulationIrpcPpcObligation({
if (!p)
return (
<section className="rounded border p-3">
<h5>Obrigação PPC</h5>
<h5>Estimativa PPC</h5>
<p>
Preparar a obrigação e os meses previstos não regista
pagamentos nem cria deduções na liquidação.
@@ -28,7 +28,7 @@ export function SimulationIrpcPpcObligation({
onChange(newPpcObligation(book.subjectNuit, book.year))
}
>
Preparar obrigação PPC
Simular PPC
</button>
</section>
);
@@ -62,10 +62,10 @@ export function SimulationIrpcPpcObligation({
);
return (
<section
aria-label="Preparação da obrigação PPC"
aria-label="Preparação da estimativa PPC"
className="rounded border p-3"
>
<h5>Obrigação PPC — exercício anterior</h5>
<h5>Estimativa PPC — exercício anterior</h5>
<p>
Reutiliza o titular e exercício ao iniciar. Complete a
liquidação anterior e as retenções documentadas; campos vazios
@@ -94,8 +94,8 @@ export function SimulationIrpcPpcObligation({
</label>
{p.scope === 'ordinary_resident' && (
<>
{field('subjectNuit', 'NUIT — obrigação PPC')}
{field('year', 'Exercício — obrigação PPC')}
{field('subjectNuit', 'NUIT — estimativa PPC')}
{field('year', 'Exercício — estimativa PPC')}
{field('previousYear', 'Exercício anterior — PPC')}
<label className="block">
Exercício anterior civil completo
@@ -1,3 +1,4 @@
import {pecPlanVersion} from './irpc-pec-plan.ts';
import {validatePecObligation,pecObligationReport,pecObligationVersion,type PecObligation} from './irpc-pec-obligation.ts';
import { validatePpcObligation, ppcObligationReport, ppcObligationVersion, type PpcObligation } from './irpc-ppc-obligation.ts';
import {
@@ -573,6 +574,7 @@ function key(d: Record<string, unknown>, b: AnnualBook) {
dependencies(d, !!b.investmentCredit),
...(b.ppcObligation ? [ppcObligationVersion] : []),
...(b.pecObligation ? [pecObligationVersion] : []),
...(b.pecObligation?.plan ? [pecPlanVersion] : []),
...(b.investmentCredit && b.creditCarry?.lots.length
? ['CBF15-credit-carry-combination/1']
: []),
+9 -5
View File
@@ -1,3 +1,4 @@
import {validatePecPlan,pecPlanReport,type PecPlan} from './irpc-pec-plan.ts';
import {
decimalFraction,
fraction,
@@ -10,6 +11,7 @@ export const pecObligationSource =
'RIRPC,Decreto9/2008,art29(1–5),BR16/I16-04-2008,original SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee.0,5% do volume de negócios anterior,limites30.000/100.000MZN,menos PPC efectivamente pagos no ano anterior.Junho/Agosto/Outubro;opção de pagamento único prevista,sem data ou repartição inventada. Ano de início excluído29(4);sem volume anterior exige último montante29(5). Recorte2026,residente geral comercial/industrial/agrícola,ano civil,MZN. Regimes/períodos especiais,rectificação29(3),base de anos anteriores29(5),guias/juros separados.';
export type PecObligation = {
version: 1;
plan?: PecPlan;
scope: '' | 'ordinary_resident' | 'other';
subjectNuit: string;
year: string;
@@ -38,6 +40,7 @@ export const newPecObligation = (
ppcReference: '',
});
export function validatePecObligation(p: PecObligation) {
if(p?.plan!==undefined)validatePecPlan(p.plan);
if (
!p ||
p.version !== 1 ||
@@ -51,7 +54,7 @@ export function validatePecObligation(p: PecObligation) {
!['', 'ordinary_resident', 'other'].includes(p.scope)
)
throw Error(
'Preparação da obrigação PEC não recuperável;original preservado.',
'Preparação da estimativa PEC não recuperável;original preservado.',
);
}
export function calculatePecObligation(
@@ -129,7 +132,7 @@ export function calculatePecObligation(
const residual = subtractFractions(limited, ppc);
if (BigInt(residual.numerator) < 0n)
pending.push(
'PEC:PPC anteriores excedem o montante limitado;caso separado para reconciliação,não gera obrigação negativa ou reembolso.',
'PEC:PPC anteriores excedem o montante limitado;caso separado para reconciliação,não gera estimativa negativa ou reembolso.',
);
else total = residual;
}
@@ -160,9 +163,10 @@ export function pecObligationReport(
show = (v: FiscalFraction | null) =>
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
return [
...(p.plan ? pecPlanReport(p.plan,m.total,m.notApplicable,year) : []),
[
'IRPC — L8',
'PEC — obrigação preparada',
'PEC — estimativa fiscal',
m.pending.length
? m.pending.join(' ')
: m.notApplicable
@@ -171,11 +175,11 @@ export function pecObligationReport(
show(m.percentage) +
';limitado antes de PPC: ' +
show(m.limited) +
';obrigação total: ' +
';total estimado: ' +
show(m.total) +
';meses: ' +
m.months.join(', ') +
'. Montantes por prestação e opção de pagamento único não determinados neste recorte. Preparação não é pagamento nem dedução anual.',
(p.plan ? '. Plano explícito apresentado separadamente;prazo legal não validado.' : '. Montantes por prestação e opção de pagamento único não determinados neste recorte.') + ' Preparação não é pagamento nem dedução anual.',
p.activityReference +
'; ' +
p.turnoverReference +
+147
View File
@@ -0,0 +1,147 @@
import {
decimalFraction,
addFractions,
subtractFractions,
fractionPresentation,
type FiscalFraction,
} from './fiscal-rational.ts';
export const pecPlanVersion = 'IRPC-PEC-plan/1';
export const pecPlanSource =
'RIRPC29(1):mesesJunho/Agosto/Outubro e opção de pagamento único. Montantes repartidos e mês único são hipótese explícita do utilizador,sem regra de igualdade,arredondamento,guia ou prazo legal presumidos. Conferência de prazo da opção única permanece separada.';
export type PecPlan = {
version: 1;
mode: '' | 'three' | 'single';
reference: string;
singleMonth: string;
june: string;
august: string;
october: string;
};
export const newPecPlan = (): PecPlan => ({
version: 1,
mode: '',
reference: '',
singleMonth: '',
june: '',
august: '',
october: '',
});
export function validatePecPlan(p: PecPlan) {
if (
!p ||
p.version !== 1 ||
Object.keys(newPecPlan())
.filter((k) => k !== 'version')
.some(
(k) =>
typeof (p as unknown as Record<string, unknown>)[k] !==
'string',
) ||
!['', 'three', 'single'].includes(p.mode)
)
throw Error('Projecção PEC não recuperável;original preservado.');
}
export function calculatePecPlan(
p: PecPlan,
total: FiscalFraction | null,
notApplicable: boolean,
year: string,
) {
validatePecPlan(p);
const pending: string[] = [];
let rows: Array<{ month: string; amount: FiscalFraction }> = [];
if (!total)
pending.push(
'Projecção PEC:total estimado indeterminada;reconciliar a base antes de preparar os montantes.',
);
else if (notApplicable || BigInt(total.numerator) === 0n)
pending.push(
'Projecção PEC:sem estimativa positiva,não se distribuem saídas de caixa.',
);
if (year !== '2026')
pending.push('Projecção PEC:exercício fora do recorte2026.');
if (!p.reference.trim())
pending.push(
'Projecção PEC:identifique o fundamento da opção e dos montantes preparados.',
);
if (!['three', 'single'].includes(p.mode))
pending.push(
'Projecção PEC:escolha três prestações ou pagamento único.',
);
if (!pending.length && total) {
if (p.mode === 'three') {
const values = [p.june, p.august, p.october];
if (!values.every((v) => /^\d{1,18}(\.\d{1,2})?$/.test(v)))
pending.push(
'Projecção PEC:preencha os três montantes em MZN,com até dois decimais;sem preenchimento automático.',
);
else {
const amounts = values.map(decimalFraction);
if (amounts.some((v) => BigInt(v.numerator) <= 0n))
pending.push(
'Projecção PEC:três prestações exigem três montantes positivos.',
);
else if (
BigInt(
subtractFractions(addFractions(amounts), total)
.numerator,
) !== 0n
)
pending.push(
'Projecção PEC:a soma das três prestações difere do total preparado;sem arredondamento ou compensação silenciosos.',
);
else
rows = amounts.map((amount, i) => ({
month: ['2026-06', '2026-08', '2026-10'][i],
amount,
}));
}
} else if (!/^2026-(0[1-9]|1[0-2])$/.test(p.singleMonth))
pending.push(
'Projecção PEC:indique o mês previsto do pagamento único;isto não valida o prazo legal.',
);
else rows = [{ month: p.singleMonth, amount: total }];
}
return {
version: pecPlanVersion,
source: pecPlanSource,
mode: p.mode,
pending,
rows,
total,
allocationReconciled: pending.length === 0,
legalDeadlineValidated: false,
paymentExecuted: false,
annualDeductionCreated: false,
};
}
export function pecPlanReport(
p: PecPlan,
total: FiscalFraction | null,
notApplicable: boolean,
year: string,
): Array<[string, string, string, string]> {
const m = calculatePecPlan(p, total, notApplicable, year);
return [
[
'IRPC — L8',
'PEC — projecção de tesouraria',
m.pending.length
? m.pending.join(' ')
: (m.mode === 'single'
? 'Saída de caixa concentrada (hipótese): '
: 'Distribuição de caixa (hipótese): ') +
m.rows
.map(
(r) =>
r.month +
': ' +
fractionPresentation(r.amount).decimal +
' MZN',
)
.join('; ') +
'. Soma reconciliada;prazo legal não validado. Não é pagamento ou dedução.',
p.reference + '; ' + m.source,
],
];
}
+1 -1
View File
@@ -142,7 +142,7 @@ export function ppcObligationReport(
return [
[
'IRPC — L8',
'PPC — obrigação preparada',
'PPC — estimativa fiscal',
m.pending.length
? m.pending.join(' ')
: 'Base líquida anterior: ' +
+48
View File
@@ -0,0 +1,48 @@
<?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 IrpcPecPlanDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_pec_obligation_and_calendar_survive_native_draft_without_new_paid_deductions(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('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));
$id = null;
$etag = null;
foreach (['three' => 138, 'single' => 138, 'mismatch' => 138, 'changed_total' => 138] as $name => $balance) {
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-pec-plan-20261111/'.$name.'.json')), true, flags: JSON_THROW_ON_ERROR);
$personal = ['person_type' => $snapshot['person_type']];
unset($snapshot['person_type']);
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $snapshot];
if ($id === null) {
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
} else {
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
}
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
$etag = $response->headers->get('ETag');
$back = $response->json('data.economic_snapshot');
$this->assertSame($snapshot['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$book = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$snapshot['context_opportunity']];
$this->assertCount(1, $book['history']);
$this->assertSame(['numerator' => '138', 'denominator' => '1'], $book['history'][0]['model']['arithmeticBalance']);
$this->assertSame($balance === null ? null : ['numerator' => (string) $balance, 'denominator' => '1'], $book['result']['model']['arithmeticBalance']);
$this->assertSame($book['pecObligation'], $book['result']['model']['input']['pecObligation']);
$this->assertSame('SYNTHETIC cash-flow hypothesis for viability', $book['pecObligation']['plan']['reference']);
$this->assertSame('SYNTHETIC effective activity'.($name === 'start' ? '2026' : '2020'), $book['pecObligation']['activityReference']);
file_put_contents(base_path('outputs/irpc-pec-plan-20261111/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
@@ -30,7 +30,9 @@ const output = ts.transpileModule(source, {
}).outputText;
compileFunction(output, ['require', 'module', 'exports'])(
(name) =>
name === '@/lib/irpc-annual-settlement'
name === './simulation-irpc-pec-plan'
? {SimulationIrpcPecPlan:()=>null}
: name === '@/lib/irpc-annual-settlement'
? annual
: name === '@/lib/irpc-pec-obligation'
? investment
@@ -86,7 +88,7 @@ function harness(c) {
test('actual PEC form reuses context, preserves hidden edits and explains first-year exception', () => {
const c = investmentCase(),
h = harness(c);
h.click('Preparar obrigação PEC');
h.click('Simular PEC');
assert.equal(h.book().pecObligation.subjectNuit, c.b.subjectNuit);
assert.equal(h.book().pecObligation.previousTurnover, '');
h.change('Enquadramento PEC', 'ordinary_resident');
+1 -1
View File
@@ -91,7 +91,7 @@ test('existing private book and annual history retain PEC preparation without ch
before.arithmeticBalance,
);
assert.deepEqual(back.pecPayments, b.pecPayments);
assert.match(annualHtml(d), /PEC — obrigação preparada/);
assert.match(annualHtml(d), /PEC — estimativa fiscal/);
assert.match(annualHtml(d), /2026-06/);
});
+100
View File
@@ -0,0 +1,100 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { compileFunction } from 'node:vm';
import { createRequire } from 'node:module';
import React from 'react';
import ts from 'typescript';
import * as annual from '../../resources/js/lib/irpc-annual-settlement.ts';
import * as investment from '../../resources/js/lib/irpc-pec-plan.ts';
import * as rational from '../../resources/js/lib/fiscal-rational.ts';
import { renderToStaticMarkup } from 'react-dom/server';
const require = createRequire(import.meta.url),
module = { exports: {} };
const source = fs.readFileSync(
new URL(
'../../resources/js/components/simulation-irpc-pec-plan.tsx',
import.meta.url,
),
'utf8',
);
const output = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
jsx: ts.JsxEmit.ReactJSX,
},
}).outputText;
compileFunction(output, ['require', 'module', 'exports'])(
(name) =>
name === './simulation-irpc-pec-plan'
? { SimulationIrpcPecPlan: () => null }
: name === '@/lib/irpc-annual-settlement'
? annual
: name === '@/lib/irpc-pec-plan'
? investment
: name === '@/lib/fiscal-rational'
? rational
: name === './simulation-irpc-special'
? { QualificationFields: () => null }
: name === './fiscal-proof-parcel'
? { FiscalProofParcelFields: () => null }
: require(name),
module,
module.exports,
);
const Component = module.exports.SimulationIrpcPecPlan;
const text = (node) =>
typeof node === 'string' || typeof node === 'number'
? String(node)
: React.isValidElement(node)
? React.Children.toArray(node.props.children).map(text).join('')
: '';
const all = (node) =>
React.isValidElement(node)
? [node, ...React.Children.toArray(node.props.children).flatMap(all)]
: [];
test('actual plan form retains deliberate amounts across mode changes and explains legal deadline limitation', () => {
let p;
const total = { numerator: '40000', denominator: '1' };
const render = () =>
Component({
plan: p,
total,
notApplicable: false,
year: '2026',
onChange: (next) => (p = next),
});
const click = (label) => {
const n = all(render()).find(
(x) => x.type === 'button' && text(x) === label,
);
assert.ok(n, label);
n.props.onClick();
};
const change = (label, value) => {
const n = all(render()).find((x) => x.props['aria-label'] === label);
assert.ok(n, label);
n.props.onChange({ target: { value } });
};
click('Simular tesouraria PEC');
assert.equal(p.june, '');
change('Opção de preparação PEC', 'three');
change('Fundamento do projecção PEC', 'SYNTHETIC allocation');
change('PEC Junho — MZN', '10000');
change('PEC Agosto — MZN', '15000');
change('PEC Outubro — MZN', '15000');
assert.match(text(render()), /Soma reconciliada/);
assert.match(text(render()), /Prazo legal não validado/);
change('Opção de preparação PEC', 'single');
change('Mês previsto do PEC único', '2026-06');
assert.equal(p.june, '10000');
assert.match(text(render()), /40000/);
change('Opção de preparação PEC', 'three');
assert.equal(p.singleMonth, '2026-06');
assert.match(text(render()), /15000/);
const html = renderToStaticMarkup(render());
assert.match(html, /Alterar projecção PEC/);
assert.match(html, /guia/);
});
+99
View File
@@ -0,0 +1,99 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
newPecPlan,
calculatePecPlan,
validatePecPlan,
} from '../../resources/js/lib/irpc-pec-plan.ts';
import { calculatePecObligation } from '../../resources/js/lib/irpc-pec-obligation.ts';
import { plan as obligation } from './helpers/irpc-pec-obligation-case.mjs';
import { combinedCase } from './helpers/irpc-investment-combination-case.mjs';
import {
recordAnnual,
annualCurrent,
annualBook,
saveAnnualBook,
annualHtml,
} from '../../resources/js/lib/irpc-annual-settlement.ts';
const f = (n) => ({ numerator: String(n), denominator: '1' }),
three = () => ({
...newPecPlan(),
mode: 'three',
reference: 'SYNTHETIC proposed allocation29(1)',
june: '10000',
august: '15000',
october: '15000',
}),
calc = (p, total = f(40000), na = false) =>
calculatePecPlan(p, total, na, '2026');
test('three explicit positive instalments reconcile exact total without inventing equal allocation', () => {
const m = calc(three());
assert.deepEqual(m.pending, []);
assert.deepEqual(
m.rows.map((r) => r.month),
['2026-06', '2026-08', '2026-10'],
);
assert.deepEqual(
m.rows.map((r) => r.amount),
[f(10000), f(15000), f(15000)],
);
assert.equal(m.legalDeadlineValidated, false);
assert.equal(m.paymentExecuted, false);
assert.equal(m.annualDeductionCreated, false);
});
test('single option records chosen month and entire obligation but does not certify deadline', () => {
const m = calc({ ...three(), mode: 'single', singleMonth: '2026-06' });
assert.deepEqual(m.pending, []);
assert.deepEqual(m.rows, [{ month: '2026-06', amount: f(40000) }]);
assert.equal(m.legalDeadlineValidated, false);
});
test('unknown,total changed,zero,not applicable,invalid month and mismatched cents never release a reconciled schedule', () => {
for (const p of [
{ ...three(), june: '' },
{ ...three(), june: '9999.99' },
{ ...three(), june: '10000.01' },
{ ...three(), june: '0' },
{ ...three(), reference: '' },
{ ...three(), mode: 'single', singleMonth: '2025-06' },
]) {
const m = calc(p);
assert.ok(m.pending.length);
assert.equal(m.allocationReconciled, false);
assert.deepEqual(m.rows, []);
}
for (const t of [null, f(0), f(50000)])
assert.ok(calc(three(), t).pending.length);
assert.ok(calc(three(), f(40000), true).pending.length);
assert.throws(() => validatePecPlan({ ...three(), june: null }));
});
test('schedule preserves explicit option in native book and invalidates annual snapshot on edits without new paid deductions', () => {
const { d, b } = combinedCase();
b.pecObligation = { ...obligation(), subjectNuit: b.subjectNuit };
let current = recordAnnual(d, b, '2026-11-11T11:00:00Z');
current.pecObligation.plan = three();
assert.equal(annualCurrent(d, current), false);
current = recordAnnual(d, current, '2026-11-11T11:01:00Z');
d.modelo_a_snapshot = saveAnnualBook(d, current);
const back = annualBook(d);
assert.equal(annualCurrent(d, back), true);
assert.deepEqual(back.result.model.arithmeticBalance, f(138));
assert.deepEqual(back.pecPayments, b.pecPayments);
assert.equal(back.history.length, 1);
assert.match(annualHtml(d), /PEC — projecção de tesouraria/);
back.pecObligation.previousTurnover = '20000000';
assert.ok(
calculatePecPlan(
back.pecObligation.plan,
calculatePecObligation(
back.pecObligation,
back.subjectNuit,
back.year,
back.assessment,
).total,
false,
'2026',
).pending.length,
);
assert.equal(annualCurrent(d, back), false);
});
@@ -84,7 +84,7 @@ function harness(c) {
};
}
test('actual progressive PPC form reuses subject, retains deliberate edits and exposes calendar separately from paid proofs',()=>{
const c=investmentCase(),h=harness(c);h.click('Preparar obrigação PPC');assert.equal(h.book().ppcObligation.subjectNuit,c.b.subjectNuit);assert.equal(h.book().ppcObligation.previousTax,'');
const c=investmentCase(),h=harness(c);h.click('Simular PPC');assert.equal(h.book().ppcObligation.subjectNuit,c.b.subjectNuit);assert.equal(h.book().ppcObligation.previousTax,'');
h.change('Enquadramento PPC','ordinary_resident');h.change('Exercício anterior — PPC','2025');h.change('Exercício anterior civil completo','yes');h.change('Imposto liquidado anterior21(1) — MZN','100001');h.change('Retenções na fonte anteriores — MZN','10000');h.change('Fonte da liquidação anterior — PPC','SYNTHETIC tax');h.change('Fonte das retenções anteriores — PPC','SYNTHETIC RF');
assert.match(text(h.render()),/75000/);assert.match(text(h.render()),/2026-05/);assert.deepEqual(h.book().ppc,c.b.ppc);
h.change('Enquadramento PPC','other');assert.equal(h.book().ppcObligation.previousTax,'100001');assert.match(text(h.render()),/Outros períodos/);h.change('Enquadramento PPC','ordinary_resident');assert.match(text(h.render()),/75000/);
+1 -1
View File
@@ -82,6 +82,6 @@ test('preparation survives existing book, versions/history/report; obligation ne
assert.deepEqual(recovered.ppc, b.ppc);
assert.equal(annualCurrent(d, recovered), true);
assert.equal(recovered.history.length, 1);
assert.match(annualHtml(d), /PPC — obrigação preparada/);
assert.match(annualHtml(d), /PPC — estimativa fiscal/);
assert.match(annualHtml(d), /2026-05/);
});
+2 -2
View File
@@ -69,8 +69,8 @@ for (const [name, changes] of Object.entries(scenarios)) {
selectedSections: new Set(['finance']),
generatedAt: '2026-11-11T10:02:00Z',
});
assert.match(html, /PEC — obrigação preparada/);
assert.match(JSON.stringify(report), /PEC — obrigação preparada/);
assert.match(html, /PEC — estimativa fiscal/);
assert.match(JSON.stringify(report), /PEC — estimativa fiscal/);
assert.equal(m.paymentExecuted, false);
assert.equal(m.annualDeductionCreated, false);
const expected = {
+116
View File
@@ -0,0 +1,116 @@
import {
calculatePecPlan,
newPecPlan,
} from '../resources/js/lib/irpc-pec-plan.ts';
import fs from 'node:fs';
import assert from 'node:assert/strict';
import { combinedCase } from '../tests/frontend/helpers/irpc-investment-combination-case.mjs';
import { plan } from '../tests/frontend/helpers/irpc-pec-obligation-case.mjs';
import { calculatePecObligation } from '../resources/js/lib/irpc-pec-obligation.ts';
import {
recordAnnual,
saveAnnualBook,
annualBook,
annualCurrent,
annualHtml,
} from '../resources/js/lib/irpc-annual-settlement.ts';
import { createProjectDossierReport } from '../resources/js/lib/project-dossier-report.ts';
const out = new URL('../outputs/irpc-pec-plan-20261111/', import.meta.url);
fs.mkdirSync(out, { recursive: true });
const allocation = {
...newPecPlan(),
mode: 'three',
reference: 'SYNTHETIC cash-flow hypothesis for viability',
june: '10000',
august: '15000',
october: '15000',
};
const scenarios = {
three: { plan: allocation },
single: { plan: { ...allocation, mode: 'single', singleMonth: '2026-06' } },
mismatch: { plan: { ...allocation, october: '14000' } },
changed_total: { previousTurnover: '20000000', plan: allocation },
};
for (const [name, changes] of Object.entries(scenarios)) {
if (process.argv[2] === 'generate') {
const { d, b } = combinedCase();
const prior = recordAnnual(d, b, '2026-11-11T10:00:00Z');
prior.pecObligation = {
...plan(),
subjectNuit: b.subjectNuit,
year: b.year,
...changes,
};
const next = recordAnnual(d, prior, '2026-11-11T10:01:00Z');
d.modelo_a_snapshot = saveAnnualBook(d, next);
fs.writeFileSync(new URL(name + '.json', out), JSON.stringify(d));
} else {
const d = JSON.parse(
fs.readFileSync(
new URL('recovered-' + name + '.json', out),
'utf8',
),
),
b = annualBook(d),
m = calculatePecObligation(
b.pecObligation,
b.subjectNuit,
b.year,
b.assessment,
);
assert.equal(annualCurrent(d, b), true);
assert.equal(b.history.length, 1);
assert.deepEqual(b.result.model.arithmeticBalance, {
numerator: '138',
denominator: '1',
});
const html = annualHtml(d),
report = createProjectDossierReport(d, {
selectedSections: new Set(['finance']),
generatedAt: '2026-11-11T10:02:00Z',
});
assert.match(html, /PEC — estimativa fiscal/);
assert.match(JSON.stringify(report), /PEC — estimativa fiscal/);
assert.equal(m.paymentExecuted, false);
assert.equal(m.annualDeductionCreated, false);
const allocationModel = calculatePecPlan(
b.pecObligation.plan,
m.total,
m.notApplicable,
b.year,
);
assert.equal(
allocationModel.allocationReconciled,
['three', 'single'].includes(name),
);
assert.equal(allocationModel.legalDeadlineValidated, false);
assert.equal(allocationModel.paymentExecuted, false);
assert.equal(allocationModel.annualDeductionCreated, false);
assert.match(html, /PEC — projecção de tesouraria/);
assert.match(JSON.stringify(report), /PEC — projecção de tesouraria/);
fs.writeFileSync(new URL(name + '-report.html', out), html);
fs.writeFileSync(
new URL(name + '-verification.json', out),
JSON.stringify(
{
synthetic: true,
apiRecovered: true,
current: true,
historyPreserved: true,
annualBalanceUnchanged: true,
report: true,
visualReview: false,
model: m,
allocationModel,
},
null,
2,
),
);
}
}
console.log(
process.argv[2] === 'generate'
? 'Four synthetic PEC cash-flow scenarios prepared.'
: 'Four native recovered PEC cash-flow scenarios recalculated and reported without creating deductions.',
);
+26
View File
@@ -0,0 +1,26 @@
<?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-pec-plan-20261111/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.');
}
$argv = ['phpunit', '-c', 'phpunit.investmoz.xml', '--do-not-cache-result', 'tests/Feature/IrpcPecPlanDraftTest.php'];
$argc = count($argv);
$_SERVER['argv'] = $argv;
$_SERVER['argc'] = $argc;
require $root.'/vendor/autoload.php';
require $root.'/vendor/phpunit/phpunit/phpunit';
+2 -2
View File
@@ -61,8 +61,8 @@ for (const [name, changes] of Object.entries(scenarios)) {
selectedSections: new Set(['finance']),
generatedAt: '2026-11-11T10:02:00Z',
});
assert.match(html, /PPC — obrigação preparada/);
assert.match(JSON.stringify(report), /PPC — obrigação preparada/);
assert.match(html, /PPC — estimativa fiscal/);
assert.match(JSON.stringify(report), /PPC — estimativa fiscal/);
assert.equal(m.paymentExecuted, false);
assert.equal(m.annualDeductionCreated, false);
if (name === 'ordinary') assert.equal(m.total.numerator, '72000');