Compare commits

...
Author SHA1 Message Date
nala 95b33275aa Integrate monthly PPC PEC cash projections across screen and dossier report
tests / ci (push) Waiting to run
2026-11-13 23:32:00 +02:00
9 changed files with 276 additions and 4 deletions
@@ -9891,3 +9891,16 @@ Entrega expressamente autorizada e instalada pelo executor Git existente na main
Backup/restauro fresco de193tabelas/413ficheiros e conferência pós-instalação de21tabelas protegidas sem diferenças. Mantidos instituições, dossiers, documentos, simulações, histórico, Educação, mapa, relatórios, pesquisa, suporte, permissões e12lotes/920456observações demográficas.23fontes conferidas por hash; revisão pública e HTTPS verificados. Executor/configuração inalterados, zero migrações/importações; readiness demográfica vinculada à revisão com original arquivado. CI obrigatório executado normalmente; provas anteriores reutilizadas sem nova ronda manual. Recibo: outputs/irpc-pec-last-amount-20261113/homologation-installed.json. Pacote agrupado v2 selado preservado; a versão anterior do ZIP conserva-se como histórico de normalização de finais de linha, sem alteração do produto.
Este lote deixa de ser pendência de instalação. L8 continua parcial para períodos/regimes especiais e casos fora do recorte; os resíduos L1–L7/restanteL8/L9–L16 e restantes módulos mantêm o saldo próprio. Próximo lote compatível: projecção conjunta PPC/PEC no ecrã e relatório, separada dos comprovativos pagos e do fecho anual.
## 13/11/2026 — lote integrado de projecção mensal PPC/PEC, QA
Continuidade do saldo de integração e relatórios AUD-SIM-INT02/INT06, após f059c226 instalado. Antes deste delta, as estimativas PPC e a hipótese PEC eram apresentadas separadamente; faltava a vista mensal conjunta. O lote reúne a apresentação no ecrã L8, a mesma projecção no relatório do dossier e a separação entre estimativas e comprovativos documentados como pagos. Reutiliza os calculadores aceites, o livro, o rascunho, a recuperação e os exportadores existentes. Não acrescenta um motor fiscal, formulário de pagamento, API, tabela ou permissão.
A vista agrega cada componente uma única vez por mês, com fracções exactas. A opção PEC concentrada conserva o mês escolhido; a repartida conserva os três montantes explícitos. Uma componente desconhecida ou uma distribuição por reconciliar produz total por determinar e subtotais conhecidos identificados; não vira zero. Zero documentado é distinguido da falta de dados. São previsões PPC/PEC do recorte ordinário2026, não o saldo a executar ou todos os impostos do negócio. Valores pagos, deduções, saldo anual, fluxo financeiro e métricas do investimento não são acrescidos ou substituídos.
O relatório usa as opções guardadas na versão calculada, preservando as escolhas posteriores ainda por recalcular. Fontes e explicação aparecem no resumo da projecção; as linhas mensais conservam os montantes sem repetir extensamente as mesmas citações. A identificação de dossier/oportunidade e os exportadores existentes mantêm-se. Legados sem estas preparações não recebem uma projecção presumida.
QA:42testes frontend afectados, incluindo sete novos de agregação exacta, sobreposição, ausência, distribuição decimal, zero, recuperação, relatório e apresentação. Os sete testes do delta foram reconferidos após o ajuste de apresentação; o ensaio da vista L8 confirma os grupos separados, além da tabela. Prova nativa de POST/PUT/GET/recuperação em quatro cenários com42asserções e rollback na base isolada investmoz_demography_test:55441. Cenários:112000MZN totais com64000emMaio, distribuição por seis meses, PEC por completar sem total presumido, e zero explícito sem saída. Saldo anual sintético138MZN e histórico anterior preservados; relatório financeiro do mesmo dossier conferido. Tipos, lint focado sem reformatação dos ficheiros legados, Pint/sintaxe e build QA aprovados. Edge:componente real compilado e relatório, estados completo/parcial/zero e apresentação final sem explicações repetidas. Prévia sintética sem API/BD; provas aceites dos restantes percursos reutilizadas. Provas:outputs/irpc-cashflow-group-20261113/.
Estado:implementado e verificado em QA, ainda sem instalação. Homologação mantém f059c226. Pacote único compatível; instalação depende de autorização concreta, com backup/restauro fresco e reversão de release preservando snapshots e edições posteriores. Este lote encerra a integração visual e documental destas previsões mensais; não encerra L8 nem o portal. Períodos/regimes especiais, anualização PPC28(3), outros casos documentais e resíduos L1–L7/restanteL8/L9–L16 e módulos conservam o saldo próprio. Execução de pagamentos não é requisito do portal.
File diff suppressed because one or more lines are too long
@@ -8,6 +8,8 @@ import { SimulationIrpcInternational } from './simulation-irpc-international';
import { SimulationIrpcPec } from './simulation-irpc-pec';
import { useState } from 'react';
import {
advanceCashflowAmount,
annualAdvanceCashflow,
annualBook,
newAnnualBook,
saveAnnualBook,
@@ -341,10 +343,14 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
})}
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>PPC efectivamente pagos</legend>
<legend>Estimativas PPC e PEC para a viabilidade</legend>
<p>Simulação fiscal para avaliar a viabilidade do negócio e as necessidades de tesouraria. Os valores e meses previstos são estimativas; o portal não executa pagamentos nem submete declarações fiscais.</p>
<SimulationIrpcPecObligation book={b} onChange={pecObligation=>patch({pecObligation})}/>
<SimulationIrpcPpcObligation book={b} onChange={ppcObligation=>patch({ppcObligation})}/>
<SimulationIrpcAdvanceCashflow book={b}/>
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>PPC documentados como pagos</legend>
{group('ppc', 'PPC efectuados')}
<p>
Regista comprovativos de pagamentos por conta; não
@@ -613,3 +619,23 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
);
}
}
export function SimulationIrpcAdvanceCashflow({book}: {book: AnnualBook}) {
if (!book.ppcObligation && !book.pecObligation) return null;
const m = annualAdvanceCashflow(book);
const show = advanceCashflowAmount;
return <section className="space-y-3 rounded border p-3" aria-label="Projecção conjunta PPC e PEC">
<h5 className="font-semibold">Saídas previstas PPC/PEC por mês</h5>
<p>Reúne as estimativas PPC e a hipótese de tesouraria PEC. Os comprovativos pagos e o fecho anual permanecem separados; esta vista não acrescenta outra saída ao fluxo financeiro do negócio.</p>
<p role="status">{m.total ? 'Total da projecção: '+show(m.total) : 'Projecção parcial — total por determinar.'}</p>
{!!m.pending.length && <ul className="list-disc pl-5">{m.pending.map(p=><li key={p}>{p}</li>)}</ul>}
{!!m.rows.length && <div className="overflow-x-auto"><table className="w-full text-left text-sm">
<caption className="sr-only">Projecção mensal das estimativas PPC e PEC</caption>
<thead><tr><th className="p-2">Mês</th><th className="p-2">PPC</th><th className="p-2">PEC</th><th className="p-2">{m.total ? 'Total previsto' : 'Subtotal conhecido'}</th></tr></thead>
<tbody>{m.rows.map(r=><tr key={r.month} className="border-t"><th scope="row" className="p-2">{r.month}</th><td className="p-2">{m.ppcKnown ? show(r.ppc) : 'Por determinar'}</td><td className="p-2">{m.pecKnown ? show(r.pec) : 'Por determinar'}</td><td className="p-2">{show(r.knownSubtotal)}</td></tr>)}</tbody>
</table></div>}
{!m.rows.length && m.total && <p>Sem saída PPC/PEC estimada neste recorte.</p>}
<p>Meses e montantes dependem das fontes e opções preparadas acima. A opção PEC concentrada conserva o mês escolhido; esta apresentação não certifica o prazo legal.</p>
</section>;
}
+64 -1
View File
@@ -1,7 +1,8 @@
import { calculatePecPlan } from './irpc-pec-plan.ts';
import {pecCorrectionVersion} from './irpc-pec-correction.ts';
import {pecPlanVersion} from './irpc-pec-plan.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 { calculatePpcObligation, validatePpcObligation, ppcObligationReport, ppcObligationVersion, type PpcObligation } from './irpc-ppc-obligation.ts';
import {
newInvestmentCredit,
validateInvestmentCredit,
@@ -1727,6 +1728,7 @@ export function annualReportRows(
return [
...(m.input.pecObligation ? pecObligationReport(m.input.pecObligation,m.subjectNuit,m.year,m.input.assessment) : []),
...(m.input.ppcObligation ? ppcObligationReport(m.input.ppcObligation,m.subjectNuit,m.year,m.input.assessment) : []),
...annualAdvanceCashflowReport(m.input),
[
'IRPC — L8',
'Fecho anual — âmbito e origem',
@@ -2408,3 +2410,64 @@ export function annualHtml(d: Record<string, unknown>) {
'</html>'
);
}
/** Derived cash view only. Reuses the existing estimates; never changes the annual ledger. */
export function annualAdvanceCashflow(b: Pick<AnnualBook, 'subjectNuit' | 'year' | 'assessment' | 'ppcObligation' | 'pecObligation'>) {
const pending: string[] = [];
const sources: string[] = [];
const amounts: Array<{month: string; kind: 'ppc' | 'pec'; amount: FiscalFraction}> = [];
let ppcKnown = false, pecKnown = false;
if (!b.ppcObligation) pending.push('Prepare a estimativa PPC para completar a projecção conjunta.');
else {
const p = calculatePpcObligation(b.ppcObligation, b.subjectNuit, b.year, b.assessment);
sources.push(b.ppcObligation.assessmentReference, b.ppcObligation.withholdingReference, p.source);
pending.push(...p.pending);
ppcKnown = p.pending.length === 0 && p.total !== null;
if (ppcKnown) amounts.push(...p.schedule.map(r => ({...r, kind: 'ppc' as const})));
}
if (!b.pecObligation) pending.push('Prepare a estimativa PEC para completar a projecção conjunta.');
else {
const p = calculatePecObligation(b.pecObligation, b.subjectNuit, b.year, b.assessment);
sources.push(b.pecObligation.turnoverReference, b.pecObligation.ppcReference, p.source);
pending.push(...p.pending);
if (!p.pending.length && p.total) {
if (BigInt(p.total.numerator) === 0n) pecKnown = true;
else if (!b.pecObligation.plan) pending.push('Distribua a hipótese de tesouraria PEC para completar os meses da projecção conjunta.');
else {
const allocation = calculatePecPlan(b.pecObligation.plan, p.total, p.notApplicable, b.year);
sources.push(b.pecObligation.plan.reference, allocation.source);
pending.push(...allocation.pending);
pecKnown = allocation.allocationReconciled;
if (pecKnown) amounts.push(...allocation.rows.map(r => ({...r, kind: 'pec' as const})));
}
}
}
const grouped = new Map<string, {month: string; ppc: FiscalFraction; pec: FiscalFraction}>();
for (const row of amounts) {
if (BigInt(row.amount.numerator) === 0n) continue;
const current = grouped.get(row.month) ?? {month: row.month, ppc: fraction(0n), pec: fraction(0n)};
current[row.kind] = addFractions([current[row.kind], row.amount]);
grouped.set(row.month, current);
}
const rows = [...grouped.values()].sort((a,b) => a.month.localeCompare(b.month)).map(r => ({...r, knownSubtotal: addFractions([r.ppc,r.pec])}));
const knownSubtotal = addFractions(rows.map(r => r.knownSubtotal));
return {version:'IRPC-advance-cashflow/1', ppcKnown, pecKnown, pending:[...new Set(pending)], rows,
total: ppcKnown && pecKnown ? knownSubtotal : null, knownSubtotal, sources:[...new Set(sources.filter(s => s.trim()))]};
}
export function advanceCashflowAmount(value: FiscalFraction): string {
return fractionPresentation(value).decimal+' MZN';
}
export function annualAdvanceCashflowReport(b: Pick<AnnualBook, 'subjectNuit' | 'year' | 'assessment' | 'ppcObligation' | 'pecObligation'>): Array<[string,string,string,string]> {
if (!b.ppcObligation && !b.pecObligation) return [];
const m = annualAdvanceCashflow(b);
const show = advanceCashflowAmount;
const source = m.sources.join('; ');
return [
['IRPC — L8', 'Projecção mensal conjunta PPC/PEC', (m.total ? 'Total da projecção: '+show(m.total)+'.' : 'Projecção parcial; total por determinar. '+m.pending.join(' '))+' Estimativas de tesouraria; comprovativos pagos, deduções e fecho anual separados. Esta vista não acrescenta outra saída ao fluxo financeiro.', source],
...m.rows.map(r => ['IRPC — L8', 'PPC/PEC — '+r.month,
'PPC: '+(m.ppcKnown ? show(r.ppc) : 'por determinar')+'; PEC: '+(m.pecKnown ? show(r.pec) : 'por determinar')+'; '+(m.total ? 'total previsto: ' : 'subtotal conhecido: ')+show(r.knownSubtotal)+'.', ''] as [string,string,string,string]),
];
}
@@ -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 IrpcAdvanceCashflowDraftTest 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 (['overlap' => 138, 'distributed' => 138, 'partial' => 138, 'zero' => 138] as $name => $balance) {
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-cashflow-group-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($book['ppcObligation'], $book['result']['model']['input']['ppcObligation']);
$this->assertSame('2026', $book['ppcObligation']['year']);
$this->assertSame('SYNTHETIC effective activity'.($name === 'start' ? '2026' : '2020'), $book['pecObligation']['activityReference']);
file_put_contents(base_path('outputs/irpc-cashflow-group-20261113/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,31 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import {compileFunction} from 'node:vm';
import ts from 'typescript';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import * as annual from '../../resources/js/lib/irpc-annual-settlement.ts';
import {combinedCase} from './helpers/irpc-investment-combination-case.mjs';
import {plan as ppc} from './helpers/irpc-ppc-obligation-case.mjs';
import {plan as pec} from './helpers/irpc-pec-obligation-case.mjs';
import {newPecPlan} from '../../resources/js/lib/irpc-pec-plan.ts';
const require=createRequire(import.meta.url),module={exports:{}};
const source=fs.readFileSync(new URL('../../resources/js/components/simulation-irpc-annual.tsx',import.meta.url),'utf8');
const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
compileFunction(code,['require','module','exports'])(name=>name==='@/lib/irpc-annual-settlement'?annual:name.startsWith('./')?new Proxy({},{get:()=>()=>null}):require(name),module,module.exports);
const Summary=module.exports.SimulationIrpcAdvanceCashflow;
const book=()=>({...combinedCase().b,ppcObligation:ppc(),pecObligation:{...pec(),plan:{...newPecPlan(),mode:'single',singleMonth:'2026-05',reference:'SYNTHETIC cash choice'}}});
const render=b=>renderToStaticMarkup(React.createElement(Summary,{book:b}));
test('joint view labels monthly contributions and distinguishes a partial subtotal from a complete forecast',()=>{
const b=book(),html=render(b);assert.match(html,/Saídas previstas PPC\/PEC por mês/);assert.match(html,/112000\.00/);assert.match(html,/64000\.00/);assert.match(html,/não acrescenta outra saída/);assert.match(html,/prazo legal/);
delete b.pecObligation;const partial=render(b);assert.match(partial,/Projecção parcial/);assert.match(partial,/Subtotal conhecido/);assert.match(partial,/Por determinar/);assert.ok(!partial.includes('Total da projecção:'));
});
test('explicit zero has a complete zero forecast and legacy absence does not create a forecast',()=>{
const b=book();b.ppcObligation.previousTax='0';b.ppcObligation.previousWithholdings='0';b.pecObligation.previousPpcPaid='60000';const html=render(b);assert.match(html,/Total da projecção: 0\.00 MZN/);assert.match(html,/Sem saída PPC\/PEC estimada/);
delete b.ppcObligation;delete b.pecObligation;assert.equal(render(b),'');const {d}=combinedCase(),prepared=book();d.modelo_a_snapshot=annual.saveAnnualBook(d,annual.recordAnnual(d,prepared,'2026-11-13T21:45:00Z'));
const full=renderToStaticMarkup(React.createElement(module.exports.SimulationIrpcAnnual,{draft:d,onChange:()=>{}}));
const estimation=full.indexOf('<legend>Estimativas PPC e PEC para a viabilidade</legend>'),paid=full.indexOf('<legend>PPC documentados como pagos</legend>');
assert.ok(estimation>=0 && paid>estimation);const between=full.slice(estimation,paid);assert.match(between,/Saídas previstas PPC\/PEC por mês/);assert.match(between,/<\/fieldset>/);
});
@@ -0,0 +1,35 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {plan as ppcPlan} from './helpers/irpc-ppc-obligation-case.mjs';
import {plan as pecPlan} from './helpers/irpc-pec-obligation-case.mjs';
import {newPecPlan} from '../../resources/js/lib/irpc-pec-plan.ts';
import {combinedCase} from './helpers/irpc-investment-combination-case.mjs';
import {annualAdvanceCashflow,annualAdvanceCashflowReport,annualHtml,recordAnnual,saveAnnualBook,annualBook,annualCurrent} from '../../resources/js/lib/irpc-annual-settlement.ts';
const f=n=>({numerator:String(n),denominator:'1'});
const book=()=>({...combinedCase().b,ppcObligation:ppcPlan(),pecObligation:{...pecPlan(),plan:{...newPecPlan(),mode:'single',singleMonth:'2026-05',reference:'SYNTHETIC cash hypothesis'}}});
test('joint cash projection groups the same month once using existing exact PPC and PEC estimates',()=>{
const b=book(),before=JSON.stringify(b),m=annualAdvanceCashflow(b);
assert.equal(m.pending.length,0);assert.deepEqual(m.total,f(112000));assert.equal(m.rows.length,3);assert.deepEqual(m.rows[0],{month:'2026-05',ppc:f(24000),pec:f(40000),knownSubtotal:f(64000)});
assert.equal(JSON.stringify(b),before);
const report=annualAdvanceCashflowReport(b);assert.ok(report[0][3].includes('SYNTHETIC'));assert.ok(report.slice(1).every(r=>r[3]===''));
});
test('missing or unreconciled component stays unknown; known rows never become a complete total',()=>{
const b=book();delete b.pecObligation;let m=annualAdvanceCashflow(b);assert.equal(m.total,null);assert.equal(m.pecKnown,false);assert.deepEqual(m.knownSubtotal,f(72000));
const c=book();c.pecObligation.plan={...c.pecObligation.plan,mode:'three',june:'1',august:'1',october:'1'};m=annualAdvanceCashflow(c);assert.equal(m.total,null);assert.equal(m.pecKnown,false);assert.ok(m.pending.length);assert.equal(m.rows.length,3);
delete c.ppcObligation;m=annualAdvanceCashflow(c);assert.equal(m.total,null);assert.equal(m.rows.length,0);
});
test('confirmed zero PEC and zero PPC differ from unavailable estimates and preserve deliberate dates',()=>{
const b=book();b.pecObligation.previousPpcPaid='60000';let m=annualAdvanceCashflow(b);assert.equal(m.pecKnown,true);assert.deepEqual(m.total,f(72000));
b.ppcObligation.previousTax='0';b.ppcObligation.previousWithholdings='0';m=annualAdvanceCashflow(b);assert.deepEqual(m.total,f(0));assert.equal(m.ppcKnown,true);assert.equal(m.rows.length,0);
const c=book();c.pecObligation.plan.singleMonth='2026-04';m=annualAdvanceCashflow(c);assert.equal(m.rows[0].month,'2026-04');assert.deepEqual(m.rows[0].pec,f(40000));
});
test('monthly decimal allocation retains source precision and sums only the accepted cash hypotheses',()=>{
const b=book();b.pecObligation.plan={...newPecPlan(),mode:'three',june:'10000.01',august:'14999.99',october:'15000',reference:'SYNTHETIC three explicit amounts'};
const m=annualAdvanceCashflow(b);assert.deepEqual(m.total,f(112000));assert.equal(m.rows.length,6);assert.deepEqual(m.rows.find(r=>r.month==='2026-06').pec,{numerator:'1000001',denominator:'100'});
});
test('recovery and frozen report use the same cash projection without adding to the annual balance or rewriting history',()=>{
const {d}=combinedCase(),b=book();const prior=recordAnnual(d,b,'2026-11-13T21:30:00Z');d.modelo_a_snapshot=saveAnnualBook(d,prior);const recovered=annualBook(d);
assert.equal(annualCurrent(d,recovered),true);assert.deepEqual(recovered.result.model.arithmeticBalance,f(138));assert.match(annualHtml(d),/Projecção mensal conjunta PPC\/PEC/);assert.match(annualHtml(d),/64000\.00/);
const changed={...recovered,pecObligation:{...recovered.pecObligation,plan:{...recovered.pecObligation.plan,singleMonth:'2026-06'}}};assert.equal(annualCurrent(d,changed),false);
assert.deepEqual(annualAdvanceCashflow(changed).total,f(112000));assert.match(annualHtml(d),/PPC\/PEC — 2026-05/);assert.equal(annualAdvanceCashflowReport({...b,ppcObligation:undefined,pecObligation:undefined}).length,0);
});
+31
View File
@@ -0,0 +1,31 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import {combinedCase} from '../tests/frontend/helpers/irpc-investment-combination-case.mjs';
import {plan as ppc} from '../tests/frontend/helpers/irpc-ppc-obligation-case.mjs';
import {plan as pec} from '../tests/frontend/helpers/irpc-pec-obligation-case.mjs';
import {newPecPlan} from '../resources/js/lib/irpc-pec-plan.ts';
import {annualAdvanceCashflow,annualBook,annualCurrent,annualHtml,recordAnnual,saveAnnualBook} from '../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
const out=new URL('../outputs/irpc-cashflow-group-20261113/',import.meta.url);fs.mkdirSync(out,{recursive:true});
for(const name of ['overlap','distributed','partial','zero']){
if(process.argv[2]==='generate'){
const {d,b}=combinedCase();const prior=recordAnnual(d,b,'2026-11-13T21:40:00Z');
prior.ppcObligation={...ppc(),subjectNuit:b.subjectNuit};
prior.pecObligation={...pec(),subjectNuit:b.subjectNuit,plan:{...newPecPlan(),mode:'single',singleMonth:'2026-05',reference:'SYNTHETIC cash hypothesis'}};
if(name==='distributed')prior.pecObligation.plan={...newPecPlan(),mode:'three',june:'10000.01',august:'14999.99',october:'15000',reference:'SYNTHETIC explicit allocation'};
if(name==='partial')delete prior.pecObligation.plan;
if(name==='zero'){prior.ppcObligation.previousTax='0';prior.ppcObligation.previousWithholdings='0';prior.pecObligation.previousPpcPaid='60000';}
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,prior,'2026-11-13T21:41: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),before=JSON.stringify(b),m=annualAdvanceCashflow(b);
assert.equal(annualCurrent(d,b),true);assert.equal(b.history.length,1);assert.deepEqual(b.result.model.arithmeticBalance,{numerator:'138',denominator:'1'});assert.equal(JSON.stringify(b),before);
assert.deepEqual(m.total,name==='partial'?null:{numerator:name==='zero'?'0':'112000',denominator:'1'});
if(name==='overlap')assert.deepEqual(m.rows[0].knownSubtotal,{numerator:'64000',denominator:'1'});
if(name==='partial')assert.equal(m.pecKnown,false);
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-11-13T21:42:00Z'});
assert.match(html,/Projecção mensal conjunta PPC\/PEC/);assert.match(JSON.stringify(report),/Projecção mensal conjunta PPC\/PEC/);
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 cash projection cases':'Verified four recovered cash projections 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-cashflow-group-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/IrpcAdvanceCashflowDraftTest.php'];
$_SERVER['argv'] = $argv;
$_SERVER['argc'] = count($argv);
require $root.'/vendor/autoload.php';
require $root.'/vendor/phpunit/phpunit/phpunit';