Merge remote-tracking branch 'origin/main' into qa/editorial-fase1-20261113
tests / ci (push) Waiting to run

This commit is contained in:
2026-11-14 09:12:25 +02:00
17 changed files with 674 additions and 52 deletions
@@ -10004,3 +10004,27 @@ A dispensa PEC do ano de início documentado mantém zero nos períodos válidos
QA iniciada em13/11 e fechada em14/11:61testes frontend afectados, incluindo oito novos de período/edição/recuperação/consumidores. Prova nativa POST/PUT/GET com42asserções, quatro cenários e rollback na base isolada investmoz_demography_test:55441. Civil completo mantém112000MZN de projecção; não civil e curto ficam por reconciliar; início preserva PECzero e PPC separado. Nos quatro cenários, o resultado anual independente coincide com o exemplar anterior sem as preparações e o histórico mantém-se. Originais L6 e datas são produzidos pelo percurso normal existente, sem fabricar um calendário oficial. Relatório financeiro do mesmo dossier conferido. Tipos, lint/formatação focados, Pint/sintaxe PHP e build QA aprovados. Edge:componentes reais compilados, quatro contextos, dispensa e aviso específico PPC, ecrã/relatório concordantes. Prévia sintética sem API ou base de dados. Provas aceites dos percursos não afectados reutilizadas; dependências financeiras e exportadores conferidos idênticos a95b33275. Provas:outputs/irpc-period-consistency-20261113/.
Estado:implementado e verificado em QA, sem instalação. Homologação mantém95b33275. Lote único para estimadores, projecção, recuperação e relatório. Encerra a lacuna de coerência do período conhecido; não implementa ainda os cálculos/calendários dos períodos ou regimes especiais nem a anualização PPC28(3). L8 e portal permanecem parciais; resíduos L1–L7/restanteL8/L9–L16 e módulos mantidos. Sem pagamentos, novas deduções, migrações, importações ou permissões. Instalação exige autorização concreta e o executor Git existente, com backup/restauro fresco e reversão de release preservando snapshots/edições posteriores.
## 14/11/2026 — coerência dos períodos fiscais instalada em 078f770d
Instalação expressamente autorizada, concluída pelo executor Git existente na homologação https://teste-investidor.tempo.co.mz. Revisão servida078f770dea3d901624e2d9d8566a2d0f30f5acfe. Inclui coerência entre o período preparado em L6, estimativas PPC/PEC, projecção e relatório; contextos fora do recorte civil ficam por reconciliar, com a dispensa PEC inicial preservada. Sem alteração dos totais do negócio, pagamentos, deduções ou histórico.
Backup/restauro fresco de193tabelas/415ficheiros,37fontes conferidas por hash e21tabelas protegidas sem diferenças. Mantidos instituições, dossiers, documentos, simulações, Educação, mapa, relatórios, pesquisa, suporte, permissões e12lotes/920456observações demográficas. Zero migrações/importações; executor e configuração intactos. Readiness demográfica vinculada à revisão com original arquivado. HTTPS e revisão pública conferidos; CI obrigatório executado normalmente. Provas aceites reutilizadas sem nova ronda manual de percursos. Recibo:outputs/irpc-period-consistency-20261113/homologation-installed.json. Pacote agrupado selado inalterado.
Este lote deixa de ser pendência de instalação. Não encerra o simulador ou o portal: períodos/regimes especiais, anualização28(3), casos documentais e resíduos L1–L7/restanteL8/L9–L16 e módulos mantêm o saldo próprio. Próximo lote compatível: períodos anuais completos não civis documentados, preservando a preparação e o cálculo anual independente.
## 14/11/2026 — PPC/PEC em períodos anuais não civis documentados, QA
Continuidade executável de AUD-SIM-INT02/INT06 após078f770d instalado. Reutiliza L6, os calculadores PPC/PEC, o livro privado, o histórico, a projecção conjunta e os relatórios existentes. Acrescenta uma correspondência documental no mesmo livro anual: titular, exercício, datas e fundamento do período corrente, datas e fonte do período anterior e confirmação explícita dos montantes/fontes. As datas anteriores sugeridas não equivalem a comprovação. Nenhuma autorização fiscal é produzida pelo portal.
Recorte: residente geral/MZN, actividade comercial/industrial/agrícola, período corrente não civil com doze meses civis completos e termo2026, anterior igualmente completo, contíguo e com termo2025. Fonte aceite CIRPC7(2),Lei34/2007,BR52,original SHA256cdbef875ad7ccc1517b12faf5de0e893a70ed2ed81e33913cd6dc4a674474e6e; meses RIRPC27(1)(a)/29(1),BR16/I16-04-2008,original SHA256fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee. PPC nos5.º/7.º/9.ºmeses; PEC nos6.º/8.º/10.ºmeses. Fórmulas e aritmética racional reutilizadas, sem segundo motor ou novos parâmetros oficiais.
O PEC exige actividade documentada anterior ao início do período anterior neste recorte. Início não civil, volume anterior zero com histórico29(5), períodos curtos, meses incompletos, outros regimes e anualização PPC28(3) mantêm-se separados e pendentes. A opção de mês único só aceita um mês dentro do período documentado, como hipótese de tesouraria e sem certificar prazo legal.
Mudanças de datas/fontes não substituem conclusões ou valores. A correspondência financeira tem marcas independentes PPC/PEC: alterar PPC mantém PEC conhecido e vice-versa; o total fica parcial até à reconferência explícita. A distribuição PEC antiga é conservada sem deslocamento automático para meses não civis; exige vinculação expressa às novas datas, preservando montantes, opção e referência. O período usado e a declaração ficam congelados no resultado guardado, sem mudar o cálculo anual independente ou o histórico. Sem alteração global dos livros civis aceites.
QA:70testes frontend afectados distintos, incluindo nove novos de domínio/UI; sucesso, falta de fonte, divergência de titular/datas, período anterior curto/não contíguo, reconferência independente, preservação da distribuição, mês único e histórico. API nativa POST/PUT/GET:quatro cenários sintéticos,46asserções, rollback em investmoz_demography_test:55441, identidade PDO conferida. Cálculo→gravação→recuperação→relatório financeiro do mesmo dossier demonstrados. Tipos, lint focado, Pint e compilação aprovados. Edge com componentes reais compilados:112000MZN nos meses relativos correctos, concentração emJaneiro2026, total indeterminado sem fonte, PEC preservado quando PPC muda e193000MZN apenas após reconferência. Corrigido aviso contraditório de ano civil no caso documental incompleto; regressão acrescentada. Provas:outputs/irpc-noncivil-20261114/. Provas aceites não afectadas reutilizadas.
Estado:implementado e verificado em QA, ainda sem instalação. Homologação mantém078f770d. Este lote encerra apenas as estimativas PPC/PEC e a projecção deste recorte anual não civil documentado; não encerra L6/L8, a liquidação final não civil, o simulador ou o portal. Resíduos L1–L7/restanteL8/L9–L16 e outros módulos preservados na matriz. Sem migrações, permissões, rotas, importações, pagamentos, declarações submetidas ou novas deduções. Instalação agrupada depende de autorização concreta, executor Git, backup/restauro fresco e reversão de release preservando edições posteriores.
File diff suppressed because one or more lines are too long
@@ -9,6 +9,9 @@ import { SimulationIrpcInternational } from './simulation-irpc-international';
import { SimulationIrpcPec } from './simulation-irpc-pec';
import { useState } from 'react';
import {
advancePeriodBasis,
advanceCalendar,
newAdvancePeriodDeclaration,
advanceCashflowAmount,
annualAdvanceCashflow,
annualAdvancePeriod,
@@ -81,7 +84,7 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
);
const patch = (p: Partial<AnnualBook>) => save({ ...b, ...p }),
a = annualAssessment(draft),
advancePeriod = annualAdvancePeriod(draft),
advancePeriod = annualAdvancePeriod(draft,b),
options = annualWithholdingOptions(draft);
const field = (
key: 'subjectNuit' | 'year' | 'reference' | 'assessmentReference',
@@ -348,6 +351,7 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
<fieldset className="space-y-2 rounded border p-3">
<legend>Estimativas PPC e PEC para a viabilidade</legend>
<p>Simulação fiscal para avaliar a viabilidade do negócio e as necessidades de tesouraria. Os valores e meses previstos são estimativas; o portal não executa pagamentos nem submete declarações fiscais.</p>
<SimulationIrpcAdvancePeriodDeclaration draft={draft} book={b} onChange={declaration=>patch({advancePeriodDeclaration:declaration})}/>
<SimulationIrpcPecObligation book={b} period={advancePeriod} onChange={pecObligation=>patch({pecObligation})}/>
<SimulationIrpcPpcObligation book={b} period={advancePeriod} onChange={ppcObligation=>patch({ppcObligation})}/>
<SimulationIrpcAdvanceCashflow book={b} period={advancePeriod}/>
@@ -643,3 +647,13 @@ export function SimulationIrpcAdvanceCashflow({book,period}: {book: AnnualBook;p
<p>Meses e montantes dependem das fontes e opções preparadas acima. A opção PEC concentrada conserva o mês escolhido; esta apresentação não certifica o prazo legal.</p>
</section>;
}
export function SimulationIrpcAdvancePeriodDeclaration({draft,book,onChange}:{draft:Record<string,unknown>;book:AnnualBook;onChange:(value:NonNullable<AnnualBook['advancePeriodDeclaration']>)=>void}){
const period=annualAdvancePeriod(draft,book),calendar=advanceCalendar(period,book.year,book.subjectNuit),d=book.advancePeriodDeclaration;
if(!d&&!calendar.candidate)return null;
if(!d)return <section className="rounded border p-3"><h5>Período anual não civil</h5><p>Confronte o fundamento do período L6 e os doze meses do período anterior. As datas sugeridas não substituem o suporte documental.</p><button type="button" className="rounded border p-2" onClick={()=>onChange(newAdvancePeriodDeclaration(book.subjectNuit,book.year,period))}>Preparar correspondência dos períodos</button></section>;
const patch=(values:Partial<typeof d>)=>onChange({...d,...values,confirmed:''});
const labels={subjectNuit:'NUIT dos períodos',year:'Exercício de termo',currentFrom:'Início do período L6 declarado',currentTo:'Termo do período L6 declarado',currentReference:'Fundamento documentado do período anual não civil',previousFrom:'Início do período anterior',previousTo:'Termo do período anterior',previousReference:'Fonte e correspondência do período anterior'};
return <section className="space-y-2 rounded border p-3" aria-label="Correspondência dos períodos anuais"><h5>Correspondência dos períodos anuais</h5><p role="status">{calendar.mode==='annual_non_civil'?'Correspondência documental conferida;previsões nos meses do período L6.':calendar.mode==='civil'?'L6 usa o ano civil;referência anterior conservada sem aplicação.':calendar.pending.join(' ')}</p><p>PPC: imposto anterior {book.ppcObligation?.previousTax||'por confirmar'} MZN;retenções {book.ppcObligation?.previousWithholdings||'por confirmar'} MZN. PEC: volume anterior {book.pecObligation?.previousTurnover||'por confirmar'} MZN;PPC anteriores {book.pecObligation?.previousPpcPaid||'por confirmar'} MZN.</p>{d.confirmed==='yes'&&(d.ppcBasisKey!==advancePeriodBasis(book,'ppc')||d.pecBasisKey!==advancePeriodBasis(book,'pec'))&&<button type="button" className="rounded border p-2" onClick={()=>onChange({...d,ppcBasisKey:advancePeriodBasis(book,'ppc'),pecBasisKey:advancePeriodBasis(book,'pec')})}>Reconferi os montantes e as fontes neste período</button>}<details open={d.confirmed!=='yes'||calendar.pending.length>0}><summary>Conferir ou alterar períodos</summary><label className="block">Aplicação<select aria-label="Aplicação do período não civil" value={d.mode} onChange={e=>patch({mode:e.target.value as typeof d.mode})}><option value="annual_non_civil">Período anual não civil</option><option value="">Conservar sem aplicação</option></select></label>{(Object.keys(labels) as Array<keyof typeof labels>).map(key=><label className="block" key={key}>{labels[key]}<input className="mt-1 block w-full rounded border px-3 py-2" aria-label={labels[key]} value={d[key]} onChange={e=>patch({[key]:e.target.value})}/></label>)}<p>No PPC, indique que o período anterior não é um ano civil completo. Os doze meses e a continuidade são conferidos pelas datas e fontes aqui indicadas.</p><label className="block"><input type="checkbox" checked={d.confirmed==='yes'} onChange={e=>onChange({...d,confirmed:e.target.checked?'yes':'',ppcBasisKey:advancePeriodBasis(book,'ppc'),pecBasisKey:advancePeriodBasis(book,'pec')})}/>Conferi no suporte os dois períodos, o fundamento do enquadramento e a correspondência dos montantes e fontes PPC/PEC</label></details><p>Guardar conserva estas escolhas. Uma alteração nas datas L6 exige reconferência; o portal não autoriza uma mudança do período fiscal.</p></section>;
}
@@ -83,7 +83,7 @@ export function SimulationIrpcPecObligation({
<option value="">Por confirmar</option>
<option value="ordinary_resident">
Residente geral — comercial,industrial ou agrícola,ano
civil
civil ou anual não civil documentado
</option>
<option value="other">
Outro enquadramento — separado
@@ -280,6 +280,7 @@ export function SimulationIrpcPecObligation({
total={m.total}
notApplicable={m.notApplicable}
year={book.year}
period={period}
onChange={(plan) => patch({ plan })}
/>
<p>
@@ -1,7 +1,7 @@
import type { AdvanceFiscalPeriod } from '@/lib/irpc-ppc-obligation';
import {
newPecPlan,
calculatePecPlan,
pecPlanSource,
type PecPlan,
} from '@/lib/irpc-pec-plan';
import {
@@ -13,12 +13,14 @@ export function SimulationIrpcPecPlan({
total,
notApplicable,
year,
period,
onChange,
}: {
plan?: PecPlan;
total: FiscalFraction | null;
notApplicable: boolean;
year: string;
period?: AdvanceFiscalPeriod;
onChange: (p: PecPlan) => void;
}) {
if (!plan)
@@ -33,13 +35,13 @@ export function SimulationIrpcPecPlan({
disabled={
!total || notApplicable || BigInt(total.numerator) <= 0n
}
onClick={() => onChange(newPecPlan())}
onClick={() => onChange(newPecPlan(period))}
>
Simular tesouraria PEC
</button>
</div>
);
const m = calculatePecPlan(plan, total, notApplicable, year),
const m = calculatePecPlan(plan, total, notApplicable, year, period),
patch = (p: Partial<PecPlan>) => onChange({ ...plan, ...p });
const field = (
key: 'reference' | 'june' | 'august' | 'october',
@@ -77,6 +79,20 @@ export function SimulationIrpcPecPlan({
. Prazo legal não validado.
</p>
)}
{m.needsPeriodConfirmation && !m.calendarPending && (
<button
type="button"
className="rounded border p-2"
onClick={() =>
patch({
periodFrom: m.contextFrom,
periodTo: m.contextTo,
})
}
>
Conferi a distribuição para {m.contextFrom} — {m.contextTo}
</button>
)}
<details open={m.pending.length > 0}>
<summary>Alterar projecção PEC</summary>
<label className="block">
@@ -100,9 +116,30 @@ export function SimulationIrpcPecPlan({
{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')}
{field(
'june',
m.calendarPending
? 'PEC primeiro montante — mês por confirmar'
: m.nonCivil
? 'PEC 6.º mês (' + m.months[0] + ') — MZN'
: 'PEC Junho — MZN',
)}
{field(
'august',
m.calendarPending
? 'PEC segundo montante — mês por confirmar'
: m.nonCivil
? 'PEC 8.º mês (' + m.months[1] + ') — MZN'
: 'PEC Agosto — MZN',
)}
{field(
'october',
m.calendarPending
? 'PEC terceiro montante — mês por confirmar'
: m.nonCivil
? 'PEC 10.º mês (' + m.months[2] + ') — MZN'
: 'PEC Outubro — MZN',
)}
</>
)}
{plan.mode === 'single' && (
@@ -128,7 +165,7 @@ export function SimulationIrpcPecPlan({
</p>
<details>
<summary>Fonte do projecção PEC</summary>
<p>{pecPlanSource}</p>
<p>{m.source}</p>
</details>
</section>
);
@@ -22,7 +22,7 @@ export function SimulationIrpcPpcObligation({
<section className="rounded border p-3">
<h5>Estimativa PPC</h5>
<p>
Preparar a obrigação e os meses previstos não regista
Preparar a estimativa e os meses previstos não regista
pagamentos nem cria deduções na liquidação.
</p>
<button
@@ -89,7 +89,7 @@ export function SimulationIrpcPpcObligation({
<option value="">Por confirmar</option>
<option value="ordinary_resident">
Residente geral — actividade comercial, industrial ou
agrícola, ano civil
agrícola, período documentado
</option>
<option value="other">
Outro enquadramento — cálculo separado
+215
View File
@@ -0,0 +1,215 @@
export type AdvancePeriodDeclaration = {
version: 1;
mode: '' | 'annual_non_civil';
subjectNuit: string;
year: string;
currentFrom: string;
currentTo: string;
currentReference: string;
previousFrom: string;
previousTo: string;
previousReference: string;
ppcBasisKey: string;
pecBasisKey: string;
confirmed: '' | 'yes';
};
export type AdvanceFiscalPeriod = {
from?: string | null;
to?: string | null;
subjectNuit?: string;
ppcBasisKey?: string;
pecBasisKey?: string;
declaration?: AdvancePeriodDeclaration;
} | null;
export const advanceCalendarVersion = 'IRPC-annual-noncivil-calendar/1';
const iso = (s?: string | null): s is string =>
Boolean(
s &&
/^20\d{2}-\d{2}-\d{2}$/.test(s) &&
Number.isFinite(Date.parse(s)) &&
new Date(s).toISOString().slice(0, 10) === s,
);
const index = (s: string) =>
Number(s.slice(0, 4)) * 12 + Number(s.slice(5, 7)) - 1;
const month = (from: string, offset: number) =>
new Date(
Date.UTC(
Number(from.slice(0, 4)),
Number(from.slice(5, 7)) - 1 + offset,
1,
),
)
.toISOString()
.slice(0, 7);
const end = (s: string) =>
new Date(Date.UTC(Number(s.slice(0, 4)), Number(s.slice(5, 7)), 0))
.toISOString()
.slice(0, 10);
const previousDay = (s: string) =>
new Date(Date.parse(s) - 86400000).toISOString().slice(0, 10);
const annual = (from?: string | null, to?: string | null) =>
iso(from) &&
iso(to) &&
from.endsWith('-01') &&
to === end(to) &&
index(to) - index(from) === 11;
export function newAdvancePeriodDeclaration(
subjectNuit: string,
year: string,
period: AdvanceFiscalPeriod,
): AdvancePeriodDeclaration {
const from = period?.from ?? '',
to = period?.to ?? '';
return {
version: 1,
mode: 'annual_non_civil',
subjectNuit,
year,
currentFrom: from,
currentTo: to,
currentReference: '',
previousFrom: iso(from) ? month(from, -12) + '-01' : '',
previousTo: iso(from) ? previousDay(from) : '',
previousReference: '',
ppcBasisKey: '',
pecBasisKey: '',
confirmed: '',
};
}
export function validateAdvancePeriodDeclaration(d: AdvancePeriodDeclaration) {
if (
!d ||
d.version !== 1 ||
!['', 'annual_non_civil'].includes(d.mode) ||
!['', 'yes'].includes(d.confirmed) ||
[
'subjectNuit',
'year',
'currentFrom',
'currentTo',
'currentReference',
'previousFrom',
'previousTo',
'previousReference',
'ppcBasisKey',
'pecBasisKey',
].some(
(k) =>
typeof (d as unknown as Record<string, unknown>)[k] !==
'string',
)
)
throw Error(
'Correspondência de períodos não recuperável;original preservado.',
);
}
export function advanceCalendar(
period: AdvanceFiscalPeriod | undefined,
year: string,
subjectNuit?: string,
) {
const from = period?.from ?? '',
to = period?.to ?? '';
if (!/^20\d{2}$/.test(year))
return {
version: advanceCalendarVersion,
mode: 'unavailable' as const,
candidate: false,
pending: ['exercício das estimativas por confirmar'],
from,
to,
ppcMonths: [],
pecMonths: [],
previousFrom: '',
previousTo: '',
source: '',
};
const civil =
!period || (from === year + '-01-01' && to === year + '-12-31');
const candidate = annual(from, to) && to.slice(0, 4) === year && !civil;
let mode: 'civil' | 'annual_non_civil' | 'unavailable' = civil
? 'civil'
: 'unavailable';
let pending: string[] = [];
const d = period?.declaration;
if (!civil) {
if (d?.mode === 'annual_non_civil') {
validateAdvancePeriodDeclaration(d);
const issues: string[] = [];
if (!candidate)
issues.push(
'o período L6 deve conter doze meses civis completos com termo no exercício indicado',
);
if (
d.subjectNuit !== (subjectNuit ?? period?.subjectNuit) ||
d.year !== year ||
d.currentFrom !== from ||
d.currentTo !== to
)
issues.push(
'titular,exercício ou datas declaradas divergem de L6',
);
if (
!annual(d.previousFrom, d.previousTo) ||
!iso(from) ||
d.previousTo !== previousDay(from) ||
d.previousTo.slice(0, 4) !== String(Number(year) - 1)
)
issues.push(
'o período anterior deve ser anual completo e imediatamente anterior ao período L6',
);
if (
!d.currentReference.trim() ||
!d.previousReference.trim() ||
d.confirmed !== 'yes'
)
issues.push(
'faltam fontes e confirmação da correspondência dos dois períodos',
);
if (issues.length)
pending = [
'período anual não civil: ' +
issues.join('; ') +
'. Preparação conservada.',
];
else mode = 'annual_non_civil';
} else
pending = [
'o período L6 ' +
(from || 'por confirmar') +
' — ' +
(to || 'por confirmar') +
' não corresponde ao ano civil ' +
year +
'. Reconciliar o enquadramento antes de usar as estimativas ordinárias; a preparação fica conservada.',
];
}
const start = mode === 'civil' ? year + '-01-01' : from;
return {
version: advanceCalendarVersion,
mode,
candidate,
pending,
from: mode === 'civil' ? year + '-01-01' : from,
to: mode === 'civil' ? year + '-12-31' : to,
ppcMonths:
mode === 'unavailable' ? [] : [4, 6, 8].map((i) => month(start, i)),
pecMonths:
mode === 'unavailable' ? [] : [5, 7, 9].map((i) => month(start, i)),
previousFrom:
mode === 'annual_non_civil'
? d!.previousFrom
: String(Number(year) - 1) + '-01-01',
previousTo:
mode === 'annual_non_civil'
? d!.previousTo
: String(Number(year) - 1) + '-12-31',
source:
mode === 'annual_non_civil'
? 'CIRPC7(2),Lei34/2007,BR52,original SHA256cdbef875ad7ccc1517b12faf5de0e893a70ed2ed81e33913cd6dc4a674474e6e; RIRPC27(1)(a)/29(1),BR16/I16-04-2008,original SHA256fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee. Períodos anuais completos documentados;5.º/7.º/9.º mês PPC e6.º/8.º/10.º mês PEC. ' +
d!.currentReference +
'; ' +
d!.previousReference
: '',
};
}
+15 -5
View File
@@ -1,3 +1,5 @@
import {validateAdvancePeriodDeclaration,type AdvancePeriodDeclaration} from './irpc-advance-calendar.ts';
export {advanceCalendar,newAdvancePeriodDeclaration} from './irpc-advance-calendar.ts';
import { calculatePecPlan } from './irpc-pec-plan.ts';
import {pecCorrectionVersion} from './irpc-pec-correction.ts';
import {pecPlanVersion} from './irpc-pec-plan.ts';
@@ -127,6 +129,7 @@ export type AnnualBook = {
creditCarry?: CreditCarry;
economicCredits?: EconomicCredits;
investmentCredit?: InvestmentCredit;
advancePeriodDeclaration?: AdvancePeriodDeclaration;
ppcObligation?: PpcObligation;
pecObligation?: PecObligation;
result?: AnnualRecord;
@@ -298,6 +301,7 @@ function books(d: Record<string, unknown>): Record<string, AnnualBook> {
return m;
}
export function validateAnnual(b: AnnualBook) {
if(b?.advancePeriodDeclaration!==undefined)validateAdvancePeriodDeclaration(b.advancePeriodDeclaration);
if(b?.pecObligation!==undefined)validatePecObligation(b.pecObligation);
if (b?.ppcObligation !== undefined) validatePpcObligation(b.ppcObligation);
if (b?.investmentCredit !== undefined)
@@ -422,9 +426,15 @@ export function annualAssessment(d: Record<string, unknown>) {
: zero(),
};
}
export function annualAdvancePeriod(d: Record<string, unknown>): AdvanceFiscalPeriod {
export function advancePeriodBasis(b:AnnualBook,kind:'ppc'|'pec'):string {
let value:unknown=b.ppcObligation??null;
if(kind==='pec'){if(b.pecObligation){const {plan:_,...basis}=b.pecObligation;value=basis;}else value=null;}
return closureFingerprint([b.subjectNuit,b.year,b.assessment,value]);
}
export function annualAdvancePeriod(d: Record<string, unknown>,b?: AnnualBook): AdvanceFiscalPeriod {
const prepared = normalBook(d).periodsFx;
return prepared ? {from: prepared.from, to: prepared.to} : annualAssessment(d).period;
const dates=prepared ? {from: prepared.from, to: prepared.to} : annualAssessment(d).period;
return b?.advancePeriodDeclaration ? {...dates,subjectNuit:b.subjectNuit,ppcBasisKey:advancePeriodBasis(b,'ppc'),pecBasisKey:advancePeriodBasis(b,'pec'),declaration:structuredClone(b.advancePeriodDeclaration)} : dates;
}
export function reuseAnnualAssessment(
@@ -574,7 +584,7 @@ function dependencies(d: Record<string, unknown>, includeInvestment = false) {
}
function key(d: Record<string, unknown>, b: AnnualBook) {
const { result: _, history: __, ...input } = b;
const period = annualAdvancePeriod(d);
const period = annualAdvancePeriod(d,b);
const periodGuard = ordinaryAdvancePeriodIssues(period, b.year).length > 0 &&
(b.ppcObligation || b.pecObligation && !calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,period).notApplicable);
return closureFingerprint([
@@ -1603,7 +1613,7 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
return {
version: annualSettlementVersion,
input: structuredClone(input),
...(b.ppcObligation || b.pecObligation ? {advancePeriod: annualAdvancePeriod(d)} : {}),
...(b.ppcObligation || b.pecObligation ? {advancePeriod: annualAdvancePeriod(d,b)} : {}),
subjectNuit: b.subjectNuit,
year: b.year,
assessment: assessment?.record
@@ -2445,7 +2455,7 @@ export function annualAdvanceCashflow(b: Pick<AnnualBook, 'subjectNuit' | 'year'
if (BigInt(p.total.numerator) === 0n) pecKnown = true;
else if (!b.pecObligation.plan) pending.push('Distribua a hipótese de tesouraria PEC para completar os meses da projecção conjunta.');
else {
const allocation = calculatePecPlan(b.pecObligation.plan, p.total, p.notApplicable, b.year);
const allocation = calculatePecPlan(b.pecObligation.plan, p.total, p.notApplicable, b.year, period);
sources.push(b.pecObligation.plan.reference, allocation.source);
pending.push(...allocation.pending);
pecKnown = allocation.allocationReconciled;
+58 -6
View File
@@ -1,3 +1,4 @@
import { advanceCalendar } from './irpc-advance-calendar.ts';
import {
ordinaryAdvancePeriodIssues,
type AdvanceFiscalPeriod,
@@ -110,6 +111,7 @@ export function calculatePecObligation(
) {
validatePecObligation(p);
const pending: string[] = [];
const calendar = advanceCalendar(period, year, subjectNuit);
const turnover = correctedPecTurnover(
p.correction,
p.previousTurnover,
@@ -158,14 +160,43 @@ export function calculatePecObligation(
period.from! <= period.to!
)
)
pending.push(...ordinaryAdvancePeriodIssues(period, year, 'PEC'));
pending.push(
...ordinaryAdvancePeriodIssues(
period,
year,
'PEC',
subjectNuit,
),
);
if (calendar.mode === 'annual_non_civil')
pending.push(
'PEC: início de actividade em período não civil exige correspondência própria;fora deste recorte de continuidade.',
);
if (!pending.length) {
notApplicable = true;
total = fraction(0n);
}
} else if (!pending.length) {
pending.push(...ordinaryAdvancePeriodIssues(period, year, 'PEC'));
pending.push(
...ordinaryAdvancePeriodIssues(period, year, 'PEC', subjectNuit),
);
if (
calendar.mode === 'annual_non_civil' &&
(!period?.pecBasisKey ||
period.declaration?.pecBasisKey !== period.pecBasisKey)
)
pending.push(
'PEC: montantes ou fontes alterados;reconfira a sua correspondência com o período anterior,sem substituir os valores.',
);
pending.push(...turnover.pending);
if (
calendar.mode === 'annual_non_civil' &&
Number(p.activityStartYear) >=
Number(calendar.previousFrom.slice(0, 4))
)
pending.push(
'PEC: este recorte exige actividade já iniciada antes do período anterior documentado;início ou transição têm correspondência própria.',
);
if (
p.previousYear !== '2025' ||
!p.turnoverReference.trim() ||
@@ -185,7 +216,14 @@ export function calculatePecObligation(
if (!pending.length) {
const v = decimalFraction(turnover.value),
ppc = decimalFraction(p.previousPpcPaid);
if (BigInt(v.numerator) === 0n) {
if (
BigInt(v.numerator) === 0n &&
calendar.mode === 'annual_non_civil'
) {
pending.push(
'PEC29(5): último montante num período não civil exige correspondência da sua origem;fora deste recorte de continuidade.',
);
} else if (BigInt(v.numerator) === 0n) {
const last = p.lastAmount;
if (
!last ||
@@ -235,12 +273,27 @@ export function calculatePecObligation(
source.replace('base de anos anteriores29(5),', '') +
' ' +
pecLastAmountSource;
if (calendar.mode === 'annual_non_civil')
source =
source
.replace(
'ano civil,MZN',
'período anual não civil completo,MZN',
)
.replace('Junho/Agosto/Outubro', '6.º/8.º/10.º mês do período')
.replace(
'Regimes/períodos especiais',
'Períodos curtos e outros regimes',
) +
' ' +
calendar.source;
if (ppcExcess)
source +=
' Reconciliação da estimativa29(2):PPC documentados cobrem a base limitada;diferença conservada sem gerar direito a reembolso ou crédito.';
return {
version: pecObligationVersion,
source,
nonCivil: calendar.mode === 'annual_non_civil',
lastAmountSource: p.lastAmount ? pecLastAmountSource : null,
pending,
percentage,
@@ -263,8 +316,7 @@ export function calculatePecObligation(
source: pecCorrectionSource,
}
: null,
months:
total && !notApplicable ? ['2026-06', '2026-08', '2026-10'] : [],
months: total && !notApplicable ? calendar.pecMonths : [],
instalmentAmountsDetermined: false,
paymentExecuted: false,
annualDeductionCreated: false,
@@ -359,7 +411,7 @@ export function pecObligationReport(
]
: []),
...(p.plan
? pecPlanReport(p.plan, m.total, m.notApplicable, year)
? pecPlanReport(p.plan, m.total, m.notApplicable, year, period)
: []),
[
'IRPC — L8',
+50 -5
View File
@@ -1,3 +1,7 @@
import {
advanceCalendar,
type AdvanceFiscalPeriod,
} from './irpc-advance-calendar.ts';
import {
decimalFraction,
addFractions,
@@ -10,6 +14,8 @@ 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;
periodFrom?: string;
periodTo?: string;
mode: '' | 'three' | 'single';
reference: string;
singleMonth: string;
@@ -17,7 +23,10 @@ export type PecPlan = {
august: string;
october: string;
};
export const newPecPlan = (): PecPlan => ({
export const newPecPlan = (period?: AdvanceFiscalPeriod): PecPlan => ({
...(period?.from && period?.to
? { periodFrom: period.from, periodTo: period.to }
: {}),
version: 1,
mode: '',
reference: '',
@@ -27,6 +36,13 @@ export const newPecPlan = (): PecPlan => ({
october: '',
});
export function validatePecPlan(p: PecPlan) {
if (
(p?.periodFrom !== undefined || p?.periodTo !== undefined) &&
(typeof p.periodFrom !== 'string' || typeof p.periodTo !== 'string')
)
throw Error(
'Contexto da hipótese PEC não recuperável;original preservado.',
);
if (
!p ||
p.version !== 1 ||
@@ -46,9 +62,19 @@ export function calculatePecPlan(
total: FiscalFraction | null,
notApplicable: boolean,
year: string,
period?: AdvanceFiscalPeriod,
) {
validatePecPlan(p);
const pending: string[] = [];
const calendar = advanceCalendar(period, year);
pending.push(...calendar.pending.map((p) => 'Projecção PEC: ' + p));
const needsPeriodConfirmation =
(calendar.mode === 'annual_non_civil' || p.periodFrom !== undefined) &&
(p.periodFrom !== calendar.from || p.periodTo !== calendar.to);
if (needsPeriodConfirmation)
pending.push(
'Projecção PEC: reconfira a distribuição no período actual;montantes e mês anterior conservados sem deslocação automática.',
);
let rows: Array<{ month: string; amount: FiscalFraction }> = [];
if (!total)
pending.push(
@@ -92,11 +118,15 @@ export function calculatePecPlan(
);
else
rows = amounts.map((amount, i) => ({
month: ['2026-06', '2026-08', '2026-10'][i],
month: calendar.pecMonths[i],
amount,
}));
}
} else if (!/^2026-(0[1-9]|1[0-2])$/.test(p.singleMonth))
} else if (
!/^20\d{2}-(0[1-9]|1[0-2])$/.test(p.singleMonth) ||
p.singleMonth < calendar.from.slice(0, 7) ||
p.singleMonth > calendar.to.slice(0, 7)
)
pending.push(
'Projecção PEC:indique o mês previsto do pagamento único;isto não valida o prazo legal.',
);
@@ -104,7 +134,21 @@ export function calculatePecPlan(
}
return {
version: pecPlanVersion,
source: pecPlanSource,
source:
calendar.mode === 'annual_non_civil'
? pecPlanSource.replace(
'mesesJunho/Agosto/Outubro',
'6.º/8.º/10.º mês do período anual não civil',
) +
' ' +
calendar.source
: pecPlanSource,
months: calendar.pecMonths,
needsPeriodConfirmation,
contextFrom: calendar.from,
contextTo: calendar.to,
calendarPending: calendar.mode === 'unavailable',
nonCivil: calendar.mode === 'annual_non_civil',
mode: p.mode,
pending,
rows,
@@ -120,8 +164,9 @@ export function pecPlanReport(
total: FiscalFraction | null,
notApplicable: boolean,
year: string,
period?: AdvanceFiscalPeriod,
): Array<[string, string, string, string]> {
const m = calculatePecPlan(p, total, notApplicable, year);
const m = calculatePecPlan(p, total, notApplicable, year, period);
return [
[
'IRPC — L8',
+48 -23
View File
@@ -6,28 +6,20 @@ import {
type FiscalFraction,
} from './fiscal-rational.ts';
export type AdvanceFiscalPeriod = {
from?: string | null;
to?: string | null;
} | null;
export type { AdvanceFiscalPeriod } from './irpc-advance-calendar.ts';
import {
advanceCalendar,
type AdvanceFiscalPeriod,
} from './irpc-advance-calendar.ts';
export function ordinaryAdvancePeriodIssues(
period: AdvanceFiscalPeriod | undefined,
year: string,
component: 'PPC' | 'PEC' | 'PPC/PEC' = 'PPC/PEC',
subjectNuit?: string,
): string[] {
if (!period) return [];
return period.from === year + '-01-01' && period.to === year + '-12-31'
? []
: [
component +
': o período L6 ' +
(period.from || 'por confirmar') +
' — ' +
(period.to || 'por confirmar') +
' não corresponde ao ano civil ' +
year +
'. Reconciliar o enquadramento antes de usar as estimativas ordinárias; a preparação fica conservada.',
];
return advanceCalendar(period, year, subjectNuit).pending.map(
(message) => component + ': ' + message,
);
}
export const ppcObligationVersion = 'IRPC-PPC-obligation/1';
@@ -85,13 +77,16 @@ export function calculatePpcObligation(
) {
validatePpcObligation(p);
const pending: string[] = [];
const calendar = advanceCalendar(period, year, subjectNuit);
const noncivilPreparation =
calendar.candidate && period?.declaration?.mode === 'annual_non_civil';
let base: FiscalFraction | null = null,
annualBeforeRounding: FiscalFraction | null = null,
instalment: FiscalFraction | null = null;
if (
p.scope !== 'ordinary_resident' ||
assessment !== 'normal' ||
p.previousFullCalendarYear !== 'yes' ||
p.previousFullCalendarYear !== (noncivilPreparation ? 'no' : 'yes') ||
year !== '2026' ||
p.year !== year ||
p.previousYear !== '2025' ||
@@ -99,7 +94,9 @@ export function calculatePpcObligation(
p.subjectNuit !== subjectNuit
)
pending.push(
'PPC: confirmar titular,2026,residente geral com actividade comercial/industrial/agrícola e exercício civil completo2025. Outros períodos/regimes e anualização permanecem separados.',
noncivilPreparation
? 'PPC: confirmar titular2026 e período anterior anual não civil com termo2025;indique Não em ano civil completo e confronte a correspondência documentada.'
: 'PPC: confirmar titular,2026,residente geral com actividade comercial/industrial/agrícola e exercício civil completo2025. Outros períodos/regimes e anualização permanecem separados.',
);
if (!p.assessmentReference.trim() || !p.withholdingReference.trim())
pending.push(
@@ -113,7 +110,17 @@ export function calculatePpcObligation(
pending.push(
'PPC: montantes anteriores em MZN desconhecidos ou inválidos; zero deve ser explícito.',
);
pending.push(...ordinaryAdvancePeriodIssues(period, year, 'PPC'));
pending.push(
...ordinaryAdvancePeriodIssues(period, year, 'PPC', subjectNuit),
);
if (
calendar.mode === 'annual_non_civil' &&
(!period?.ppcBasisKey ||
period.declaration?.ppcBasisKey !== period.ppcBasisKey)
)
pending.push(
'PPC: montantes ou fontes alterados;reconfira a sua correspondência com o período anterior,sem substituir os valores.',
);
if (!pending.length) {
base = subtractFractions(
decimalFraction(p.previousTax),
@@ -135,7 +142,25 @@ export function calculatePpcObligation(
}
return {
version: ppcObligationVersion,
source: ppcObligationSource,
source:
calendar.mode === 'annual_non_civil'
? ppcObligationSource
.replace(
'ano civil e exercício anterior completo2025',
'períodos anuais completos não civis com termo2026/2025',
)
.replace(
'Maio,Julho,Setembro',
'5.º/7.º/9.º mês do período',
)
.replace(
'Períodos especiais,',
'Períodos curtos e outros regimes,',
) +
' ' +
calendar.source
: ppcObligationSource,
nonCivil: calendar.mode === 'annual_non_civil',
pending,
base,
annualBeforeRounding,
@@ -147,7 +172,7 @@ export function calculatePpcObligation(
)
: null,
schedule: instalment
? ['2026-05', '2026-07', '2026-09'].map((month) => ({
? calendar.ppcMonths.map((month) => ({
month,
amount: instalment,
}))
@@ -184,7 +209,7 @@ export function ppcObligationReport(
m.schedule
.map((s) => s.month + ': ' + show(s.amount))
.join('; ') +
'. Previsão de obrigação; não é pagamento nem dedução anual.',
'. Estimativa fiscal para viabilidade; não é pagamento nem dedução anual.',
p.assessmentReference +
'; ' +
p.withholdingReference +
@@ -0,0 +1,51 @@
<?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 IrpcAnnualNoncivilDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_period_survives_native_recovery_without_changing_the_independent_annual_result(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('investmoz_demography_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$actor = User::factory()->create();
$this->actingAs($actor);
$id = null;
$etag = null;
foreach (['three', 'single', 'missing', 'changed'] as $name) {
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-noncivil-20261114/'.$name.'.json')), true, flags: JSON_THROW_ON_ERROR);
$personal = ['person_type' => $snapshot['person_type']];
unset($snapshot['person_type']);
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $snapshot];
if ($id === null) {
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
} else {
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
}
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
$etag = $response->headers->get('ETag');
$back = $response->json('data.economic_snapshot');
$this->assertSame($snapshot['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$book = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$snapshot['context_opportunity']];
$this->assertCount(1, $book['history']);
$this->assertArrayHasKey('advancePeriod', $book['result']['model']);
$this->assertSame($book['history'][0]['model']['arithmeticBalance'], $book['result']['model']['arithmeticBalance']);
$this->assertSame($book['pecObligation'], $book['result']['model']['input']['pecObligation']);
$this->assertSame($book['ppcObligation'], $book['result']['model']['input']['ppcObligation']);
$this->assertSame('2026', $book['ppcObligation']['year']);
$this->assertSame($book['advancePeriodDeclaration'], $book['result']['model']['input']['advancePeriodDeclaration']);
$this->assertSame('SYNTHETIC effective activity2020', $book['pecObligation']['activityReference']);
file_put_contents(base_path('outputs/irpc-noncivil-20261114/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,11 @@
import {periodAdvanceCase} from './irpc-advance-period-case.mjs';
import {newAdvancePeriodDeclaration,advancePeriodBasis,annualAdvancePeriod} from '../../../resources/js/lib/irpc-annual-settlement.ts';
import {newPecPlan} from '../../../resources/js/lib/irpc-pec-plan.ts';
export function noncivilCase(){
const c=periodAdvanceCase('noncivil');c.b.ppcObligation.previousFullCalendarYear='no';
c.b.advancePeriodDeclaration={...newAdvancePeriodDeclaration(c.b.subjectNuit,c.b.year,c.period),currentReference:'SYNTHETIC documented authorised annual period CIRPC7(2), continued activity',previousReference:'SYNTHETIC prior annual period and amount/source correspondence',confirmed:'yes'};
c.b.advancePeriodDeclaration.ppcBasisKey=advancePeriodBasis(c.b,'ppc');c.b.advancePeriodDeclaration.pecBasisKey=advancePeriodBasis(c.b,'pec');
c.period=annualAdvancePeriod(c.d,c.b);
c.b.pecObligation.plan={...newPecPlan(c.period),mode:'three',june:'10000',august:'15000',october:'15000',reference:'SYNTHETIC reviewed noncivil cash distribution'};
return c;
}
@@ -0,0 +1,42 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import {compileFunction} from 'node:vm';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import ts from 'typescript';
import * as annual from '../../resources/js/lib/irpc-annual-settlement.ts';
import * as planLib from '../../resources/js/lib/irpc-pec-plan.ts';
import * as rational from '../../resources/js/lib/fiscal-rational.ts';
import {noncivilCase} from './helpers/irpc-annual-noncivil-case.mjs';
const require=createRequire(import.meta.url);
function load(name,resolver){const module={exports:{}};const code=ts.transpileModule(fs.readFileSync(new URL('../../resources/js/components/'+name,import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;compileFunction(code,['require','module','exports'])(resolver,module,module.exports);return module.exports;}
function all(tree){if(!tree||typeof tree!=='object')return [];return [tree,...React.Children.toArray(tree.props?.children).flatMap(all)];}
const annualUi=()=>load('simulation-irpc-annual.tsx',n=>n==='@/lib/irpc-annual-settlement'?annual:n.startsWith('./')?new Proxy({},{get:()=>()=>null}):require(n));
test('period preparation suggests dates without evidence or silent confirmation; editing retains deliberate inputs',()=>{
const {d,b}=noncivilCase();delete b.advancePeriodDeclaration;let changed;
const C=annualUi().SimulationIrpcAdvancePeriodDeclaration;
let tree=C({draft:d,book:b,onChange:v=>changed=v});all(tree).find(e=>e.type==='button').props.onClick();
assert.equal(changed.currentFrom,'2025-07-01');assert.equal(changed.previousFrom,'2024-07-01');assert.equal(changed.confirmed,'');assert.equal(changed.previousReference,'');
b.advancePeriodDeclaration={...changed,currentReference:'SYNTHETIC chosen reference',previousReference:'SYNTHETIC prior reference',confirmed:'yes'};
tree=C({draft:d,book:b,onChange:v=>changed=v});
all(tree).find(e=>e.props?.['aria-label']==='Fonte e correspondência do período anterior').props.onChange({target:{value:'SYNTHETIC corrected source'}});
assert.equal(changed.confirmed,'');assert.equal(changed.currentReference,'SYNTHETIC chosen reference');assert.equal(changed.previousReference,'SYNTHETIC corrected source');
});
test('reconfirmation retains periods and deliberate references while binding changed financial amounts',()=>{
const {d,b}=noncivilCase(),old=structuredClone(b.advancePeriodDeclaration);b.ppcObligation.previousTax='200000';let changed;
const tree=annualUi().SimulationIrpcAdvancePeriodDeclaration({draft:d,book:b,onChange:v=>changed=v});
all(tree).find(e=>e.type==='button'&&String(e.props.children).includes('Reconferi')).props.onClick();
assert.equal(changed.currentReference,old.currentReference);assert.equal(changed.previousFrom,old.previousFrom);assert.equal(changed.confirmed,'yes');assert.notEqual(changed.ppcBasisKey,old.ppcBasisKey);assert.equal(changed.pecBasisKey,old.pecBasisKey);
});
test('PEC explicit period binding preserves cash distribution and renders actual relative months',()=>{
const C=load('simulation-irpc-pec-plan.tsx',n=>n==='@/lib/irpc-pec-plan'?planLib:n==='@/lib/fiscal-rational'?rational:require(n)).SimulationIrpcPecPlan;
const {b,period}=noncivilCase(),plan={...b.pecObligation.plan};delete plan.periodFrom;delete plan.periodTo;let changed;
const props={plan,total:{numerator:'40000',denominator:'1'},notApplicable:false,year:'2026',period,onChange:v=>changed=v};
const tree=C(props);all(tree).find(e=>e.type==='button'&&String(e.props.children).includes('Conferi')).props.onClick();
assert.equal(changed.june,'10000');assert.equal(changed.august,'15000');assert.equal(changed.reference,plan.reference);assert.equal(changed.periodFrom,'2025-07-01');
const html=renderToStaticMarkup(React.createElement(C,{...props,plan:changed}));
for(const month of ['2025-12','2026-02','2026-04'])assert.match(html,new RegExp(month));
assert.ok(!html.includes('Conferi a distribuição para'));
});
@@ -0,0 +1,40 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {noncivilCase} from './helpers/irpc-annual-noncivil-case.mjs';
import {advanceCalendar,newAdvancePeriodDeclaration} from '../../resources/js/lib/irpc-advance-calendar.ts';
import {calculatePpcObligation} from '../../resources/js/lib/irpc-ppc-obligation.ts';
import {calculatePecObligation} from '../../resources/js/lib/irpc-pec-obligation.ts';
import {calculatePecPlan} from '../../resources/js/lib/irpc-pec-plan.ts';
import {annualAdvancePeriod,advancePeriodBasis,annualAdvanceCashflow,recordAnnual,saveAnnualBook,annualBook,annualCurrent,annualHtml} from '../../resources/js/lib/irpc-annual-settlement.ts';
const f=n=>({numerator:String(n),denominator:'1'});
test('documented annual noncivil periods reuse exact estimates and relative PPC PEC months',()=>{
const {b,period}=noncivilCase(),before=JSON.stringify(b);const ppc=calculatePpcObligation(b.ppcObligation,b.subjectNuit,b.year,b.assessment,period),pec=calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,period);
assert.deepEqual(ppc.total,f(72000));assert.deepEqual(ppc.schedule.map(r=>r.month),['2025-11','2026-01','2026-03']);assert.deepEqual(pec.total,f(40000));assert.deepEqual(pec.months,['2025-12','2026-02','2026-04']);
const joint=annualAdvanceCashflow(b,period);assert.deepEqual(joint.total,f(112000));assert.deepEqual(joint.rows.map(r=>r.month),['2025-11','2025-12','2026-01','2026-02','2026-03','2026-04']);assert.equal(JSON.stringify(b),before);
});
test('missing evidence, identity mismatch, nonadjacent and short previous periods cannot qualify the annual recorte',()=>{
for(const changes of [{confirmed:''},{currentReference:''},{previousReference:''},{subjectNuit:'999999999'},{currentFrom:'2025-08-01'},{previousFrom:'2025-01-01'},{previousTo:'2025-05-31'}]){
const {d,b}=noncivilCase();Object.assign(b.advancePeriodDeclaration,changes);assert.equal(annualAdvanceCashflow(b,annualAdvancePeriod(d,b)).total,null,JSON.stringify(changes));
}
const missing=noncivilCase();missing.b.advancePeriodDeclaration.previousReference='';const result=calculatePpcObligation(missing.b.ppcObligation,missing.b.subjectNuit,missing.b.year,missing.b.assessment,annualAdvancePeriod(missing.d,missing.b));assert.ok(result.pending.length);assert.ok(!result.pending.join(' ').includes('exercício civil completo2025'));
assert.equal(advanceCalendar(null,'').mode,'unavailable');
});
test('editing one component invalidates only its confirmed amount correspondence',()=>{
const {d,b}=noncivilCase();b.ppcObligation.previousTax='200000';let m=annualAdvanceCashflow(b,annualAdvancePeriod(d,b));assert.equal(m.ppcKnown,false);assert.equal(m.pecKnown,true);assert.equal(m.total,null);
b.advancePeriodDeclaration.ppcBasisKey=advancePeriodBasis(b,'ppc');m=annualAdvanceCashflow(b,annualAdvancePeriod(d,b));assert.equal(m.ppcKnown,true);assert.deepEqual(m.total,f(193000));
});
test('civil cash choices are retained without automatic displacement and require explicit noncivil binding',()=>{
const {b,period}=noncivilCase();delete b.pecObligation.plan.periodFrom;delete b.pecObligation.plan.periodTo;let m=annualAdvanceCashflow(b,period);assert.equal(m.pecKnown,false);assert.equal(b.pecObligation.plan.june,'10000');
b.pecObligation.plan.periodFrom=period.from;b.pecObligation.plan.periodTo=period.to;m=annualAdvanceCashflow(b,period);assert.deepEqual(m.total,f(112000));
const p={...b.pecObligation.plan,mode:'single',singleMonth:'2026-01'};assert.deepEqual(calculatePecPlan(p,f(40000),false,'2026',period).rows,[{month:'2026-01',amount:f(40000)}]);p.singleMonth='2026-07';assert.equal(calculatePecPlan(p,f(40000),false,'2026',period).rows.length,0);
});
test('source changes preserve the declaration but require reconfirmation; excluded historical and start cases stay separate',()=>{
const {d,b}=noncivilCase();const old=JSON.stringify(b.advancePeriodDeclaration);const period={...annualAdvancePeriod(d,b),from:'2025-08-01',to:'2026-07-31'};assert.equal(annualAdvanceCashflow(b,period).total,null);assert.equal(JSON.stringify(b.advancePeriodDeclaration),old);
b.pecObligation.previousTurnover='0';b.advancePeriodDeclaration.pecBasisKey=advancePeriodBasis(b,'pec');assert.equal(calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,annualAdvancePeriod(d,b)).total,null);
b.pecObligation.activityStartYear='2026';b.advancePeriodDeclaration.pecBasisKey=advancePeriodBasis(b,'pec');assert.equal(calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,annualAdvancePeriod(d,b)).total,null);
});
test('annual recovery freezes qualified periods and keeps the independent settlement and history unchanged',()=>{
const {d,b}=noncivilCase();const prior=recordAnnual(d,{...b,ppcObligation:undefined,pecObligation:undefined},'2026-11-14T07:00:00Z');const next=recordAnnual(d,{...prior,ppcObligation:b.ppcObligation,pecObligation:b.pecObligation},'2026-11-14T07:01:00Z');assert.deepEqual(next.result.model.arithmeticBalance,prior.result.model.arithmeticBalance);
d.modelo_a_snapshot=saveAnnualBook(d,next);const back=annualBook(d);assert.equal(annualCurrent(d,back),true);assert.equal(back.history.length,1);assert.deepEqual(annualAdvanceCashflow(back,back.result.model.advancePeriod).total,f(112000));assert.match(annualHtml(d),/2025-11/);
const frozen=JSON.stringify(back.result.model.advancePeriod);back.advancePeriodDeclaration.previousReference='SYNTHETIC revised reference';assert.equal(JSON.stringify(back.result.model.advancePeriod),frozen);assert.equal(annualCurrent(d,back),false);
});
+32
View File
@@ -0,0 +1,32 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import {noncivilCase} from '../tests/frontend/helpers/irpc-annual-noncivil-case.mjs';
import {annualAdvanceCashflow,annualAdvancePeriod,annualBook,annualCurrent,annualHtml,recordAnnual,saveAnnualBook} from '../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
const out=new URL('../outputs/irpc-noncivil-20261114/',import.meta.url);
fs.mkdirSync(out,{recursive:true});
for(const name of ['three','single','missing','changed']){
if(process.argv[2]==='generate'){
const {d,b}=noncivilCase();
if(name==='single')Object.assign(b.pecObligation.plan,{mode:'single',singleMonth:'2026-01'});
if(name==='missing')b.advancePeriodDeclaration.previousReference='';
if(name==='changed')b.ppcObligation.previousTax='200000';
const prior=recordAnnual(d,{...b,ppcObligation:undefined,pecObligation:undefined,advancePeriodDeclaration:undefined},'2026-11-14T08:00:00Z');
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,{...prior,ppcObligation:b.ppcObligation,pecObligation:b.pecObligation,advancePeriodDeclaration:b.advancePeriodDeclaration},'2026-11-14T08:01:00Z'));
fs.writeFileSync(new URL(name+'.json',out),JSON.stringify(d));
}else{
const d=JSON.parse(fs.readFileSync(new URL('recovered-'+name+'.json',out),'utf8')),b=annualBook(d),period=annualAdvancePeriod(d,b),m=annualAdvanceCashflow(b,period);
assert.equal(annualCurrent(d,b),true);assert.equal(b.history.length,1);
assert.deepEqual(b.result.model.arithmeticBalance,b.history[0].model.arithmeticBalance);
assert.deepEqual(b.result.model.advancePeriod,period);
if(['three','single'].includes(name))assert.deepEqual(m.total,{numerator:'112000',denominator:'1'});
else assert.equal(m.total,null);
if(name==='changed'){assert.equal(m.ppcKnown,false);assert.equal(m.pecKnown,true);}
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-11-14T08:02:00Z'});
assert.match(html,/Projecção mensal conjunta PPC\/PEC/);assert.match(JSON.stringify(report),/Projecção mensal conjunta PPC\/PEC/);
if(['three','single'].includes(name)){assert.match(html,/2025-11/);assert.match(html,/2026-03/);assert.match(JSON.stringify(report),/112000/);}
fs.writeFileSync(new URL(name+'-report.html',out),html);
fs.writeFileSync(new URL(name+'-verification.json',out),JSON.stringify({synthetic:true,nativeApiRecovery:true,historyPreserved:true,independentAnnualCalculationUnchanged:true,period,model:m},null,2));
}
}
console.log(process.argv[2]==='generate'?'Generated four synthetic annual noncivil cases':'Verified four recovered annual noncivil cases and reports');
+23
View File
@@ -0,0 +1,23 @@
<?php
$root = dirname(__DIR__);
if (($argv[1] ?? '') !== 'test' || basename($root) !== 'portal-investidor') {
throw new RuntimeException('Explicit isolated QA test operation required');
}
foreach (['APP_ENV' => 'testing', 'APP_KEY' => 'base64:'.base64_encode(str_repeat('Q', 32)),
'APP_CONFIG_CACHE' => $root.'/outputs/irpc-noncivil-20261114/no-config.php', 'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441',
'DB_DATABASE' => 'investmoz_demography_test', 'DB_USERNAME' => 'demography_qa', 'DB_PASSWORD' => '', 'DB_URL' => '', 'DATABASE_URL' => '',
'CACHE_STORE' => 'array', 'SESSION_DRIVER' => 'array', 'QUEUE_CONNECTION' => 'sync', 'MAIL_MAILER' => 'array', 'SUITECRM_ENABLED' => 'false'] as $key => $value) {
putenv($key.'='.$value);
$_ENV[$key] = $_SERVER[$key] = $value;
}
$pdo = new PDO('pgsql:host=127.0.0.1;port=55441;dbname=investmoz_demography_test', 'demography_qa', '');
$identity = $pdo->query('select current_database(),inet_server_port()')->fetch(PDO::FETCH_NUM);
if ($identity !== ['investmoz_demography_test', 55441] && $identity !== ['investmoz_demography_test', '55441']) {
throw new RuntimeException('Isolated QA identity required');
}
$argv = ['phpunit', '-c', 'phpunit.investmoz.xml', '--do-not-cache-result', 'tests/Feature/IrpcAnnualNoncivilDraftTest.php'];
$_SERVER['argv'] = $argv;
$_SERVER['argc'] = count($argv);
require $root.'/vendor/autoload.php';
require $root.'/vendor/phpunit/phpunit/phpunit';