Group documented prior PEC basis and excess PPC reconciliation for viability
tests / ci (push) Waiting to run

This commit is contained in:
2026-11-13 23:14:22 +02:00
parent d9f088587b
commit f059c226d2
10 changed files with 526 additions and 44 deletions
@@ -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
+2 -1
View File
@@ -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
+203 -32
View File
@@ -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' },
+26
View File
@@ -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');
+23
View File
@@ -0,0 +1,23 @@
<?php
$root = dirname(__DIR__);
if (($argv[1] ?? '') !== 'test' || basename($root) !== 'portal-investidor') {
throw new RuntimeException('Explicit isolated QA test operation required');
}
foreach (['APP_ENV' => 'testing', 'APP_KEY' => 'base64:'.base64_encode(str_repeat('Q', 32)),
'APP_CONFIG_CACHE' => $root.'/outputs/irpc-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';