Keep PPC PEC estimates and reports consistent with the prepared L6 period

This commit is contained in:
2026-11-14 00:23:46 +02:00
parent e9278ee446
commit 078f770dea
14 changed files with 295 additions and 21 deletions
@@ -9913,3 +9913,16 @@ Instalação expressamente autorizada, concluída pelo executor Git existente na
Backup/restauro fresco de193tabelas/414ficheiros,29fontes conferidas por hash e21tabelas protegidas sem diferenças. Mantidos instituições, dossiers, documentos, simulações, Educação, mapa, relatórios, pesquisa, suporte, permissões e12lotes/920456observações demográficas. Zero migrações/importações; executor e configuração intactos. Readiness demográfica vinculada à revisão com original arquivado. HTTPS e revisão pública conferidos; CI obrigatório executado normalmente. Provas aceites reutilizadas sem nova ronda manual de percursos. Recibo:outputs/irpc-cashflow-group-20261113/homologation-installed.json. Pacote agrupado selado inalterado.
Este lote deixa de ser pendência de instalação. Não encerra o simulador ou o portal: períodos/regimes especiais, anualização28(3), casos documentais e resíduos L1–L7/restanteL8/L9–L16 e módulos mantêm o saldo próprio. Próximo lote compatível: coerência do período L6 nas estimativas, projecção e relatórios, preservando a preparação e o fecho anual independente.
## 14/11/2026 — lote de coerência do período L6 em PPC/PEC, QA
Continuidade do saldo AUD-SIM-INT02/INT06 após95b33275 instalado. Conferência delimitada do código: PPC/PEC ordinários e a projecção recebiam titular/exercício/tipo, sem confrontar as datas L6 conhecidas. Podiam apresentar meses civis num dossier com período curto ou não civil. O lote liga o contexto já existente aos dois estimadores, à vista mensal e ao relatório da mesma versão, sem duplicar o motor fiscal, criar API, tabela ou permissão.
As datas preparadas em L6 prevalecem para a conferência da preparação, mesmo quando ainda diferem do último cálculo L6; não são substituídas silenciosamente pelo resultado anterior. O período usado fica conservado no modelo da versão anual calculada. O relatório lê esse exemplar, preservando edições posteriores ainda por recalcular. Uma divergência face ao recorte civil2026 mantém as estimativas dependentes por reconciliar, sem calendário ou total presumidos. As entradas e as hipóteses de caixa ficam guardadas, sem alterar o cálculo anual independente.
A dispensa PEC do ano de início documentado mantém zero nos períodos válidos contidos em2026, incluindo o período curto de início; datas inválidas, outro exercício ou período não civil fora deste recorte não recebem dispensa presumida. Os avisos identificam só a componente afectada; se PPC e PEC estiverem ambos por reconciliar, o resumo reúne a causa comum. A chave de cálculo recebe um marcador apenas para os casos afectados pela nova conferência, sem invalidar globalmente os livros ordinários aceites. Livros e históricos antigos conservados. A preparação independente antiga, sem metadados L6 disponíveis, mantém o seu âmbito documental explícito.
QA iniciada em13/11 e fechada em14/11:61testes frontend afectados, incluindo oito novos de período/edição/recuperação/consumidores. Prova nativa POST/PUT/GET com42asserções, quatro cenários e rollback na base isolada investmoz_demography_test:55441. Civil completo mantém112000MZN de projecção; não civil e curto ficam por reconciliar; início preserva PECzero e PPC separado. Nos quatro cenários, o resultado anual independente coincide com o exemplar anterior sem as preparações e o histórico mantém-se. Originais L6 e datas são produzidos pelo percurso normal existente, sem fabricar um calendário oficial. Relatório financeiro do mesmo dossier conferido. Tipos, lint/formatação focados, Pint/sintaxe PHP e build QA aprovados. Edge:componentes reais compilados, quatro contextos, dispensa e aviso específico PPC, ecrã/relatório concordantes. Prévia sintética sem API ou base de dados. Provas aceites dos percursos não afectados reutilizadas; dependências financeiras e exportadores conferidos idênticos a95b33275. Provas:outputs/irpc-period-consistency-20261113/.
Estado:implementado e verificado em QA, sem instalação. Homologação mantém95b33275. Lote único para estimadores, projecção, recuperação e relatório. Encerra a lacuna de coerência do período conhecido; não implementa ainda os cálculos/calendários dos períodos ou regimes especiais nem a anualização PPC28(3). L8 e portal permanecem parciais; resíduos L1–L7/restanteL8/L9–L16 e módulos mantidos. Sem pagamentos, novas deduções, migrações, importações ou permissões. Instalação exige autorização concreta e o executor Git existente, com backup/restauro fresco e reversão de release preservando snapshots/edições posteriores.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
import type { AdvanceFiscalPeriod } from '@/lib/irpc-ppc-obligation';
import {SimulationIrpcPecObligation} from './simulation-irpc-pec-obligation';
import { SimulationIrpcPpcObligation } from './simulation-irpc-ppc-obligation';
import { SimulationIrpcInvestment } from './simulation-irpc-investment';
@@ -10,6 +11,7 @@ import { useState } from 'react';
import {
advanceCashflowAmount,
annualAdvanceCashflow,
annualAdvancePeriod,
annualBook,
newAnnualBook,
saveAnnualBook,
@@ -79,6 +81,7 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
);
const patch = (p: Partial<AnnualBook>) => save({ ...b, ...p }),
a = annualAssessment(draft),
advancePeriod = annualAdvancePeriod(draft),
options = annualWithholdingOptions(draft);
const field = (
key: 'subjectNuit' | 'year' | 'reference' | 'assessmentReference',
@@ -345,9 +348,9 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
<fieldset className="space-y-2 rounded border p-3">
<legend>Estimativas PPC e PEC para a viabilidade</legend>
<p>Simulação fiscal para avaliar a viabilidade do negócio e as necessidades de tesouraria. Os valores e meses previstos são estimativas; o portal não executa pagamentos nem submete declarações fiscais.</p>
<SimulationIrpcPecObligation book={b} onChange={pecObligation=>patch({pecObligation})}/>
<SimulationIrpcPpcObligation book={b} onChange={ppcObligation=>patch({ppcObligation})}/>
<SimulationIrpcAdvanceCashflow book={b}/>
<SimulationIrpcPecObligation book={b} period={advancePeriod} onChange={pecObligation=>patch({pecObligation})}/>
<SimulationIrpcPpcObligation book={b} period={advancePeriod} onChange={ppcObligation=>patch({ppcObligation})}/>
<SimulationIrpcAdvanceCashflow book={b} period={advancePeriod}/>
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>PPC documentados como pagos</legend>
@@ -621,12 +624,13 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
}
export function SimulationIrpcAdvanceCashflow({book}: {book: AnnualBook}) {
export function SimulationIrpcAdvanceCashflow({book,period}: {book: AnnualBook;period?: AdvanceFiscalPeriod}) {
if (!book.ppcObligation && !book.pecObligation) return null;
const m = annualAdvanceCashflow(book);
const m = annualAdvanceCashflow(book,period);
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>
{period && <p>Período preparado em L6: {period.from || 'por confirmar'} — {period.to || 'por confirmar'}.</p>}
<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>}
@@ -1,3 +1,4 @@
import type { AdvanceFiscalPeriod } from '@/lib/irpc-ppc-obligation';
import { SimulationIrpcPecCorrection } from './simulation-irpc-pec-correction';
import { SimulationIrpcPecPlan } from './simulation-irpc-pec-plan';
import {
@@ -9,9 +10,11 @@ import { fractionPresentation } from '@/lib/fiscal-rational';
import type { AnnualBook } from '@/lib/irpc-annual-settlement';
export function SimulationIrpcPecObligation({
book,
period,
onChange,
}: {
book: AnnualBook;
period?: AdvanceFiscalPeriod;
onChange: (p: PecObligation) => void;
}) {
const p = book.pecObligation;
@@ -38,6 +41,7 @@ export function SimulationIrpcPecObligation({
book.subjectNuit,
book.year,
book.assessment,
period,
),
patch = (changes: Partial<PecObligation>) =>
onChange({ ...p, ...changes });
@@ -1,3 +1,4 @@
import type { AdvanceFiscalPeriod } from '@/lib/irpc-ppc-obligation';
import {
newPpcObligation,
calculatePpcObligation,
@@ -8,9 +9,11 @@ import { fractionPresentation } from '@/lib/fiscal-rational';
import type { AnnualBook } from '@/lib/irpc-annual-settlement';
export function SimulationIrpcPpcObligation({
book,
period,
onChange,
}: {
book: AnnualBook;
period?: AdvanceFiscalPeriod;
onChange: (value: PpcObligation) => void;
}) {
const p = book.ppcObligation;
@@ -37,6 +40,7 @@ export function SimulationIrpcPpcObligation({
book.subjectNuit,
book.year,
book.assessment,
period,
);
const patch = (changes: Partial<PpcObligation>) =>
onChange({ ...p, ...changes });
+26 -10
View File
@@ -2,7 +2,7 @@ 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 { calculatePpcObligation, validatePpcObligation, ppcObligationReport, ppcObligationVersion, type PpcObligation } from './irpc-ppc-obligation.ts';
import { ordinaryAdvancePeriodIssues, type AdvanceFiscalPeriod, calculatePpcObligation, validatePpcObligation, ppcObligationReport, ppcObligationVersion, type PpcObligation } from './irpc-ppc-obligation.ts';
import {
newInvestmentCredit,
validateInvestmentCredit,
@@ -422,6 +422,11 @@ export function annualAssessment(d: Record<string, unknown>) {
: zero(),
};
}
export function annualAdvancePeriod(d: Record<string, unknown>): AdvanceFiscalPeriod {
const prepared = normalBook(d).periodsFx;
return prepared ? {from: prepared.from, to: prepared.to} : annualAssessment(d).period;
}
export function reuseAnnualAssessment(
d: Record<string, unknown>,
b: AnnualBook,
@@ -569,8 +574,12 @@ function dependencies(d: Record<string, unknown>, includeInvestment = false) {
}
function key(d: Record<string, unknown>, b: AnnualBook) {
const { result: _, history: __, ...input } = b;
const period = annualAdvancePeriod(d);
const periodGuard = ordinaryAdvancePeriodIssues(period, b.year).length > 0 &&
(b.ppcObligation || b.pecObligation && !calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,period).notApplicable);
return closureFingerprint([
annualSettlementVersion,
...(periodGuard ? ['IRPC-advance-period-consistency/1'] : []),
acquisitionScope(d),
input,
dependencies(d, !!b.investmentCredit),
@@ -1594,6 +1603,7 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
return {
version: annualSettlementVersion,
input: structuredClone(input),
...(b.ppcObligation || b.pecObligation ? {advancePeriod: annualAdvancePeriod(d)} : {}),
subjectNuit: b.subjectNuit,
year: b.year,
assessment: assessment?.record
@@ -1726,9 +1736,9 @@ export function annualReportRows(
indeterminate: 'Indeterminado',
}[m.status];
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),
...(m.input.pecObligation ? pecObligationReport(m.input.pecObligation,m.subjectNuit,m.year,m.input.assessment,m.advancePeriod ?? m.assessment?.period) : []),
...(m.input.ppcObligation ? ppcObligationReport(m.input.ppcObligation,m.subjectNuit,m.year,m.input.assessment,m.advancePeriod ?? m.assessment?.period) : []),
...annualAdvanceCashflowReport(m.input,m.advancePeriod ?? m.assessment?.period),
[
'IRPC — L8',
'Fecho anual — âmbito e origem',
@@ -2413,14 +2423,14 @@ export function annualHtml(d: Record<string, unknown>) {
/** Derived cash view only. Reuses the existing estimates; never changes the annual ledger. */
export function annualAdvanceCashflow(b: Pick<AnnualBook, 'subjectNuit' | 'year' | 'assessment' | 'ppcObligation' | 'pecObligation'>) {
export function annualAdvanceCashflow(b: Pick<AnnualBook, 'subjectNuit' | 'year' | 'assessment' | 'ppcObligation' | 'pecObligation'>, period?: AdvanceFiscalPeriod) {
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);
const p = calculatePpcObligation(b.ppcObligation, b.subjectNuit, b.year, b.assessment, period);
sources.push(b.ppcObligation.assessmentReference, b.ppcObligation.withholdingReference, p.source);
pending.push(...p.pending);
ppcKnown = p.pending.length === 0 && p.total !== null;
@@ -2428,7 +2438,7 @@ export function annualAdvanceCashflow(b: Pick<AnnualBook, 'subjectNuit' | 'year'
}
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);
const p = calculatePecObligation(b.pecObligation, b.subjectNuit, b.year, b.assessment, period);
sources.push(b.pecObligation.turnoverReference, b.pecObligation.ppcReference, p.source);
pending.push(...p.pending);
if (!p.pending.length && p.total) {
@@ -2452,7 +2462,13 @@ export function annualAdvanceCashflow(b: Pick<AnnualBook, 'subjectNuit' | 'year'
}
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,
let issues = [...new Set(pending)];
const periodIssues = ['PPC','PEC'].map(kind => ordinaryAdvancePeriodIssues(period,b.year,kind as 'PPC' | 'PEC')[0]);
if (periodIssues.every(issue => issue && issues.includes(issue))) {
issues = issues.filter(issue => !periodIssues.includes(issue));
issues.push(...ordinaryAdvancePeriodIssues(period,b.year));
}
return {version:'IRPC-advance-cashflow/1', ppcKnown, pecKnown, pending:issues, rows,
total: ppcKnown && pecKnown ? knownSubtotal : null, knownSubtotal, sources:[...new Set(sources.filter(s => s.trim()))]};
}
@@ -2460,9 +2476,9 @@ 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]> {
export function annualAdvanceCashflowReport(b: Pick<AnnualBook, 'subjectNuit' | 'year' | 'assessment' | 'ppcObligation' | 'pecObligation'>, period?: AdvanceFiscalPeriod): Array<[string,string,string,string]> {
if (!b.ppcObligation && !b.pecObligation) return [];
const m = annualAdvanceCashflow(b);
const m = annualAdvanceCashflow(b, period);
const show = advanceCashflowAmount;
const source = m.sources.join('; ');
return [
+29 -3
View File
@@ -1,3 +1,7 @@
import {
ordinaryAdvancePeriodIssues,
type AdvanceFiscalPeriod,
} from './irpc-ppc-obligation.ts';
import {
validatePecCorrection,
correctedPecTurnover,
@@ -102,6 +106,7 @@ export function calculatePecObligation(
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
) {
validatePecObligation(p);
const pending: string[] = [];
@@ -138,9 +143,28 @@ export function calculatePecObligation(
'PEC: falta ano de início efectivo da actividade e suporte;constituição da empresa não substitui início.',
);
if (!pending.length && p.activityStartYear === '2026') {
notApplicable = true;
total = fraction(0n);
const validStartDate = (value?: string | null) =>
Boolean(
value &&
/^2026-\d{2}-\d{2}$/.test(value) &&
Number.isFinite(Date.parse(value)) &&
new Date(value).toISOString().slice(0, 10) === value,
);
if (
period &&
!(
validStartDate(period.from) &&
validStartDate(period.to) &&
period.from! <= period.to!
)
)
pending.push(...ordinaryAdvancePeriodIssues(period, year, 'PEC'));
if (!pending.length) {
notApplicable = true;
total = fraction(0n);
}
} else if (!pending.length) {
pending.push(...ordinaryAdvancePeriodIssues(period, year, 'PEC'));
pending.push(...turnover.pending);
if (
p.previousYear !== '2025' ||
@@ -251,8 +275,9 @@ export function pecObligationReport(
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
): Array<[string, string, string, string]> {
const m = calculatePecObligation(p, subjectNuit, year, assessment),
const m = calculatePecObligation(p, subjectNuit, year, assessment, period),
show = (v: FiscalFraction | null) =>
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
const originalEstimate = p.correction
@@ -261,6 +286,7 @@ export function pecObligationReport(
subjectNuit,
year,
assessment,
period,
).total
: null;
const correctionEffect =
+28 -1
View File
@@ -6,6 +6,30 @@ import {
type FiscalFraction,
} from './fiscal-rational.ts';
export type AdvanceFiscalPeriod = {
from?: string | null;
to?: string | null;
} | null;
export function ordinaryAdvancePeriodIssues(
period: AdvanceFiscalPeriod | undefined,
year: string,
component: 'PPC' | 'PEC' | 'PPC/PEC' = 'PPC/PEC',
): string[] {
if (!period) return [];
return period.from === year + '-01-01' && period.to === year + '-12-31'
? []
: [
component +
': o período L6 ' +
(period.from || 'por confirmar') +
' — ' +
(period.to || 'por confirmar') +
' não corresponde ao ano civil ' +
year +
'. Reconciliar o enquadramento antes de usar as estimativas ordinárias; a preparação fica conservada.',
];
}
export const ppcObligationVersion = 'IRPC-PPC-obligation/1';
export const ppcObligationSource =
'RIRPC, Decreto 9/2008, arts.27(1)(a) e28(1–3), BR16/I,16-04-2008,exemplar preservado SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee: imposto liquidado no exercício anterior nos termos21(1), líquido das retenções na fonte;80%, dividido em três montantes iguais arredondados por excesso ao milhar de meticais. Maio,Julho,Setembro. Recorte2026,residente geral,actividade comercial/industrial/agrícola,ano civil e exercício anterior completo2025,MZN. Períodos especiais,anualização28(3),dispensas,juros e guias separados.';
@@ -57,6 +81,7 @@ export function calculatePpcObligation(
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
) {
validatePpcObligation(p);
const pending: string[] = [];
@@ -88,6 +113,7 @@ export function calculatePpcObligation(
pending.push(
'PPC: montantes anteriores em MZN desconhecidos ou inválidos; zero deve ser explícito.',
);
pending.push(...ordinaryAdvancePeriodIssues(period, year, 'PPC'));
if (!pending.length) {
base = subtractFractions(
decimalFraction(p.previousTax),
@@ -135,8 +161,9 @@ export function ppcObligationReport(
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
): Array<[string, string, string, string]> {
const m = calculatePpcObligation(p, subjectNuit, year, assessment),
const m = calculatePpcObligation(p, subjectNuit, year, assessment, period),
show = (v: FiscalFraction | null) =>
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
return [
@@ -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 IrpcAdvancePeriodDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_period_survives_native_recovery_without_changing_the_independent_annual_result(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('investmoz_demography_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$actor = User::factory()->create();
$this->actingAs($actor);
$id = null;
$etag = null;
foreach (['ordinary', 'noncivil', 'short', 'start'] as $name) {
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-period-consistency-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->assertArrayHasKey('advancePeriod', $book['result']['model']);
$this->assertSame($book['history'][0]['model']['arithmeticBalance'], $book['result']['model']['arithmeticBalance']);
$this->assertSame($book['pecObligation'], $book['result']['model']['input']['pecObligation']);
$this->assertSame($book['ppcObligation'], $book['result']['model']['input']['ppcObligation']);
$this->assertSame('2026', $book['ppcObligation']['year']);
$this->assertSame('SYNTHETIC effective activity'.($name === 'start' ? '2026' : '2020'), $book['pecObligation']['activityReference']);
file_put_contents(base_path('outputs/irpc-period-consistency-20261113/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,23 @@
import {combinedCase} from './irpc-investment-combination-case.mjs';
import {plan as ppc} from './irpc-ppc-obligation-case.mjs';
import {plan as pec} from './irpc-pec-obligation-case.mjs';
import {newPecPlan} from '../../../resources/js/lib/irpc-pec-plan.ts';
import {normalBook,recordNormal,saveNormalBook} from '../../../resources/js/lib/irpc-normal.ts';
import {newNormalPeriodFx} from '../../../resources/js/lib/irpc-normal-period-fx.ts';
import {annualAssessment,reuseAnnualAssessment} from '../../../resources/js/lib/irpc-annual-settlement.ts';
export function periodAdvanceCase(kind='ordinary'){
const c=combinedCase();
if(kind!=='ordinary'){
const n=normalBook(c.d);
const dates=kind==='noncivil'?['annual','2025-07-01','2026-06-30','2025-07-01']:kind==='start'?['start','2026-07-01','2026-12-31','2026-01-01']:['cessation','2026-01-01','2026-06-30','2026-01-01'];
n.periodsFx={...newNormalPeriodFx(),kind:dates[0],from:dates[1],to:dates[2],planStart:dates[3],reference:'SYNTHETIC documented fiscal period '+kind,mznMapsReference:'SYNTHETIC existing MZN mapping; no real tax filing'};
c.d.modelo_a_snapshot=saveNormalBook(c.d,recordNormal(c.d,n,'2026-11-13T22:00:00Z'));
c.b=reuseAnnualAssessment(c.d,c.b);
}
c.b.ppcObligation={...ppc(),subjectNuit:c.b.subjectNuit};
c.b.pecObligation={...pec(),subjectNuit:c.b.subjectNuit,plan:{...newPecPlan(),mode:'single',singleMonth:'2026-05',reference:'SYNTHETIC cash hypothesis'}};
if(kind==='start'){
c.b.pecObligation.activityStartYear='2026';c.b.pecObligation.activityReference='SYNTHETIC effective activity2026';c.b.ppcObligation.previousTax='0';c.b.ppcObligation.previousWithholdings='0';
}
return {...c,period:annualAssessment(c.d).period};
}
@@ -0,0 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import {compileFunction} from 'node:vm';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import ts from 'typescript';
import * as annual from '../../resources/js/lib/irpc-annual-settlement.ts';
import * as pec from '../../resources/js/lib/irpc-pec-obligation.ts';
import * as rational from '../../resources/js/lib/fiscal-rational.ts';
import {periodAdvanceCase} from './helpers/irpc-advance-period-case.mjs';
const require=createRequire(import.meta.url);
function load(name,resolver){const module={exports:{}};const s=fs.readFileSync(new URL('../../resources/js/components/'+name,import.meta.url),'utf8');const code=ts.transpileModule(s,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;compileFunction(code,['require','module','exports'])(resolver,module,module.exports);return module.exports;}
test('L8 supplies the actual prepared L6 period to both estimators and the joint view',()=>{
const seen={};const components=load('simulation-irpc-annual.tsx',name=>name==='@/lib/irpc-annual-settlement'?annual:name==='./simulation-irpc-ppc-obligation'?{SimulationIrpcPpcObligation:p=>(seen.ppc=p.period,null)}:name==='./simulation-irpc-pec-obligation'?{SimulationIrpcPecObligation:p=>(seen.pec=p.period,null)}:name.startsWith('./')?new Proxy({},{get:()=>()=>null}):require(name));
const {d,b,period}=periodAdvanceCase('noncivil');d.modelo_a_snapshot=annual.saveAnnualBook(d,annual.recordAnnual(d,b,'2026-11-13T22:04:00Z'));const html=renderToStaticMarkup(React.createElement(components.SimulationIrpcAnnual,{draft:d,onChange:()=>{}}));
assert.deepEqual(seen.ppc,period);assert.deepEqual(seen.pec,period);assert.match(html,/Período preparado em L6/);assert.match(html,/2025-07-01/);assert.match(html,/Projecção parcial/);assert.ok(!html.includes('Total da projecção: 112000'));
});
test('the existing PEC start-year exclusion remains visible with a short source period',()=>{
const components=load('simulation-irpc-pec-obligation.tsx',name=>name==='@/lib/irpc-pec-obligation'?pec:name==='@/lib/fiscal-rational'?rational:name.startsWith('./')?new Proxy({},{get:()=>()=>null}):require(name));
const {b,period}=periodAdvanceCase('start');const html=renderToStaticMarkup(React.createElement(components.SimulationIrpcPecObligation,{book:b,period,onChange:()=>{}}));assert.match(html,/Não aplicável no ano de início/);assert.ok(!html.includes('não corresponde ao ano civil'));
});
@@ -0,0 +1,39 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {periodAdvanceCase} from './helpers/irpc-advance-period-case.mjs';
import {normalBook,saveNormalBook} from '../../resources/js/lib/irpc-normal.ts';
import {newNormalPeriodFx} from '../../resources/js/lib/irpc-normal-period-fx.ts';
import {calculatePpcObligation} from '../../resources/js/lib/irpc-ppc-obligation.ts';
import {calculatePecObligation} from '../../resources/js/lib/irpc-pec-obligation.ts';
import {annualAdvancePeriod,annualAdvanceCashflow,annualAdvanceCashflowReport,recordAnnual,saveAnnualBook,annualBook,annualCurrent,annualHtml} from '../../resources/js/lib/irpc-annual-settlement.ts';
const f=n=>({numerator:String(n),denominator:'1'});
test('ordinary calendar estimates and legacy independent preparation retain accepted amounts',()=>{
const {b,period}=periodAdvanceCase();assert.deepEqual(annualAdvanceCashflow(b,period).total,f(112000));assert.deepEqual(annualAdvanceCashflow(b).total,f(112000));
});
test('actual L6 noncivil and short periods cannot produce ordinary PPC PEC dates or a complete cash total',()=>{
for(const kind of ['noncivil','short']){
const {b,period}=periodAdvanceCase(kind),before=JSON.stringify(b);assert.ok(period);assert.ok(period.to!=='2026-12-31');
const ppc=calculatePpcObligation(b.ppcObligation,b.subjectNuit,b.year,b.assessment,period),pec=calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,period),joint=annualAdvanceCashflow(b,period);
assert.equal(ppc.total,null);assert.equal(pec.total,null);assert.equal(joint.total,null);assert.deepEqual(joint.rows,[]);assert.match(joint.pending.join(' '),/período L6/);assert.equal(JSON.stringify(b),before);
assert.ok(!JSON.stringify(annualAdvanceCashflowReport(b,period)).includes('112000'));
}
});
test('documented PEC activity-start exclusion survives a short L6 period while unsupported PPC remains separate',()=>{
const {b,period}=periodAdvanceCase('start');const pec=calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,period);assert.equal(pec.notApplicable,true);assert.deepEqual(pec.total,f(0));
const joint=annualAdvanceCashflow(b,period);assert.equal(joint.pecKnown,true);assert.equal(joint.ppcKnown,false);assert.equal(joint.total,null);assert.ok(joint.pending.some(p=>p.startsWith('PPC: o período L6')));assert.ok(!joint.pending.some(p=>p.startsWith('PEC: o período L6')));
for(const outside of [{from:'2025-07-01',to:'2026-06-30'},{from:'2025-01-01',to:'2025-12-31'},{from:'2026-02-30',to:'2026-12-31'}]) assert.equal(calculatePecObligation(b.pecObligation,b.subjectNuit,b.year,b.assessment,outside).total,null);
});
test('recovery and frozen reports use the recorded L6 period without changing the independent annual calculation',()=>{
const {d,b,period}=periodAdvanceCase('noncivil');const without={...b,ppcObligation:undefined,pecObligation:undefined};const prior=recordAnnual(d,without,'2026-11-13T22:01:00Z');
const current=recordAnnual(d,{...prior,ppcObligation:b.ppcObligation,pecObligation:b.pecObligation},'2026-11-13T22:02:00Z');assert.deepEqual(current.result.model.arithmeticBalance,prior.result.model.arithmeticBalance);
d.modelo_a_snapshot=saveAnnualBook(d,current);const recovered=annualBook(d);assert.equal(annualCurrent(d,recovered),true);assert.equal(recovered.history.length,1);assert.deepEqual(recovered.result.model.assessment.period,period);assert.match(annualHtml(d),/período L6/);assert.ok(!annualHtml(d).includes('Total da projecção: 112000'));
});
test('incomplete invalid and contradictory calendar metadata require reconciliation rather than replacing dates',()=>{
const {b}=periodAdvanceCase();for(const period of [{from:'2026-01-01',to:null},{from:'2026-02-30',to:'2026-12-31'},{from:'2025-01-01',to:'2025-12-31'}])assert.equal(annualAdvanceCashflow(b,period).total,null);
});
test('edited L6 period is not silently replaced by the prior calculated period and is frozen on annual recalculation',()=>{
const {d,b}=periodAdvanceCase();const n=normalBook(d);n.periodsFx={...newNormalPeriodFx(),kind:'cessation',from:'2026-01-01',to:'2026-06-30',planStart:'2026-01-01',reference:'SYNTHETIC period edit',mznMapsReference:'SYNTHETIC pending review'};
d.modelo_a_snapshot=saveNormalBook(d,n);const period=annualAdvancePeriod(d);assert.equal(period.to,'2026-06-30');assert.equal(annualAdvanceCashflow(b,period).total,null);
const saved=recordAnnual(d,b,'2026-11-13T22:03:00Z');assert.deepEqual(saved.result.model.advancePeriod,period);d.modelo_a_snapshot=saveAnnualBook(d,saved);assert.match(annualHtml(d),/período L6/);
});
+22
View File
@@ -0,0 +1,22 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import {periodAdvanceCase} from '../tests/frontend/helpers/irpc-advance-period-case.mjs';
import {annualAdvanceCashflow,annualAdvancePeriod,annualBook,annualCurrent,annualHtml,recordAnnual,saveAnnualBook} from '../resources/js/lib/irpc-annual-settlement.ts';
import {createProjectDossierReport} from '../resources/js/lib/project-dossier-report.ts';
const out=new URL('../outputs/irpc-period-consistency-20261113/',import.meta.url);fs.mkdirSync(out,{recursive:true});
for(const name of ['ordinary','noncivil','short','start']){
if(process.argv[2]==='generate'){
const {d,b}=periodAdvanceCase(name);const prior=recordAnnual(d,{...b,ppcObligation:undefined,pecObligation:undefined},'2026-11-13T22:05:00Z');
d.modelo_a_snapshot=saveAnnualBook(d,recordAnnual(d,{...prior,ppcObligation:b.ppcObligation,pecObligation:b.pecObligation},'2026-11-13T22:06:00Z'));fs.writeFileSync(new URL(name+'.json',out),JSON.stringify(d));
}else{
const d=JSON.parse(fs.readFileSync(new URL('recovered-'+name+'.json',out),'utf8')),b=annualBook(d),period=annualAdvancePeriod(d),m=annualAdvanceCashflow(b,period);
assert.equal(annualCurrent(d,b),true);assert.equal(b.history.length,1);assert.deepEqual(b.result.model.arithmeticBalance,b.history[0].model.arithmeticBalance);assert.deepEqual(b.result.model.advancePeriod,period);
if(name==='ordinary')assert.deepEqual(m.total,{numerator:'112000',denominator:'1'});else{assert.equal(m.total,null);assert.equal(m.rows.length,0);}
if(name==='start')assert.equal(m.pecKnown,true);
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-11-13T22:07:00Z'});
assert.match(html,/Projecção mensal conjunta PPC\/PEC/);assert.match(JSON.stringify(report),/Projecção mensal conjunta PPC\/PEC/);
if(name!=='ordinary'){assert.match(html,/período L6/);assert.ok(!html.includes('Total da projecção: 112000'));}
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,annualCalculationUnchanged:true,report:true,period,model:m},null,2));
}
}
console.log(process.argv[2]==='generate'?'Generated four synthetic period consistency cases':'Verified four recovered period contexts and 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-period-consistency-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/IrpcAdvancePeriodDraftTest.php'];
$_SERVER['argv'] = $argv;
$_SERVER['argc'] = count($argv);
require $root.'/vendor/autoload.php';
require $root.'/vendor/phpunit/phpunit/phpunit';