Support documented full annual noncivil PPC PEC estimates and dossier continuity
tests / ci (push) Waiting to run
tests / ci (push) Waiting to run
This commit is contained in:
@@ -9935,3 +9935,18 @@ Instalação expressamente autorizada, concluída pelo executor Git existente na
|
||||
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
|
||||
|
||||
@@ -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
|
||||
: '',
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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');
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user