Group documented prior PEC basis and excess PPC reconciliation for viability
tests / ci (push) Waiting to run
tests / ci (push) Waiting to run
This commit is contained in:
@@ -9867,3 +9867,18 @@ Instalação autorizada e concluída na homologação https://teste-investidor.t
|
||||
Backup/restauro fresco de193tabelas/412ficheiros;15fontes verificadas por hash;21tabelas protegidas sem diferenças. Mantidos12lotes/920456observações demográficas, instituições, dossiers, relatórios, pesquisa, suporte e permissões. Sem migrações/importações ou publicação de dados. Readiness demográfica vinculada à revisão com original arquivado. Configuração e fonte do executor inalteradas. HTTPS e entrada privada conferidos. CI obrigatório executado normalmente; provas anteriores reutilizadas sem nova ronda manual. Recibo: outputs/observation-period-20261113/homologation-installed.json; pacote QA selado preservado.
|
||||
|
||||
Este delta deixa de ser pendência de instalação; ACT-02 continua parcial quanto à cobertura factual, classificação do legado, oferta laboral, séries estatisticamente comparáveis e metodologia económica. A ferramenta não cria séries factuais por si.
|
||||
|
||||
|
||||
## 13/11/2026 — lote PEC ordinário: último montante e PPC acima da base, QA
|
||||
|
||||
Conforme a directiva de agrupar melhorias relacionadas, este lote reúne dois resíduos do percurso de viabilidade fiscal, sem instalação por cada correcção. Reutiliza a preparação PEC, livro anual, rascunho privado, histórico e relatório do mesmo dossier; não cria um motor ou módulo paralelo. Recorte:2026, residente geral comercial/industrial/agrícola, ano civil, MZN. Homologação mantém2bad72c0.
|
||||
|
||||
1. Sem volume anterior, permite indicar o último montante PEC já determinado com NUIT, exercício, valor, fonte/fundamento e confirmação de que é o último aplicável ao caso. A hipótese documental29(5) conserva esse montante; não o confunde com volume de negócios ou pagamentos somados, não aplica de novo0,5%/limites nem deduz PPC pela segunda vez. Ausência, incompatibilidade, exercício futuro/anterior ao início ou falta de conferência mantêm a estimativa indeterminada. A referência fica conservada quando deixa de ser usada; alterações exigem reconferência. Uma rectificação documental para volume zero usa a mesma referência. Isto implementa a hipótese documentada, não uma inferência automática da qualificação fiscal ou do último montante a partir de dados inexistentes.
|
||||
|
||||
2. No cálculo ordinário29(2), PPC documentados acima da base limitada produzem estimativa PEC zero e diferença documental explícita. Não criam valor negativo, reembolso, crédito, transporte ou dedução anual. Esta regra tem marcador de versão na chave do livro apenas quando afecta o caso; obriga a recalcular resultados antigos dependentes, preservando histórico e casos ordinários anteriores. Fonte reutilizada:BR16/I16-04-2008,p122/PDF24,original SHA256fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee.
|
||||
|
||||
Os campos têm rótulos e controlos separados de largura completa; referências vazias aparecem por confirmar. Consulta do suporte e apresentação dos efeitos são parte do mesmo formulário. Não executa pagamentos ou declarações. Fonte, escolhas, original e histórico conservados no rascunho e no relatório.
|
||||
|
||||
QA:21testes frontend afectados, incluindo seis novos; uma prova nativa/42asserções de POST/PUT/GET em transacção com rollback na base isolada investmoz_demography_test:55441. Quatro cenários:último montante42500,25, fonte ausente, volume positivo com referência conservada e PPC60000/base50000/estimativa0/diferença10000. Recuperação e relatório financeiro do dossier conferidos; saldo anual138MZN e histórico anterior inalterados. Tipos, lint focado, sintaxe PHP, Pint e build QA aprovados. Edge:componentes reais compilados com dados sintéticos, ambos os efeitos, confirmação retirada após edição e apresentação final conferidos; sem API/escrita na base pela prévia. A primeira prévia importava um auxiliar Node com filesystem; foi substituído por JSON sintético gerado, sem alterar o produto para contornar o erro. Provas em outputs/irpc-pec-last-amount-20261113/. Não repetidas as provas aceites dos percursos não afectados.
|
||||
|
||||
Estado:implementado e verificado em QA; pacote agrupado, ainda sem instalação e dependente de autorização concreta. Não encerra L8 nem o portal. Restam períodos/regimes especiais, qualificação documental de casos fora deste recorte e os demais resíduos L1–L7/restanteL8/L9–L16 e módulos. Execução de pagamentos não é requisito do portal. A instalação deverá usar o executor Git, com backup/restauro fresco e reversão de release; preservar snapshots/históricos produzidos após a entrega, sem restaurar uma base antiga sobre edições posteriores.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import {SimulationIrpcPecCorrection} from './simulation-irpc-pec-correction';
|
||||
import {SimulationIrpcPecPlan} from './simulation-irpc-pec-plan';
|
||||
import { SimulationIrpcPecCorrection } from './simulation-irpc-pec-correction';
|
||||
import { SimulationIrpcPecPlan } from './simulation-irpc-pec-plan';
|
||||
import {
|
||||
newPecObligation,
|
||||
calculatePecObligation,
|
||||
@@ -42,12 +42,16 @@ export function SimulationIrpcPecObligation({
|
||||
patch = (changes: Partial<PecObligation>) =>
|
||||
onChange({ ...p, ...changes });
|
||||
const field = (
|
||||
key: Exclude<keyof PecObligation, 'version' | 'scope' | 'plan' | 'correction'>,
|
||||
key: Exclude<
|
||||
keyof PecObligation,
|
||||
'version' | 'scope' | 'plan' | 'correction' | 'lastAmount'
|
||||
>,
|
||||
label: string,
|
||||
) => (
|
||||
<label className="block">
|
||||
{label}
|
||||
<input
|
||||
className="mt-1 block w-full rounded border px-3 py-2"
|
||||
aria-label={label}
|
||||
value={p[key]}
|
||||
onChange={(e) => patch({ [key]: e.target.value })}
|
||||
@@ -63,6 +67,7 @@ export function SimulationIrpcPecObligation({
|
||||
<label className="block">
|
||||
Enquadramento PEC
|
||||
<select
|
||||
className="mt-1 block w-full rounded border px-3 py-2"
|
||||
aria-label="Enquadramento PEC"
|
||||
value={p.scope}
|
||||
onChange={(e) =>
|
||||
@@ -104,7 +109,131 @@ export function SimulationIrpcPecObligation({
|
||||
'turnoverReference',
|
||||
'Fonte do volume de negócios — PEC',
|
||||
)}
|
||||
<SimulationIrpcPecCorrection value={p.correction} original={p.previousTurnover} subjectNuit={p.subjectNuit} onChange={correction=>patch({correction})}/>
|
||||
<SimulationIrpcPecCorrection
|
||||
value={p.correction}
|
||||
original={p.previousTurnover}
|
||||
subjectNuit={p.subjectNuit}
|
||||
onChange={(correction) => patch({ correction })}
|
||||
/>
|
||||
{(m.requiresLastAmount || p.lastAmount) && (
|
||||
<details className="rounded border p-3">
|
||||
<summary>
|
||||
Sem volume anterior — referência PEC dos
|
||||
anos anteriores
|
||||
</summary>
|
||||
<p>
|
||||
Indique o último montante PEC já
|
||||
determinado, com suporte do mesmo
|
||||
titular, ano civil e MZN. Não introduza
|
||||
volume de negócios ou somatório de
|
||||
pagamentos. A hipótese conserva o valor
|
||||
documental sem nova percentagem ou
|
||||
segunda dedução de PPC.
|
||||
</p>
|
||||
{!p.lastAmount ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
patch({
|
||||
lastAmount: {
|
||||
version: 1,
|
||||
subjectNuit:
|
||||
p.subjectNuit,
|
||||
year: '',
|
||||
amount: '',
|
||||
reference: '',
|
||||
confirmed: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Acrescentar referência PEC anterior
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
{(
|
||||
[
|
||||
'subjectNuit',
|
||||
'year',
|
||||
'amount',
|
||||
'reference',
|
||||
] as const
|
||||
).map((key) => {
|
||||
const names = {
|
||||
subjectNuit:
|
||||
'NUIT do montante PEC anterior',
|
||||
year: 'Exercício do último montante PEC',
|
||||
amount: 'Último montante PEC já determinado — MZN',
|
||||
reference:
|
||||
'Fonte e fundamento do último montante PEC',
|
||||
};
|
||||
return (
|
||||
<label
|
||||
className="block"
|
||||
key={key}
|
||||
>
|
||||
{names[key]}
|
||||
<input
|
||||
className="mt-1 block w-full rounded border px-3 py-2"
|
||||
aria-label={
|
||||
names[key]
|
||||
}
|
||||
value={
|
||||
p.lastAmount![
|
||||
key
|
||||
]
|
||||
}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
lastAmount:
|
||||
{
|
||||
...p.lastAmount!,
|
||||
[key]: e
|
||||
.target
|
||||
.value,
|
||||
confirmed:
|
||||
'',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
<label className="block">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
p.lastAmount
|
||||
.confirmed === 'yes'
|
||||
}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
lastAmount: {
|
||||
...p.lastAmount!,
|
||||
confirmed: e
|
||||
.target
|
||||
.checked
|
||||
? 'yes'
|
||||
: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
Conferi no suporte que é o
|
||||
último montante PEC aplicável a
|
||||
esta hipótese
|
||||
</label>
|
||||
<p>
|
||||
Alterar a referência exige nova
|
||||
conferência. Guardar conserva o
|
||||
original e o histórico; não
|
||||
certifica a situação fiscal.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</details>
|
||||
)}
|
||||
{field(
|
||||
'previousPpcPaid',
|
||||
'PPC efectivamente pagos no ano anterior — MZN',
|
||||
@@ -124,18 +253,31 @@ export function SimulationIrpcPecObligation({
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
Total estimado:{' '}
|
||||
{fractionPresentation(m.total!).decimal} MZN.
|
||||
Total estimado: {fractionPresentation(m.total!).decimal}{' '}
|
||||
MZN.
|
||||
</p>
|
||||
<ul>
|
||||
{m.months.map((month) => (
|
||||
<li key={month}>{month}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
</>
|
||||
)}
|
||||
<SimulationIrpcPecPlan plan={p.plan} total={m.total} notApplicable={m.notApplicable} year={book.year} onChange={plan=>patch({plan})}/>
|
||||
{m.ppcExcess && (
|
||||
<p role="status">
|
||||
Os PPC anteriores cobrem a base limitada desta hipótese.
|
||||
Estimativa PEC: 0 MZN;diferença documental:{' '}
|
||||
{fractionPresentation(m.ppcExcess).decimal} MZN. Esta
|
||||
diferença não cria reembolso,crédito ou dedução anual.
|
||||
</p>
|
||||
)}
|
||||
<SimulationIrpcPecPlan
|
||||
plan={p.plan}
|
||||
total={m.total}
|
||||
notApplicable={m.notApplicable}
|
||||
year={book.year}
|
||||
onChange={(plan) => patch({ plan })}
|
||||
/>
|
||||
<p>
|
||||
Guardar o rascunho conserva esta preparação. Calcular o fecho
|
||||
anual inclui-a no relatório. Não regista pagamentos,cria
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {pecCorrectionVersion} from './irpc-pec-correction.ts';
|
||||
import {pecPlanVersion} from './irpc-pec-plan.ts';
|
||||
import {validatePecObligation,pecObligationReport,pecObligationVersion,type PecObligation} from './irpc-pec-obligation.ts';
|
||||
import {validatePecObligation,pecObligationReport,pecObligationVersion,pecPpcExcessVersion,calculatePecObligation,type PecObligation} from './irpc-pec-obligation.ts';
|
||||
import { validatePpcObligation, ppcObligationReport, ppcObligationVersion, type PpcObligation } from './irpc-ppc-obligation.ts';
|
||||
import {
|
||||
newInvestmentCredit,
|
||||
@@ -575,6 +575,7 @@ function key(d: Record<string, unknown>, b: AnnualBook) {
|
||||
dependencies(d, !!b.investmentCredit),
|
||||
...(b.ppcObligation ? [ppcObligationVersion] : []),
|
||||
...(b.pecObligation ? [pecObligationVersion] : []),
|
||||
...(b.pecObligation && calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment).ppcExcess ? [pecPpcExcessVersion] : []),
|
||||
...(b.pecObligation?.plan ? [pecPlanVersion] : []),
|
||||
...(b.pecObligation?.correction ? [pecCorrectionVersion] : []),
|
||||
...(b.investmentCredit && b.creditCarry?.lots.length
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import {validatePecCorrection,correctedPecTurnover,pecCorrectionSource,type PecCorrection} from './irpc-pec-correction.ts';
|
||||
import {validatePecPlan,pecPlanReport,type PecPlan} from './irpc-pec-plan.ts';
|
||||
import {
|
||||
validatePecCorrection,
|
||||
correctedPecTurnover,
|
||||
pecCorrectionSource,
|
||||
type PecCorrection,
|
||||
} from './irpc-pec-correction.ts';
|
||||
import {
|
||||
validatePecPlan,
|
||||
pecPlanReport,
|
||||
type PecPlan,
|
||||
} from './irpc-pec-plan.ts';
|
||||
import {
|
||||
decimalFraction,
|
||||
fraction,
|
||||
@@ -8,9 +17,21 @@ import {
|
||||
type FiscalFraction,
|
||||
} from './fiscal-rational.ts';
|
||||
export const pecObligationVersion = 'IRPC-PEC-obligation/1';
|
||||
export const pecPpcExcessVersion = 'IRPC-PEC-PPC-excess/1';
|
||||
export const pecObligationSource =
|
||||
'RIRPC,Decreto9/2008,art29(1–5),BR16/I16-04-2008,original SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee.0,5% do volume de negócios anterior,limites30.000/100.000MZN,menos PPC efectivamente pagos no ano anterior.Junho/Agosto/Outubro;opção de pagamento único prevista,sem data ou repartição inventada. Ano de início excluído29(4);sem volume anterior exige último montante29(5). Recorte2026,residente geral comercial/industrial/agrícola,ano civil,MZN. Regimes/períodos especiais,rectificação29(3),base de anos anteriores29(5),guias/juros separados.';
|
||||
export type PecLastAmount = {
|
||||
version: 1;
|
||||
subjectNuit: string;
|
||||
year: string;
|
||||
amount: string;
|
||||
reference: string;
|
||||
confirmed: '' | 'yes';
|
||||
};
|
||||
export const pecLastAmountSource =
|
||||
'RIRPC,Decreto9/2008,art29(5),BR16/I16-04-2008,p122/PDF24,original SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee. Hipótese documentada: último montante PEC já determinado,para titular/ano civil/MZN correspondentes;sem recalcular 0,5%,limites ou deduzir PPC pela segunda vez. Não demonstra por si a qualificação fiscal do caso.';
|
||||
export type PecObligation = {
|
||||
lastAmount?: PecLastAmount;
|
||||
version: 1;
|
||||
plan?: PecPlan;
|
||||
correction?: PecCorrection;
|
||||
@@ -42,8 +63,24 @@ export const newPecObligation = (
|
||||
ppcReference: '',
|
||||
});
|
||||
export function validatePecObligation(p: PecObligation) {
|
||||
if(p?.correction!==undefined)validatePecCorrection(p.correction);
|
||||
if(p?.plan!==undefined)validatePecPlan(p.plan);
|
||||
if (p?.lastAmount !== undefined) {
|
||||
const v = p.lastAmount;
|
||||
if (
|
||||
!v ||
|
||||
v.version !== 1 ||
|
||||
!['', 'yes'].includes(v.confirmed) ||
|
||||
['subjectNuit', 'year', 'amount', 'reference'].some(
|
||||
(k) =>
|
||||
typeof (v as unknown as Record<string, unknown>)[k] !==
|
||||
'string',
|
||||
)
|
||||
)
|
||||
throw Error(
|
||||
'Referência PEC anterior não recuperável;original preservado.',
|
||||
);
|
||||
}
|
||||
if (p?.correction !== undefined) validatePecCorrection(p.correction);
|
||||
if (p?.plan !== undefined) validatePecPlan(p.plan);
|
||||
if (
|
||||
!p ||
|
||||
p.version !== 1 ||
|
||||
@@ -68,11 +105,18 @@ export function calculatePecObligation(
|
||||
) {
|
||||
validatePecObligation(p);
|
||||
const pending: string[] = [];
|
||||
const turnover=correctedPecTurnover(p.correction,p.previousTurnover,p.subjectNuit,p.previousYear);
|
||||
const turnover = correctedPecTurnover(
|
||||
p.correction,
|
||||
p.previousTurnover,
|
||||
p.subjectNuit,
|
||||
p.previousYear,
|
||||
);
|
||||
let percentage: FiscalFraction | null = null,
|
||||
limited: FiscalFraction | null = null,
|
||||
total: FiscalFraction | null = null,
|
||||
notApplicable = false;
|
||||
ppcExcess: FiscalFraction | null = null,
|
||||
notApplicable = false,
|
||||
lastAmountApplied = false;
|
||||
if (
|
||||
p.scope !== 'ordinary_resident' ||
|
||||
assessment !== 'normal' ||
|
||||
@@ -117,11 +161,26 @@ export function calculatePecObligation(
|
||||
if (!pending.length) {
|
||||
const v = decimalFraction(turnover.value),
|
||||
ppc = decimalFraction(p.previousPpcPaid);
|
||||
if (BigInt(v.numerator) === 0n)
|
||||
pending.push(
|
||||
'PEC29(5):sem volume de negócios em2025,recuperar último montante dos anos anteriores;zero não determina automaticamente o mínimo.',
|
||||
);
|
||||
else {
|
||||
if (BigInt(v.numerator) === 0n) {
|
||||
const last = p.lastAmount;
|
||||
if (
|
||||
!last ||
|
||||
last.confirmed !== 'yes' ||
|
||||
!last.reference.trim() ||
|
||||
last.subjectNuit !== subjectNuit ||
|
||||
!/^\d{4}$/.test(last.year) ||
|
||||
Number(last.year) < Number(p.activityStartYear) ||
|
||||
Number(last.year) >= Number(year) ||
|
||||
!/^\d{1,18}(\.\d{1,2})?$/.test(last.amount)
|
||||
) {
|
||||
pending.push(
|
||||
'PEC29(5):sem volume de negócios em2025,recuperar último montante PEC já determinado dos anos anteriores,com titular,exercício,fonte e confirmação de que é o último aplicável;zero não determina automaticamente o mínimo.',
|
||||
);
|
||||
} else {
|
||||
total = decimalFraction(last.amount);
|
||||
lastAmountApplied = true;
|
||||
}
|
||||
} else {
|
||||
percentage = fraction(
|
||||
BigInt(v.numerator),
|
||||
BigInt(v.denominator) * 200n,
|
||||
@@ -135,23 +194,51 @@ export function calculatePecObligation(
|
||||
? fraction(100000n)
|
||||
: percentage;
|
||||
const residual = subtractFractions(limited, ppc);
|
||||
if (BigInt(residual.numerator) < 0n)
|
||||
pending.push(
|
||||
'PEC:PPC anteriores excedem o montante limitado;caso separado para reconciliação,não gera estimativa negativa ou reembolso.',
|
||||
);
|
||||
else total = residual;
|
||||
if (BigInt(residual.numerator) < 0n) {
|
||||
total = fraction(0n);
|
||||
ppcExcess = subtractFractions(ppc, limited);
|
||||
} else total = residual;
|
||||
}
|
||||
}
|
||||
}
|
||||
let source = p.correction
|
||||
? pecObligationSource.replace('rectificação29(3),', '') +
|
||||
' ' +
|
||||
pecCorrectionSource
|
||||
: pecObligationSource;
|
||||
if (p.lastAmount)
|
||||
source =
|
||||
source.replace('base de anos anteriores29(5),', '') +
|
||||
' ' +
|
||||
pecLastAmountSource;
|
||||
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: p.correction ? pecObligationSource.replace('rectificação29(3),','')+' '+pecCorrectionSource : pecObligationSource,
|
||||
source,
|
||||
lastAmountSource: p.lastAmount ? pecLastAmountSource : null,
|
||||
pending,
|
||||
percentage,
|
||||
limited,
|
||||
total,
|
||||
notApplicable,
|
||||
turnoverCorrection: p.correction ? {original:p.correction.originalTurnover,revised:p.correction.revisedTurnover,applied:!notApplicable&&turnover.applied,pending:turnover.pending,reference:p.correction.reference,source:pecCorrectionSource}:null,
|
||||
lastAmountApplied,
|
||||
ppcExcess,
|
||||
requiresLastAmount:
|
||||
!notApplicable &&
|
||||
turnover.pending.length === 0 &&
|
||||
/^0+(\.0+)?$/.test(turnover.value),
|
||||
turnoverCorrection: p.correction
|
||||
? {
|
||||
original: p.correction.originalTurnover,
|
||||
revised: p.correction.revisedTurnover,
|
||||
applied: !notApplicable && turnover.applied,
|
||||
pending: turnover.pending,
|
||||
reference: p.correction.reference,
|
||||
source: pecCorrectionSource,
|
||||
}
|
||||
: null,
|
||||
months:
|
||||
total && !notApplicable ? ['2026-06', '2026-08', '2026-10'] : [],
|
||||
instalmentAmountsDetermined: false,
|
||||
@@ -168,11 +255,86 @@ export function pecObligationReport(
|
||||
const m = calculatePecObligation(p, subjectNuit, year, assessment),
|
||||
show = (v: FiscalFraction | null) =>
|
||||
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
|
||||
const originalEstimate=p.correction ? calculatePecObligation({...p,correction:undefined},subjectNuit,year,assessment).total:null;
|
||||
const correctionEffect=originalEstimate&&m.total ? fractionPresentation(subtractFractions(m.total,originalEstimate)).decimal+' MZN':'por determinar';
|
||||
const originalEstimate = p.correction
|
||||
? calculatePecObligation(
|
||||
{ ...p, correction: undefined },
|
||||
subjectNuit,
|
||||
year,
|
||||
assessment,
|
||||
).total
|
||||
: null;
|
||||
const correctionEffect =
|
||||
originalEstimate && m.total
|
||||
? fractionPresentation(subtractFractions(m.total, originalEstimate))
|
||||
.decimal + ' MZN'
|
||||
: 'por determinar';
|
||||
return [
|
||||
...(p.correction ? [['IRPC — L8','PEC — rectificação do volume','Original: '+p.correction.originalTurnover+' MZN;corrigido: '+p.correction.revisedTurnover+' MZN;'+(m.turnoverCorrection?.applied?'aplicada à estimativa.':'conservada sem aplicação à estimativa.')+' Estimativa original: '+show(originalEstimate)+';efeito na estimativa: '+correctionEffect+'. '+turnoverCorrectionNotice(m.turnoverCorrection),p.correction.reference+'; '+pecCorrectionSource] as [string,string,string,string]]:[]),
|
||||
...(p.plan ? pecPlanReport(p.plan,m.total,m.notApplicable,year) : []),
|
||||
...(m.ppcExcess
|
||||
? [
|
||||
[
|
||||
'IRPC — L8',
|
||||
'PEC — PPC acima da base limitada',
|
||||
'PPC documentados: ' +
|
||||
p.previousPpcPaid +
|
||||
' MZN;base limitada: ' +
|
||||
show(m.limited) +
|
||||
';diferença: ' +
|
||||
show(m.ppcExcess) +
|
||||
';estimativa PEC: 0 MZN. A diferença é apenas reconciliação desta hipótese; não cria reembolso,crédito,transporte ou dedução anual.',
|
||||
p.ppcReference +
|
||||
'; ' +
|
||||
p.turnoverReference +
|
||||
'; RIRPC29(2); ' +
|
||||
pecPpcExcessVersion,
|
||||
] as [string, string, string, string],
|
||||
]
|
||||
: []),
|
||||
...(p.lastAmount
|
||||
? [
|
||||
[
|
||||
'IRPC — L8',
|
||||
'PEC — último montante documentado',
|
||||
'Exercício ' +
|
||||
(p.lastAmount.year || 'por confirmar') +
|
||||
';montante PEC já determinado: ' +
|
||||
(p.lastAmount.amount || 'por confirmar') +
|
||||
' MZN;' +
|
||||
(m.lastAmountApplied
|
||||
? 'usado como hipótese documentada29(5),sem novo cálculo percentual ou segunda dedução de PPC.'
|
||||
: 'conservado sem aplicação à estimativa.') +
|
||||
' Não regista pagamento nem dedução anual.',
|
||||
(p.lastAmount.reference || 'Fonte por indicar') +
|
||||
'; ' +
|
||||
pecLastAmountSource,
|
||||
] as [string, string, string, string],
|
||||
]
|
||||
: []),
|
||||
...(p.correction
|
||||
? [
|
||||
[
|
||||
'IRPC — L8',
|
||||
'PEC — rectificação do volume',
|
||||
'Original: ' +
|
||||
p.correction.originalTurnover +
|
||||
' MZN;corrigido: ' +
|
||||
p.correction.revisedTurnover +
|
||||
' MZN;' +
|
||||
(m.turnoverCorrection?.applied
|
||||
? 'aplicada à estimativa.'
|
||||
: 'conservada sem aplicação à estimativa.') +
|
||||
' Estimativa original: ' +
|
||||
show(originalEstimate) +
|
||||
';efeito na estimativa: ' +
|
||||
correctionEffect +
|
||||
'. ' +
|
||||
turnoverCorrectionNotice(m.turnoverCorrection),
|
||||
p.correction.reference + '; ' + pecCorrectionSource,
|
||||
] as [string, string, string, string],
|
||||
]
|
||||
: []),
|
||||
...(p.plan
|
||||
? pecPlanReport(p.plan, m.total, m.notApplicable, year)
|
||||
: []),
|
||||
[
|
||||
'IRPC — L8',
|
||||
'PEC — estimativa fiscal',
|
||||
@@ -180,15 +342,22 @@ export function pecObligationReport(
|
||||
? m.pending.join(' ')
|
||||
: m.notApplicable
|
||||
? 'Não aplicável no ano de início da actividade29(4);total0 MZN.'
|
||||
: '0,5%: ' +
|
||||
show(m.percentage) +
|
||||
';limitado antes de PPC: ' +
|
||||
show(m.limited) +
|
||||
';total estimado: ' +
|
||||
show(m.total) +
|
||||
';meses: ' +
|
||||
m.months.join(', ') +
|
||||
(p.plan ? '. Plano explícito apresentado separadamente;prazo legal não validado.' : '. Montantes por prestação e opção de pagamento único não determinados neste recorte.') + ' Preparação não é pagamento nem dedução anual.',
|
||||
: m.lastAmountApplied
|
||||
? 'Hipótese documentada29(5): ' +
|
||||
show(m.total) +
|
||||
';último montante PEC já determinado,sem segunda dedução de PPC. Confirmar enquadramento com o suporte indicado.'
|
||||
: '0,5%: ' +
|
||||
show(m.percentage) +
|
||||
';limitado antes de PPC: ' +
|
||||
show(m.limited) +
|
||||
';total estimado: ' +
|
||||
show(m.total) +
|
||||
';meses: ' +
|
||||
m.months.join(', ') +
|
||||
(p.plan
|
||||
? '. Plano explícito apresentado separadamente;prazo legal não validado.'
|
||||
: '. Montantes por prestação e opção de pagamento único não determinados neste recorte.') +
|
||||
' Preparação não é pagamento nem dedução anual.',
|
||||
p.activityReference +
|
||||
'; ' +
|
||||
p.turnoverReference +
|
||||
@@ -200,4 +369,6 @@ export function pecObligationReport(
|
||||
];
|
||||
}
|
||||
|
||||
function turnoverCorrectionNotice(c:{pending:string[]}|null){return c?.pending.join(' ')??'';}
|
||||
function turnoverCorrectionNotice(c: { pending: string[] } | null) {
|
||||
return c?.pending.join(' ') ?? '';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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 IrpcPecLastAmountDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_pec_obligation_and_calendar_survive_native_draft_without_new_paid_deductions(): void
|
||||
{
|
||||
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
$this->assertSame('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 (['documented' => 138, 'missing' => 138, 'positive' => 138, 'excess' => 138] as $name => $balance) {
|
||||
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-pec-last-amount-20261113/'.$name.'.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$personal = ['person_type' => $snapshot['person_type']];
|
||||
unset($snapshot['person_type']);
|
||||
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $snapshot];
|
||||
if ($id === null) {
|
||||
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||
} else {
|
||||
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
|
||||
}
|
||||
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
|
||||
$etag = $response->headers->get('ETag');
|
||||
$back = $response->json('data.economic_snapshot');
|
||||
$this->assertSame($snapshot['modelo_a_snapshot'], $back['modelo_a_snapshot']);
|
||||
$book = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$snapshot['context_opportunity']];
|
||||
$this->assertCount(1, $book['history']);
|
||||
$this->assertSame(['numerator' => '138', 'denominator' => '1'], $book['history'][0]['model']['arithmeticBalance']);
|
||||
$this->assertSame($balance === null ? null : ['numerator' => (string) $balance, 'denominator' => '1'], $book['result']['model']['arithmeticBalance']);
|
||||
$this->assertSame($book['pecObligation'], $book['result']['model']['input']['pecObligation']);
|
||||
$this->assertSame('42500.25', $book['pecObligation']['lastAmount']['amount']);
|
||||
$this->assertSame('2024', $book['pecObligation']['lastAmount']['year']);
|
||||
$this->assertSame('SYNTHETIC effective activity'.($name === 'start' ? '2026' : '2020'), $book['pecObligation']['activityReference']);
|
||||
file_put_contents(base_path('outputs/irpc-pec-last-amount-20261113/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {plan} from './helpers/irpc-pec-obligation-case.mjs';
|
||||
import {combinedCase} from './helpers/irpc-investment-combination-case.mjs';
|
||||
import {calculatePecObligation,validatePecObligation} from '../../resources/js/lib/irpc-pec-obligation.ts';
|
||||
import {recordAnnual,saveAnnualBook,annualBook,annualCurrent,annualHtml} from '../../resources/js/lib/irpc-annual-settlement.ts';
|
||||
import {calculatePecPlan,newPecPlan} from '../../resources/js/lib/irpc-pec-plan.ts';
|
||||
const f=n=>({numerator:String(n),denominator:'1'});
|
||||
const sample=()=>{const p={...plan(),previousTurnover:'0.00'};return {...p,lastAmount:{version:1,subjectNuit:p.subjectNuit,year:'2024',amount:'42500.25',reference:'SYNTHETIC final PEC amount; latest applicable annual ordinary MZN reference',confirmed:'yes'}}};
|
||||
const calc=p=>calculatePecObligation(p,p.subjectNuit,p.year,'normal');
|
||||
test('documented latest PEC amount is retained without percentage, caps or a second PPC deduction',()=>{
|
||||
const p=sample(),before=JSON.stringify(p),m=calc(p);
|
||||
assert.deepEqual(m.total,{numerator:'170001',denominator:'4'});assert.equal(m.percentage,null);assert.equal(m.limited,null);assert.equal(m.lastAmountApplied,true);assert.equal(m.requiresLastAmount,true);
|
||||
assert.equal(m.paymentExecuted,false);assert.equal(m.annualDeductionCreated,false);assert.equal(JSON.stringify(p),before);
|
||||
p.lastAmount.amount='0';assert.deepEqual(calc(p).total,f(0));
|
||||
});
|
||||
test('absent stale unconfirmed unsupported or malformed historical references never determine the estimate',()=>{
|
||||
for(const changes of [{confirmed:''},{reference:''},{subjectNuit:'999999999'},{year:'2026'},{year:'2019'},{amount:''},{amount:'-1'},{amount:'NaN'}]){const p=sample();Object.assign(p.lastAmount,changes);assert.equal(calc(p).total,null,JSON.stringify(changes));}
|
||||
const p=sample();delete p.lastAmount;assert.equal(calc(p).total,null);
|
||||
assert.throws(()=>validatePecObligation({...sample(),lastAmount:{...sample().lastAmount,amount:null}}));
|
||||
});
|
||||
test('ordinary positive turnover and activity start do not use or erase a saved historical reference',()=>{
|
||||
const p=sample();p.previousTurnover='10000000';assert.deepEqual(calc(p).total,f(40000));assert.equal(calc(p).lastAmountApplied,false);assert.equal(p.lastAmount.amount,'42500.25');
|
||||
p.activityStartYear='2026';assert.deepEqual(calc(p).total,f(0));assert.equal(calc(p).lastAmountApplied,false);
|
||||
});
|
||||
test('native annual recovery and report retain history and require reconfirming cash assumptions after the estimate changes',()=>{
|
||||
const {d,b}=combinedCase();b.pecObligation={...sample(),subjectNuit:b.subjectNuit,lastAmount:{...sample().lastAmount,subjectNuit:b.subjectNuit}};
|
||||
const prior=recordAnnual(d,b,'2026-11-13T21:00:00Z');
|
||||
const changed={...prior,pecObligation:{...prior.pecObligation,lastAmount:{...prior.pecObligation.lastAmount,amount:'50000'}}};
|
||||
assert.equal(annualCurrent(d,changed),false);
|
||||
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,changed,'2026-11-13T21:01:00Z'));
|
||||
const recovered=annualBook(d);assert.equal(annualCurrent(d,recovered),true);assert.equal(recovered.history.length,1);assert.deepEqual(recovered.result.model.arithmeticBalance,f(138));
|
||||
assert.match(annualHtml(d),/PEC — último montante documentado/);assert.match(annualHtml(d),/50000/);
|
||||
const cash={...newPecPlan(),mode:'three',june:'42500.25',august:'0',october:'0',reference:'SYNTHETIC prior cash hypothesis'};
|
||||
const cm=calculatePecPlan(cash,calc(recovered.pecObligation).total,false,'2026');assert.ok(cm.pending.length);
|
||||
});
|
||||
|
||||
test('unknown turnover cannot be bypassed and a documented correction to zero reuses the historical reference once',()=>{
|
||||
const p=sample();p.previousTurnover='';assert.equal(calc(p).total,null);
|
||||
p.previousTurnover='10000000';p.correction={version:1,apply:'yes',originalTurnover:p.previousTurnover,revisedTurnover:'0.00',subjectNuit:p.subjectNuit,turnoverYear:'2025',reference:'SYNTHETIC corrected sales to zero'};
|
||||
assert.equal(calc(p).requiresLastAmount,true);assert.equal(calc(p).lastAmountApplied,true);assert.deepEqual(calc(p).total,{numerator:'170001',denominator:'4'});
|
||||
p.correction.reference='';assert.equal(calc(p).total,null);
|
||||
p.correction.apply='no';assert.deepEqual(calc(p).total,f(40000));assert.equal(calc(p).lastAmountApplied,false);
|
||||
});
|
||||
|
||||
test('excess documented PPC covers the PEC estimate without refund credit or duplicate annual deduction',()=>{
|
||||
for(const [paid,expected] of [['50000','0'],['60000','10000'],['50000.01','0.01']]){
|
||||
const p={...plan(),previousPpcPaid:paid};const m=calc(p);assert.deepEqual(m.total,f(0));assert.equal(m.pending.length,0);
|
||||
if(expected==='0')assert.equal(m.ppcExcess,null);else assert.ok(m.ppcExcess);
|
||||
assert.equal(m.paymentExecuted,false);assert.equal(m.annualDeductionCreated,false);
|
||||
}
|
||||
const {d,b}=combinedCase();b.pecObligation={...plan(),subjectNuit:b.subjectNuit,previousPpcPaid:'60000'};
|
||||
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,b,'2026-11-13T21:20:00Z'));
|
||||
const recovered=annualBook(d);assert.equal(annualCurrent(d,recovered),true);assert.deepEqual(recovered.result.model.arithmeticBalance,f(138));assert.match(annualHtml(d),/PEC — PPC acima da base limitada/);assert.match(annualHtml(d),/não cria reembolso/);
|
||||
});
|
||||
@@ -53,7 +53,6 @@ test('unknown turnover is distinct from zero; no turnover requires29(5),excess P
|
||||
{ previousTurnover: '' },
|
||||
{ previousTurnover: '0' },
|
||||
{ previousPpcPaid: '' },
|
||||
{ previousPpcPaid: '60000' },
|
||||
{ scope: 'other' },
|
||||
{ subjectNuit: '999999999' },
|
||||
{ previousYear: '2024' },
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import fs from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import {combinedCase} from '../tests/frontend/helpers/irpc-investment-combination-case.mjs';
|
||||
import {plan} from '../tests/frontend/helpers/irpc-pec-obligation-case.mjs';
|
||||
import {calculatePecObligation} from '../resources/js/lib/irpc-pec-obligation.ts';
|
||||
import {recordAnnual,saveAnnualBook,annualBook,annualCurrent,annualHtml} from '../resources/js/lib/irpc-annual-settlement.ts';
|
||||
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
|
||||
const out=new URL('../outputs/irpc-pec-last-amount-20261113/',import.meta.url);fs.mkdirSync(out,{recursive:true});
|
||||
for(const name of ['documented','missing','positive','excess']){
|
||||
if(process.argv[2]==='generate'){
|
||||
const {d,b}=combinedCase();const prior=recordAnnual(d,b,'2026-11-13T21:10:00Z');
|
||||
prior.pecObligation={...plan(),subjectNuit:b.subjectNuit,previousTurnover:['positive','excess'].includes(name)?'10000000':'0',previousPpcPaid:name==='excess'?'60000':'10000',lastAmount:{version:1,subjectNuit:b.subjectNuit,year:'2024',amount:'42500.25',reference:name==='missing'?'':'SYNTHETIC final PEC already determined;latest applicable annual MZN reference',confirmed:'yes'}};
|
||||
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,prior,'2026-11-13T21:11: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),m=calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment);
|
||||
assert.equal(annualCurrent(d,b),true);assert.equal(b.history.length,1);assert.deepEqual(b.result.model.arithmeticBalance,{numerator:'138',denominator:'1'});
|
||||
const expected=name==='missing'?null:name==='excess'?{numerator:'0',denominator:'1'}:name==='positive'?{numerator:'40000',denominator:'1'}:{numerator:'170001',denominator:'4'};assert.deepEqual(m.total,expected);
|
||||
if(name==='excess') {assert.deepEqual(m.ppcExcess,{numerator:'10000',denominator:'1'});}
|
||||
assert.equal(m.lastAmountApplied,name==='documented');assert.equal(m.paymentExecuted,false);assert.equal(m.annualDeductionCreated,false);
|
||||
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-11-13T21:12:00Z'});
|
||||
assert.match(html,/PEC — último montante documentado/);assert.match(JSON.stringify(report),/PEC — último montante documentado/);
|
||||
fs.writeFileSync(new URL(name+'-report.html',out),html);fs.writeFileSync(new URL(name+'-verification.json',out),JSON.stringify({synthetic:true,nativeApiRecovery:true,annualCurrent:true,historyPreserved:true,annualBalanceUnchanged:true,report:true,model:m},null,2));
|
||||
}
|
||||
}
|
||||
console.log(process.argv[2]==='generate'?'Generated four synthetic cases':'Verified four recovered cases and dossier 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-pec-last-amount-20261113/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/IrpcPecLastAmountDraftTest.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