Preparar obriga??o PPC ordin?ria com recupera??o e relat?rio no dossier
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
## 11/11/2026 — obrigação PPC ordinária de L8 em QA
|
||||
|
||||
**Pendência avançada:** o fecho anual existente passa a preparar a obrigação PPC2026 com liquidação documental do exercício civil completo2025: imposto anterior21(1) líquido de retenções,80%,três prestações iguais arredondadas individualmente por excesso ao milhar de meticais. Calendário por mês:Maio,Julho,Setembro,sem inventar dia limite. Fonte:RIRPC,Decreto9/2008,arts27(1)(a)/28(1–3),BR16/I16-04-2008,original SHA256 `fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee`, já preservado no acervo. Não usa colecta projectada corrente como liquidação anterior.
|
||||
|
||||
Reutiliza titular/exercício e rascunho privado existentes; formulário progressivo conserva edições deliberadas ao trocar enquadramento. Obrigação preparada e comprovativos efectivamente pagos permanecem distintos: não cria pagamento,dedução,saldo oficial,guia ou juros. Campos vazios,fonte ausente,divergência de titular/período,base negativa e período especial ficam indeterminados,sem impedir o fecho anual independente. Zero explícito permanece zero. Anualização28(3),dispensas,PEC e restantes regimes ficam separados.
|
||||
|
||||
**Prova sintética:** cálculo→POST/PUT→GET privado→recálculo→HTML/relatório do mesmo dossier em cinco cenários:72.000MZN ordinário,75.000após arredondamento,zero,valor desconhecido e período especial indeterminados. Saldo anual138MZN e comprovativos inalterados; histórico anterior conservado. Uma prova nativa/47asserções com rollback no QA55441 existente; cinco verificações novas de cálculo/interface e24afectadas aprovadas. Tipos/lint/compilação aprovados; pacote Git com aplicação/reversão conferidas no fecho. Provas `outputs/irpc-ppc-obligation-20261111/*-verification.json`.
|
||||
|
||||
**Estado:** implementado em QA, ainda sem instalação. Homologação mantém `6805b606`,Educação entregue e270instituições publicadas. Restam entrega Git deste delta e visual; L8 não está integralmente concluído. Preservam-se os resíduos L1–L7,restanteL8/L9–L16 e restantes módulos. Próximo trabalho fiscal: PEC/obrigações e demais casos executáveis com as fontes aceites,sem reabrir PPC ordinário.
|
||||
|
||||
## 11/11/2026 — retoma da revisão de Educação instalada em6805b606
|
||||
|
||||
Instalação autorizada pelo utilizador, executada pelo processo Git existente e conferida às08:46:05Africa/Johannesburg(06:46:05UTC): `6805b606a106fbed410b191ed778c0fad7c8e913`. Guardar e recuperar a preparação privada da revisão interna de procura/capacidade fica utilizável na homologação, pelo detalhe existente em `/management/education`. A gravação privada não decide nem aprova; preserva notas, versão analisada e a chave de uma intenção incerta.
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
## 11/11/2026 — obrigação PPC ordinária de L8 em QA
|
||||
|
||||
**Pendência avançada:** o fecho anual existente passa a preparar a obrigação PPC2026 com liquidação documental do exercício civil completo2025: imposto anterior21(1) líquido de retenções,80%,três prestações iguais arredondadas individualmente por excesso ao milhar de meticais. Calendário por mês:Maio,Julho,Setembro,sem inventar dia limite. Fonte:RIRPC,Decreto9/2008,arts27(1)(a)/28(1–3),BR16/I16-04-2008,original SHA256 `fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee`, já preservado no acervo. Não usa colecta projectada corrente como liquidação anterior.
|
||||
|
||||
Reutiliza titular/exercício e rascunho privado existentes; formulário progressivo conserva edições deliberadas ao trocar enquadramento. Obrigação preparada e comprovativos efectivamente pagos permanecem distintos: não cria pagamento,dedução,saldo oficial,guia ou juros. Campos vazios,fonte ausente,divergência de titular/período,base negativa e período especial ficam indeterminados,sem impedir o fecho anual independente. Zero explícito permanece zero. Anualização28(3),dispensas,PEC e restantes regimes ficam separados.
|
||||
|
||||
**Prova sintética:** cálculo→POST/PUT→GET privado→recálculo→HTML/relatório do mesmo dossier em cinco cenários:72.000MZN ordinário,75.000após arredondamento,zero,valor desconhecido e período especial indeterminados. Saldo anual138MZN e comprovativos inalterados; histórico anterior conservado. Uma prova nativa/47asserções com rollback no QA55441 existente; cinco verificações novas de cálculo/interface e24afectadas aprovadas. Tipos/lint/compilação aprovados; pacote Git com aplicação/reversão conferidas no fecho. Provas `outputs/irpc-ppc-obligation-20261111/*-verification.json`.
|
||||
|
||||
**Estado:** implementado em QA, ainda sem instalação. Homologação mantém `6805b606`,Educação entregue e270instituições publicadas. Restam entrega Git deste delta e visual; L8 não está integralmente concluído. Preservam-se os resíduos L1–L7,restanteL8/L9–L16 e restantes módulos. Próximo trabalho fiscal: PEC/obrigações e demais casos executáveis com as fontes aceites,sem reabrir PPC ordinário.
|
||||
|
||||
## 11/11/2026 — retoma da revisão de Educação instalada em6805b606
|
||||
|
||||
Instalação autorizada pelo utilizador, executada pelo processo Git existente e conferida às08:46:05Africa/Johannesburg(06:46:05UTC): `6805b606a106fbed410b191ed778c0fad7c8e913`. Guardar e recuperar a preparação privada da revisão interna de procura/capacidade fica utilizável na homologação, pelo detalhe existente em `/management/education`. A gravação privada não decide nem aprova; preserva notas, versão analisada e a chave de uma intenção incerta.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,13 @@
|
||||
## 11/11/2026 — obrigação PPC ordinária de L8 em QA
|
||||
|
||||
**Pendência avançada:** o fecho anual existente passa a preparar a obrigação PPC2026 com liquidação documental do exercício civil completo2025: imposto anterior21(1) líquido de retenções,80%,três prestações iguais arredondadas individualmente por excesso ao milhar de meticais. Calendário por mês:Maio,Julho,Setembro,sem inventar dia limite. Fonte:RIRPC,Decreto9/2008,arts27(1)(a)/28(1–3),BR16/I16-04-2008,original SHA256 `fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee`, já preservado no acervo. Não usa colecta projectada corrente como liquidação anterior.
|
||||
|
||||
Reutiliza titular/exercício e rascunho privado existentes; formulário progressivo conserva edições deliberadas ao trocar enquadramento. Obrigação preparada e comprovativos efectivamente pagos permanecem distintos: não cria pagamento,dedução,saldo oficial,guia ou juros. Campos vazios,fonte ausente,divergência de titular/período,base negativa e período especial ficam indeterminados,sem impedir o fecho anual independente. Zero explícito permanece zero. Anualização28(3),dispensas,PEC e restantes regimes ficam separados.
|
||||
|
||||
**Prova sintética:** cálculo→POST/PUT→GET privado→recálculo→HTML/relatório do mesmo dossier em cinco cenários:72.000MZN ordinário,75.000após arredondamento,zero,valor desconhecido e período especial indeterminados. Saldo anual138MZN e comprovativos inalterados; histórico anterior conservado. Uma prova nativa/47asserções com rollback no QA55441 existente; cinco verificações novas de cálculo/interface e24afectadas aprovadas. Tipos/lint/compilação aprovados; pacote Git com aplicação/reversão conferidas no fecho. Provas `outputs/irpc-ppc-obligation-20261111/*-verification.json`.
|
||||
|
||||
**Estado:** implementado em QA, ainda sem instalação. Homologação mantém `6805b606`,Educação entregue e270instituições publicadas. Restam entrega Git deste delta e visual; L8 não está integralmente concluído. Preservam-se os resíduos L1–L7,restanteL8/L9–L16 e restantes módulos. Próximo trabalho fiscal: PEC/obrigações e demais casos executáveis com as fontes aceites,sem reabrir PPC ordinário.
|
||||
|
||||
## 11/11/2026 — retoma da revisão de Educação instalada em6805b606
|
||||
|
||||
Instalação autorizada pelo utilizador, executada pelo processo Git existente e conferida às08:46:05Africa/Johannesburg(06:46:05UTC): `6805b606a106fbed410b191ed778c0fad7c8e913`. Guardar e recuperar a preparação privada da revisão interna de procura/capacidade fica utilizável na homologação, pelo detalhe existente em `/management/education`. A gravação privada não decide nem aprova; preserva notas, versão analisada e a chave de uma intenção incerta.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SimulationIrpcPpcObligation } from './simulation-irpc-ppc-obligation';
|
||||
import { SimulationIrpcInvestment } from './simulation-irpc-investment';
|
||||
import { SimulationIrpcEconomic } from './simulation-irpc-economic';
|
||||
import { FiscalProofParcelFields } from './fiscal-proof-parcel';
|
||||
@@ -340,6 +341,7 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
|
||||
</fieldset>
|
||||
<fieldset className="space-y-2 rounded border p-3">
|
||||
<legend>PPC efectivamente pagos</legend>
|
||||
<SimulationIrpcPpcObligation book={b} onChange={ppcObligation=>patch({ppcObligation})}/>
|
||||
{group('ppc', 'PPC efectuados')}
|
||||
<p>
|
||||
Regista comprovativos de pagamentos por conta; não
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
newPpcObligation,
|
||||
calculatePpcObligation,
|
||||
ppcObligationSource,
|
||||
type PpcObligation,
|
||||
} from '@/lib/irpc-ppc-obligation';
|
||||
import { fractionPresentation } from '@/lib/fiscal-rational';
|
||||
import type { AnnualBook } from '@/lib/irpc-annual-settlement';
|
||||
export function SimulationIrpcPpcObligation({
|
||||
book,
|
||||
onChange,
|
||||
}: {
|
||||
book: AnnualBook;
|
||||
onChange: (value: PpcObligation) => void;
|
||||
}) {
|
||||
const p = book.ppcObligation;
|
||||
if (!p)
|
||||
return (
|
||||
<section className="rounded border p-3">
|
||||
<h5>Obrigação PPC</h5>
|
||||
<p>
|
||||
Preparar a obrigação e os meses previstos não regista
|
||||
pagamentos nem cria deduções na liquidação.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange(newPpcObligation(book.subjectNuit, book.year))
|
||||
}
|
||||
>
|
||||
Preparar obrigação PPC
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
const m = calculatePpcObligation(
|
||||
p,
|
||||
book.subjectNuit,
|
||||
book.year,
|
||||
book.assessment,
|
||||
);
|
||||
const patch = (changes: Partial<PpcObligation>) =>
|
||||
onChange({ ...p, ...changes });
|
||||
const field = (
|
||||
key:
|
||||
| 'subjectNuit'
|
||||
| 'year'
|
||||
| 'previousYear'
|
||||
| 'previousTax'
|
||||
| 'previousWithholdings'
|
||||
| 'assessmentReference'
|
||||
| 'withholdingReference',
|
||||
label: string,
|
||||
) => (
|
||||
<label className="block">
|
||||
{label}
|
||||
<input
|
||||
aria-label={label}
|
||||
value={p[key]}
|
||||
onChange={(e) => patch({ [key]: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
return (
|
||||
<section
|
||||
aria-label="Preparação da obrigação PPC"
|
||||
className="rounded border p-3"
|
||||
>
|
||||
<h5>Obrigação PPC — exercício anterior</h5>
|
||||
<p>
|
||||
Reutiliza o titular e exercício ao iniciar. Complete a
|
||||
liquidação anterior e as retenções documentadas; campos vazios
|
||||
não equivalem a zero.
|
||||
</p>
|
||||
<label className="block">
|
||||
Enquadramento PPC
|
||||
<select
|
||||
aria-label="Enquadramento PPC"
|
||||
value={p.scope}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
scope: e.target.value as PpcObligation['scope'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Por confirmar</option>
|
||||
<option value="ordinary_resident">
|
||||
Residente geral — actividade comercial, industrial ou
|
||||
agrícola, ano civil
|
||||
</option>
|
||||
<option value="other">
|
||||
Outro enquadramento — cálculo separado
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
{p.scope === 'ordinary_resident' && (
|
||||
<>
|
||||
{field('subjectNuit', 'NUIT — obrigação PPC')}
|
||||
{field('year', 'Exercício — obrigação PPC')}
|
||||
{field('previousYear', 'Exercício anterior — PPC')}
|
||||
<label className="block">
|
||||
Exercício anterior civil completo
|
||||
<select
|
||||
aria-label="Exercício anterior civil completo"
|
||||
value={p.previousFullCalendarYear}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
previousFullCalendarYear: e.target
|
||||
.value as PpcObligation['previousFullCalendarYear'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Por confirmar</option>
|
||||
<option value="yes">Sim</option>
|
||||
<option value="no">
|
||||
Não — anualização/período especial separado
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
{field(
|
||||
'previousTax',
|
||||
'Imposto liquidado anterior21(1) — MZN',
|
||||
)}
|
||||
{field(
|
||||
'assessmentReference',
|
||||
'Fonte da liquidação anterior — PPC',
|
||||
)}
|
||||
{field(
|
||||
'previousWithholdings',
|
||||
'Retenções na fonte anteriores — MZN',
|
||||
)}
|
||||
{field(
|
||||
'withholdingReference',
|
||||
'Fonte das retenções anteriores — PPC',
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{m.pending.length ? (
|
||||
<p role="status">{m.pending.join(' ')}</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
80% antes de arredondar:{' '}
|
||||
{fractionPresentation(m.annualBeforeRounding!).decimal}{' '}
|
||||
MZN. Total das três prestações arredondadas:{' '}
|
||||
{fractionPresentation(m.total!).decimal} MZN.
|
||||
</p>
|
||||
<ul>
|
||||
{m.schedule.map((s) => (
|
||||
<li key={s.month}>
|
||||
{s.month}:{' '}
|
||||
{fractionPresentation(s.amount).decimal} MZN
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
<p>
|
||||
Guardar o rascunho conserva esta preparação; calcular o fecho
|
||||
anual inclui-a no relatório. Só comprovativos efectivos podem
|
||||
alimentar as deduções PPC abaixo. Não executa pagamento,
|
||||
dispensa, guia ou juros.
|
||||
</p>
|
||||
<details>
|
||||
<summary>Fonte e limites</summary>
|
||||
<p>{ppcObligationSource}</p>
|
||||
</details>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { validatePpcObligation, ppcObligationReport, ppcObligationVersion, type PpcObligation } from './irpc-ppc-obligation.ts';
|
||||
import {
|
||||
newInvestmentCredit,
|
||||
validateInvestmentCredit,
|
||||
@@ -122,6 +123,7 @@ export type AnnualBook = {
|
||||
creditCarry?: CreditCarry;
|
||||
economicCredits?: EconomicCredits;
|
||||
investmentCredit?: InvestmentCredit;
|
||||
ppcObligation?: PpcObligation;
|
||||
result?: AnnualRecord;
|
||||
history: AnnualRecord[];
|
||||
};
|
||||
@@ -291,6 +293,7 @@ function books(d: Record<string, unknown>): Record<string, AnnualBook> {
|
||||
return m;
|
||||
}
|
||||
export function validateAnnual(b: AnnualBook) {
|
||||
if (b?.ppcObligation !== undefined) validatePpcObligation(b.ppcObligation);
|
||||
if (b?.investmentCredit !== undefined)
|
||||
validateInvestmentCredit(b.investmentCredit);
|
||||
if (b?.economicCredits !== undefined) validateEconomic(b.economicCredits);
|
||||
@@ -565,6 +568,7 @@ function key(d: Record<string, unknown>, b: AnnualBook) {
|
||||
acquisitionScope(d),
|
||||
input,
|
||||
dependencies(d, !!b.investmentCredit),
|
||||
...(b.ppcObligation ? [ppcObligationVersion] : []),
|
||||
...(b.investmentCredit && b.creditCarry?.lots.length
|
||||
? ['CBF15-credit-carry-combination/1']
|
||||
: []),
|
||||
@@ -1712,6 +1716,7 @@ export function annualReportRows(
|
||||
indeterminate: 'Indeterminado',
|
||||
}[m.status];
|
||||
return [
|
||||
...(m.input.ppcObligation ? ppcObligationReport(m.input.ppcObligation,m.subjectNuit,m.year,m.input.assessment) : []),
|
||||
[
|
||||
'IRPC — L8',
|
||||
'Fecho anual — âmbito e origem',
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
decimalFraction,
|
||||
fraction,
|
||||
subtractFractions,
|
||||
fractionPresentation,
|
||||
type FiscalFraction,
|
||||
} from './fiscal-rational.ts';
|
||||
|
||||
export const ppcObligationVersion = 'IRPC-PPC-obligation/1';
|
||||
export const ppcObligationSource =
|
||||
'RIRPC, Decreto 9/2008, arts.27(1)(a) e28(1–3), BR16/I,16-04-2008,exemplar preservado SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee: imposto liquidado no exercício anterior nos termos21(1), líquido das retenções na fonte;80%, dividido em três montantes iguais arredondados por excesso ao milhar de meticais. Maio,Julho,Setembro. Recorte2026,residente geral,actividade comercial/industrial/agrícola,ano civil e exercício anterior completo2025,MZN. Períodos especiais,anualização28(3),dispensas,juros e guias separados.';
|
||||
export type PpcObligation = {
|
||||
version: 1;
|
||||
scope: '' | 'ordinary_resident' | 'other';
|
||||
subjectNuit: string;
|
||||
year: string;
|
||||
previousYear: string;
|
||||
previousFullCalendarYear: '' | 'yes' | 'no';
|
||||
previousTax: string;
|
||||
previousWithholdings: string;
|
||||
assessmentReference: string;
|
||||
withholdingReference: string;
|
||||
};
|
||||
export const newPpcObligation = (
|
||||
subjectNuit = '',
|
||||
year = '',
|
||||
): PpcObligation => ({
|
||||
version: 1,
|
||||
scope: '',
|
||||
subjectNuit,
|
||||
year,
|
||||
previousYear: '',
|
||||
previousFullCalendarYear: '',
|
||||
previousTax: '',
|
||||
previousWithholdings: '',
|
||||
assessmentReference: '',
|
||||
withholdingReference: '',
|
||||
});
|
||||
export function validatePpcObligation(p: PpcObligation) {
|
||||
if (
|
||||
!p ||
|
||||
p.version !== 1 ||
|
||||
Object.keys(newPpcObligation())
|
||||
.filter((k) => k !== 'version')
|
||||
.some(
|
||||
(k) =>
|
||||
typeof (p as unknown as Record<string, unknown>)[k] !==
|
||||
'string',
|
||||
) ||
|
||||
!['', 'ordinary_resident', 'other'].includes(p.scope) ||
|
||||
!['', 'yes', 'no'].includes(p.previousFullCalendarYear)
|
||||
)
|
||||
throw Error('Preparação PPC não recuperável; original preservado.');
|
||||
}
|
||||
export function calculatePpcObligation(
|
||||
p: PpcObligation,
|
||||
subjectNuit: string,
|
||||
year: string,
|
||||
assessment: string,
|
||||
) {
|
||||
validatePpcObligation(p);
|
||||
const pending: string[] = [];
|
||||
let base: FiscalFraction | null = null,
|
||||
annualBeforeRounding: FiscalFraction | null = null,
|
||||
instalment: FiscalFraction | null = null;
|
||||
if (
|
||||
p.scope !== 'ordinary_resident' ||
|
||||
assessment !== 'normal' ||
|
||||
p.previousFullCalendarYear !== 'yes' ||
|
||||
year !== '2026' ||
|
||||
p.year !== year ||
|
||||
p.previousYear !== '2025' ||
|
||||
!/^\d{9}$/.test(subjectNuit) ||
|
||||
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.',
|
||||
);
|
||||
if (!p.assessmentReference.trim() || !p.withholdingReference.trim())
|
||||
pending.push(
|
||||
'PPC: falta suporte do imposto liquidado anterior21(1) e das retenções na fonte, incluindo zero conhecido. Colecta projectada corrente não substitui liquidação anterior.',
|
||||
);
|
||||
if (
|
||||
![p.previousTax, p.previousWithholdings].every((v) =>
|
||||
/^\d{1,18}(\.\d{1,2})?$/.test(v),
|
||||
)
|
||||
)
|
||||
pending.push(
|
||||
'PPC: montantes anteriores em MZN desconhecidos ou inválidos; zero deve ser explícito.',
|
||||
);
|
||||
if (!pending.length) {
|
||||
base = subtractFractions(
|
||||
decimalFraction(p.previousTax),
|
||||
decimalFraction(p.previousWithholdings),
|
||||
);
|
||||
if (BigInt(base.numerator) < 0n)
|
||||
pending.push(
|
||||
'PPC: retenções superiores ao imposto anterior; reconciliar o caso antes de determinar a obrigação.',
|
||||
);
|
||||
else {
|
||||
annualBeforeRounding = fraction(
|
||||
BigInt(base.numerator) * 4n,
|
||||
BigInt(base.denominator) * 5n,
|
||||
);
|
||||
const n = BigInt(annualBeforeRounding.numerator),
|
||||
d = BigInt(annualBeforeRounding.denominator) * 3n * 1000n;
|
||||
instalment = fraction(((n + d - 1n) / d) * 1000n);
|
||||
}
|
||||
}
|
||||
return {
|
||||
version: ppcObligationVersion,
|
||||
source: ppcObligationSource,
|
||||
pending,
|
||||
base,
|
||||
annualBeforeRounding,
|
||||
instalment,
|
||||
total: instalment
|
||||
? fraction(
|
||||
BigInt(instalment.numerator) * 3n,
|
||||
BigInt(instalment.denominator),
|
||||
)
|
||||
: null,
|
||||
schedule: instalment
|
||||
? ['2026-05', '2026-07', '2026-09'].map((month) => ({
|
||||
month,
|
||||
amount: instalment,
|
||||
}))
|
||||
: [],
|
||||
paymentExecuted: false,
|
||||
annualDeductionCreated: false,
|
||||
};
|
||||
}
|
||||
export function ppcObligationReport(
|
||||
p: PpcObligation,
|
||||
subjectNuit: string,
|
||||
year: string,
|
||||
assessment: string,
|
||||
): Array<[string, string, string, string]> {
|
||||
const m = calculatePpcObligation(p, subjectNuit, year, assessment),
|
||||
show = (v: FiscalFraction | null) =>
|
||||
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
|
||||
return [
|
||||
[
|
||||
'IRPC — L8',
|
||||
'PPC — obrigação preparada',
|
||||
m.pending.length
|
||||
? m.pending.join(' ')
|
||||
: 'Base líquida anterior: ' +
|
||||
show(m.base) +
|
||||
';80% antes de arredondar: ' +
|
||||
show(m.annualBeforeRounding) +
|
||||
'; cada prestação: ' +
|
||||
show(m.instalment) +
|
||||
'; total arredondado: ' +
|
||||
show(m.total) +
|
||||
'. ' +
|
||||
m.schedule
|
||||
.map((s) => s.month + ': ' + show(s.amount))
|
||||
.join('; ') +
|
||||
'. Previsão de obrigação; não é pagamento nem dedução anual.',
|
||||
p.assessmentReference +
|
||||
'; ' +
|
||||
p.withholdingReference +
|
||||
'; ' +
|
||||
m.source,
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class IrpcPpcObligationDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_ppc_obligation_and_calendar_survive_native_draft_without_new_paid_deductions(): void
|
||||
{
|
||||
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
$this->assertSame('proactive_investment_20261027_test', $target->name);
|
||||
$this->assertSame(55441, (int) $target->port);
|
||||
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||
$this->actingAs(User::findOrFail(1021));
|
||||
$id = null;
|
||||
$etag = null;
|
||||
foreach (['ordinary' => 138, 'rounded' => 138, 'zero' => 138, 'unknown' => 138, 'other_period' => 138] as $name => $balance) {
|
||||
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-ppc-obligation-20261111/'.$name.'.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$personal = ['person_type' => $snapshot['person_type']];
|
||||
unset($snapshot['person_type']);
|
||||
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $snapshot];
|
||||
if ($id === null) {
|
||||
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||
} else {
|
||||
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
|
||||
}
|
||||
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
|
||||
$etag = $response->headers->get('ETag');
|
||||
$back = $response->json('data.economic_snapshot');
|
||||
$this->assertSame($snapshot['modelo_a_snapshot'], $back['modelo_a_snapshot']);
|
||||
$book = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$snapshot['context_opportunity']];
|
||||
$this->assertCount(1, $book['history']);
|
||||
$this->assertSame(['numerator' => '138', 'denominator' => '1'], $book['history'][0]['model']['arithmeticBalance']);
|
||||
$this->assertSame($balance === null ? null : ['numerator' => (string) $balance, 'denominator' => '1'], $book['result']['model']['arithmeticBalance']);
|
||||
$this->assertSame($book['ppcObligation'], $book['result']['model']['input']['ppcObligation']);
|
||||
$this->assertSame('2025', $book['ppcObligation']['previousYear']);
|
||||
$this->assertSame('SYNTHETIC prior assessment21(1)', $book['ppcObligation']['assessmentReference']);
|
||||
file_put_contents(base_path('outputs/irpc-ppc-obligation-20261111/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { newPpcObligation } from '../../../resources/js/lib/irpc-ppc-obligation.ts';
|
||||
export const plan = () => ({
|
||||
...newPpcObligation('123456789', '2026'),
|
||||
scope: 'ordinary_resident',
|
||||
previousYear: '2025',
|
||||
previousFullCalendarYear: 'yes',
|
||||
previousTax: '100000',
|
||||
previousWithholdings: '10000',
|
||||
assessmentReference: 'SYNTHETIC prior assessment21(1)',
|
||||
withholdingReference: 'SYNTHETIC prior RF reconciliation',
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { compileFunction } from 'node:vm';
|
||||
import { createRequire } from 'node:module';
|
||||
import React from 'react';
|
||||
import ts from 'typescript';
|
||||
import * as annual from '../../resources/js/lib/irpc-annual-settlement.ts';
|
||||
import * as investment from '../../resources/js/lib/irpc-ppc-obligation.ts';
|
||||
import * as rational from '../../resources/js/lib/fiscal-rational.ts';
|
||||
import {renderToStaticMarkup} from 'react-dom/server';
|
||||
import {plan} from './helpers/irpc-ppc-obligation-case.mjs';
|
||||
import { investmentCase } from './helpers/irpc-investment-case.mjs';
|
||||
|
||||
const require = createRequire(import.meta.url),
|
||||
module = { exports: {} };
|
||||
const source = fs.readFileSync(
|
||||
new URL(
|
||||
'../../resources/js/components/simulation-irpc-ppc-obligation.tsx',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const output = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
jsx: ts.JsxEmit.ReactJSX,
|
||||
},
|
||||
}).outputText;
|
||||
compileFunction(output, ['require', 'module', 'exports'])(
|
||||
(name) =>
|
||||
name === '@/lib/irpc-annual-settlement'
|
||||
? annual
|
||||
: name === '@/lib/irpc-ppc-obligation'
|
||||
? investment
|
||||
: name === '@/lib/fiscal-rational'
|
||||
? rational
|
||||
: name === './simulation-irpc-special'
|
||||
? { QualificationFields: () => null }
|
||||
: name === './fiscal-proof-parcel'
|
||||
? { FiscalProofParcelFields: () => null }
|
||||
: require(name),
|
||||
module,
|
||||
module.exports,
|
||||
);
|
||||
const Component = module.exports.SimulationIrpcPpcObligation;
|
||||
const text = (node) =>
|
||||
typeof node === 'string' || typeof node === 'number'
|
||||
? String(node)
|
||||
: React.isValidElement(node)
|
||||
? React.Children.toArray(node.props.children).map(text).join('')
|
||||
: '';
|
||||
const all = (node) =>
|
||||
React.isValidElement(node)
|
||||
? [node, ...React.Children.toArray(node.props.children).flatMap(all)]
|
||||
: [];
|
||||
function harness(c) {
|
||||
let b = c.b;
|
||||
return {
|
||||
book: () => b,
|
||||
render: () =>
|
||||
Component({
|
||||
draft: c.d,
|
||||
book: b,
|
||||
onChange: (next) => {
|
||||
b = {...b,ppcObligation:next};
|
||||
},
|
||||
}),
|
||||
click(label) {
|
||||
const button = all(this.render()).find(
|
||||
(n) => n.type === 'button' && text(n) === label,
|
||||
);
|
||||
assert.ok(button, label);
|
||||
button.props.onClick();
|
||||
},
|
||||
change(label, value) {
|
||||
const input = all(this.render()).find(
|
||||
(n) => n.props['aria-label'] === label,
|
||||
);
|
||||
assert.ok(input, label);
|
||||
input.props.onChange({ target: { value } });
|
||||
},
|
||||
};
|
||||
}
|
||||
test('actual progressive PPC form reuses subject, retains deliberate edits and exposes calendar separately from paid proofs',()=>{
|
||||
const c=investmentCase(),h=harness(c);h.click('Preparar obrigação PPC');assert.equal(h.book().ppcObligation.subjectNuit,c.b.subjectNuit);assert.equal(h.book().ppcObligation.previousTax,'');
|
||||
h.change('Enquadramento PPC','ordinary_resident');h.change('Exercício anterior — PPC','2025');h.change('Exercício anterior civil completo','yes');h.change('Imposto liquidado anterior21(1) — MZN','100001');h.change('Retenções na fonte anteriores — MZN','10000');h.change('Fonte da liquidação anterior — PPC','SYNTHETIC tax');h.change('Fonte das retenções anteriores — PPC','SYNTHETIC RF');
|
||||
assert.match(text(h.render()),/75000/);assert.match(text(h.render()),/2026-05/);assert.deepEqual(h.book().ppc,c.b.ppc);
|
||||
h.change('Enquadramento PPC','other');assert.equal(h.book().ppcObligation.previousTax,'100001');assert.match(text(h.render()),/Outros períodos/);h.change('Enquadramento PPC','ordinary_resident');assert.match(text(h.render()),/75000/);
|
||||
const html=renderToStaticMarkup(Component({book:{...c.b,ppcObligation:{...plan(),subjectNuit:c.b.subjectNuit}},onChange:()=>{}}));assert.match(html,/2026-09/);assert.match(html,/Fonte e limites/);
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
calculatePpcObligation,
|
||||
validatePpcObligation,
|
||||
} from '../../resources/js/lib/irpc-ppc-obligation.ts';
|
||||
import { combinedCase } from './helpers/irpc-investment-combination-case.mjs';
|
||||
import {
|
||||
calculateAnnual,
|
||||
recordAnnual,
|
||||
annualCurrent,
|
||||
saveAnnualBook,
|
||||
annualBook,
|
||||
annualHtml,
|
||||
} from '../../resources/js/lib/irpc-annual-settlement.ts';
|
||||
import { plan } from './helpers/irpc-ppc-obligation-case.mjs';
|
||||
const f = (n) => ({ numerator: String(n), denominator: '1' });
|
||||
const calculate = (p) =>
|
||||
calculatePpcObligation(p, '123456789', '2026', 'normal');
|
||||
test('PPC applies80% then three equal instalments rounded upwards to1000, not rounding annual first', () => {
|
||||
let m = calculate(plan());
|
||||
assert.deepEqual(m.base, f(90000));
|
||||
assert.deepEqual(m.annualBeforeRounding, f(72000));
|
||||
assert.deepEqual(m.instalment, f(24000));
|
||||
assert.deepEqual(m.total, f(72000));
|
||||
assert.deepEqual(
|
||||
m.schedule.map((s) => s.month),
|
||||
['2026-05', '2026-07', '2026-09'],
|
||||
);
|
||||
m = calculate({ ...plan(), previousTax: '100001' });
|
||||
assert.deepEqual(m.instalment, f(25000));
|
||||
assert.deepEqual(m.total, f(75000));
|
||||
assert.equal(m.paymentExecuted, false);
|
||||
assert.equal(m.annualDeductionCreated, false);
|
||||
});
|
||||
test('explicit zero and cents retain exact statutory rounding', () => {
|
||||
assert.deepEqual(
|
||||
calculate({ ...plan(), previousTax: '10000' }).total,
|
||||
f(0),
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculate({ ...plan(), previousTax: '10000.01' }).total,
|
||||
f(3000),
|
||||
);
|
||||
});
|
||||
test('unknown,negative base,other periods,wrong subject/year and absent evidence remain indeterminate', () => {
|
||||
for (const change of [
|
||||
{ previousTax: '' },
|
||||
{ previousTax: '1,00' },
|
||||
{ previousTax: '9999' },
|
||||
{ scope: 'other' },
|
||||
{ previousFullCalendarYear: 'no' },
|
||||
{ previousYear: '2024' },
|
||||
{ subjectNuit: '999999999' },
|
||||
{ year: '2027' },
|
||||
{ assessmentReference: '' },
|
||||
{ withholdingReference: '' },
|
||||
]) {
|
||||
const m = calculate({ ...plan(), ...change });
|
||||
assert.ok(m.pending.length);
|
||||
assert.equal(m.instalment, null);
|
||||
assert.deepEqual(m.schedule, []);
|
||||
}
|
||||
assert.throws(() =>
|
||||
validatePpcObligation({ ...plan(), previousTax: null }),
|
||||
);
|
||||
});
|
||||
test('preparation survives existing book, versions/history/report; obligation never becomes paid PPC deduction', () => {
|
||||
const { d, b } = combinedCase(),
|
||||
before = calculateAnnual(d, b);
|
||||
let old = recordAnnual(d, b, '2026-11-11T10:00:00Z');
|
||||
old.ppcObligation = { ...plan(), subjectNuit: b.subjectNuit, year: b.year };
|
||||
assert.equal(annualCurrent(d, old), false);
|
||||
const current = recordAnnual(d, old, '2026-11-11T10:01:00Z');
|
||||
d.modelo_a_snapshot = saveAnnualBook(d, current);
|
||||
const recovered = annualBook(d);
|
||||
assert.deepEqual(recovered.ppcObligation, current.ppcObligation);
|
||||
assert.deepEqual(
|
||||
recovered.result.model.arithmeticBalance,
|
||||
before.arithmeticBalance,
|
||||
);
|
||||
assert.deepEqual(recovered.ppc, b.ppc);
|
||||
assert.equal(annualCurrent(d, recovered), true);
|
||||
assert.equal(recovered.history.length, 1);
|
||||
assert.match(annualHtml(d), /PPC — obrigação preparada/);
|
||||
assert.match(annualHtml(d), /2026-05/);
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import fs from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { combinedCase } from '../tests/frontend/helpers/irpc-investment-combination-case.mjs';
|
||||
import { plan } from '../tests/frontend/helpers/irpc-ppc-obligation-case.mjs';
|
||||
import { calculatePpcObligation } from '../resources/js/lib/irpc-ppc-obligation.ts';
|
||||
import {
|
||||
recordAnnual,
|
||||
saveAnnualBook,
|
||||
annualBook,
|
||||
annualCurrent,
|
||||
annualHtml,
|
||||
} from '../resources/js/lib/irpc-annual-settlement.ts';
|
||||
import { createProjectDossierReport } from '../resources/js/lib/project-dossier-report.ts';
|
||||
const out = new URL(
|
||||
'../outputs/irpc-ppc-obligation-20261111/',
|
||||
import.meta.url,
|
||||
);
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const scenarios = {
|
||||
ordinary: {},
|
||||
rounded: { previousTax: '100001' },
|
||||
zero: { previousTax: '10000' },
|
||||
unknown: { previousTax: '' },
|
||||
other_period: { previousFullCalendarYear: 'no' },
|
||||
};
|
||||
for (const [name, changes] of Object.entries(scenarios)) {
|
||||
if (process.argv[2] === 'generate') {
|
||||
const { d, b } = combinedCase();
|
||||
const prior = recordAnnual(d, b, '2026-11-11T10:00:00Z');
|
||||
prior.ppcObligation = {
|
||||
...plan(),
|
||||
subjectNuit: b.subjectNuit,
|
||||
year: b.year,
|
||||
...changes,
|
||||
};
|
||||
const next = recordAnnual(d, prior, '2026-11-11T10:01:00Z');
|
||||
d.modelo_a_snapshot = saveAnnualBook(d, next);
|
||||
fs.writeFileSync(new URL(name + '.json', out), JSON.stringify(d));
|
||||
} else {
|
||||
const d = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('recovered-' + name + '.json', out),
|
||||
'utf8',
|
||||
),
|
||||
),
|
||||
b = annualBook(d),
|
||||
m = calculatePpcObligation(
|
||||
b.ppcObligation,
|
||||
b.subjectNuit,
|
||||
b.year,
|
||||
b.assessment,
|
||||
);
|
||||
assert.equal(annualCurrent(d, b), true);
|
||||
assert.equal(b.history.length, 1);
|
||||
assert.deepEqual(b.result.model.arithmeticBalance, {
|
||||
numerator: '138',
|
||||
denominator: '1',
|
||||
});
|
||||
const html = annualHtml(d),
|
||||
report = createProjectDossierReport(d, {
|
||||
selectedSections: new Set(['finance']),
|
||||
generatedAt: '2026-11-11T10:02:00Z',
|
||||
});
|
||||
assert.match(html, /PPC — obrigação preparada/);
|
||||
assert.match(JSON.stringify(report), /PPC — obrigação preparada/);
|
||||
assert.equal(m.paymentExecuted, false);
|
||||
assert.equal(m.annualDeductionCreated, false);
|
||||
if (name === 'ordinary') assert.equal(m.total.numerator, '72000');
|
||||
if (name === 'rounded') assert.equal(m.total.numerator, '75000');
|
||||
if (name === 'zero') assert.equal(m.total.numerator, '0');
|
||||
if (['unknown', 'other_period'].includes(name))
|
||||
assert.equal(m.total, null);
|
||||
fs.writeFileSync(new URL(name + '-report.html', out), html);
|
||||
fs.writeFileSync(
|
||||
new URL(name + '-verification.json', out),
|
||||
JSON.stringify(
|
||||
{
|
||||
synthetic: true,
|
||||
apiRecovered: true,
|
||||
current: true,
|
||||
historyPreserved: true,
|
||||
annualBalanceUnchanged: true,
|
||||
report: true,
|
||||
visualReview: false,
|
||||
model: m,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
process.argv[2] === 'generate'
|
||||
? 'Five synthetic PPC scenarios prepared.'
|
||||
: 'Five native recovered PPC scenarios recalculated and reported without creating deductions.',
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
if (($argv[1] ?? '') !== 'test' || basename($root) !== 'portal-investidor') {
|
||||
throw new RuntimeException('Explicit local test operation required.');
|
||||
}
|
||||
foreach (['APP_ENV' => 'testing', 'APP_KEY' => 'base64:'.base64_encode(str_repeat('Q', 32)),
|
||||
'APP_CONFIG_CACHE' => $root.'/outputs/irpc-ppc-obligation-20261111/no-config.php',
|
||||
'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441',
|
||||
'DB_DATABASE' => 'proactive_investment_20261027_test', 'DB_USERNAME' => 'investmoz_test', 'DB_PASSWORD' => '',
|
||||
'DB_URL' => '', 'DATABASE_URL' => '', 'CACHE_STORE' => 'array', 'SESSION_DRIVER' => 'array',
|
||||
'QUEUE_CONNECTION' => 'sync', 'MAIL_MAILER' => 'array', 'SUITECRM_ENABLED' => 'false'] as $key => $value) {
|
||||
putenv($key.'='.$value);
|
||||
$_ENV[$key] = $_SERVER[$key] = $value;
|
||||
}
|
||||
$pdo = new PDO('pgsql:host=127.0.0.1;port=55441;dbname=proactive_investment_20261027_test', 'investmoz_test', '');
|
||||
$identity = $pdo->query('select current_database() as name,inet_server_port() as port')->fetch(PDO::FETCH_ASSOC);
|
||||
if ($identity['name'] !== 'proactive_investment_20261027_test' || (int) $identity['port'] !== 55441) {
|
||||
throw new RuntimeException('Isolated QA identity required.');
|
||||
}
|
||||
$argv = ['phpunit', '-c', 'phpunit.investmoz.xml', '--do-not-cache-result', 'tests/Feature/IrpcPpcObligationDraftTest.php'];
|
||||
$argc = count($argv);
|
||||
$_SERVER['argv'] = $argv;
|
||||
$_SERVER['argc'] = $argc;
|
||||
require $root.'/vendor/autoload.php';
|
||||
require $root.'/vendor/phpunit/phpunit/phpunit';
|
||||
Reference in New Issue
Block a user