224 lines
8.9 KiB
JavaScript
224 lines
8.9 KiB
JavaScript
import { pecCase } from './helpers/irpc-pec-case.mjs';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
annualBook,
|
|
calculateAnnual,
|
|
recordAnnual,
|
|
saveAnnualBook,
|
|
annualCurrent,
|
|
annualReportRows,
|
|
annualHtml,
|
|
} from '../../resources/js/lib/irpc-annual-settlement.ts';
|
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
|
const value = (x) => (x ? Number(x.numerator) / Number(x.denominator) : null);
|
|
const now = '2026-10-27T15:00:00+02:00';
|
|
|
|
const model = ({ d, b }) => calculateAnnual(d, b);
|
|
const indeterminate = (c) => {
|
|
const m = model(c);
|
|
assert.equal(m.status, 'indeterminate', JSON.stringify(m.pending));
|
|
assert.equal(m.arithmeticBalance, null);
|
|
assert.equal(m.pecDeduction.deducted, null);
|
|
assert.equal(m.pecDeduction.remaining, null);
|
|
return m;
|
|
};
|
|
|
|
void test('PEC precedes PPC; 1408 - 200 - 500 = 708, no official consumption', () => {
|
|
const m = model(pecCase());
|
|
assert.deepEqual(m.pending, []);
|
|
assert.equal(value(m.pecDeduction.deducted), 200);
|
|
assert.equal(value(m.pecDeduction.afterPec), 1208);
|
|
assert.equal(value(m.payable), 708);
|
|
assert.equal(m.pecDeduction.carryCreated, false);
|
|
assert.equal(m.officialBalancesConsumed, false);
|
|
});
|
|
void test('existing received L5 proof is reused after PEC and cannot become another PEC receipt', () => {
|
|
const c = pecCase('200', '500', '300');
|
|
assert.equal(value(model(c).payable), 408);
|
|
Object.assign(c.b.pecPayments.rows[0], {
|
|
document: 'QA-RF-PEC-2026',
|
|
proofAmount: '300',
|
|
});
|
|
assert.match(indeterminate(c).pending.join(' '), /natureza/);
|
|
});
|
|
void test('PEC cap never generates refund; PPC can still generate recoverable balance', () => {
|
|
const a = model(pecCase('2000', '0'));
|
|
assert.equal(value(a.pecDeduction.deducted), 1408);
|
|
assert.equal(value(a.pecDeduction.remaining), 592);
|
|
assert.equal(value(a.arithmeticBalance), 0);
|
|
assert.equal(value(a.recoverable), 0);
|
|
const b = model(pecCase('2000', '500'));
|
|
assert.equal(value(b.recoverable), 500);
|
|
assert.equal(value(b.pecDeduction.remaining), 592);
|
|
assert.equal(value(b.pecDeduction.afterPec), 0);
|
|
});
|
|
void test('previous use explicit, precise decimals and available portion are respected', () => {
|
|
const c = pecCase('200.25', '0');
|
|
Object.assign(c.b.pecPayments.rows[0], {
|
|
proofAmount: '300.75',
|
|
previouslyUsed: '100.50',
|
|
});
|
|
assert.equal(value(model(c).payable), 1207.75);
|
|
c.b.pecPayments.rows[0].amount = '200.26';
|
|
indeterminate(c);
|
|
});
|
|
void test('absence, known zero and missing PEC are distinct; contradictory rows block', () => {
|
|
const c = pecCase();
|
|
c.b.pecPayments.rows = [];
|
|
c.b.pecPayments.state = 'absent';
|
|
assert.equal(value(model(c).payable), 908);
|
|
c.b.pecPayments.state = '';
|
|
indeterminate(c);
|
|
c.b.pecPayments.state = 'known';
|
|
indeterminate(c);
|
|
const z = pecCase('0');
|
|
assert.equal(value(model(z).payable), 908);
|
|
z.b.pecPayments.rows[0].previouslyUsed = '';
|
|
indeterminate(z);
|
|
z.b.pecPayments.state = 'absent';
|
|
indeterminate(z);
|
|
});
|
|
void test('payment proof, unused balance, entity, year and currency cannot be presumed', () => {
|
|
for (const change of [
|
|
{ effective: '' },
|
|
{ effective: 'no' },
|
|
{ document: '' },
|
|
{ reference: '' },
|
|
{ priorUseReference: '' },
|
|
{ previouslyUsed: '' },
|
|
{ previouslyUsed: '201' },
|
|
{ amount: '201' },
|
|
{ amount: '1.001' },
|
|
{ subjectNuit: '999999999' },
|
|
{ year: '2025' },
|
|
{ date: '2025-08-31' },
|
|
{ date: '2026-02-30' },
|
|
{ currency: 'USD' },
|
|
]) {
|
|
const c = pecCase();
|
|
Object.assign(c.b.pecPayments.rows[0], change);
|
|
indeterminate(c);
|
|
}
|
|
});
|
|
void test('same physical proof cannot be duplicated, re-dated or relabelled PPC', () => {
|
|
const a = pecCase();
|
|
a.b.pecPayments.rows.push({
|
|
...a.b.pecPayments.rows[0],
|
|
id: 'copy',
|
|
date: '2026-09-01',
|
|
});
|
|
assert.match(
|
|
indeterminate(a).pending.join(' '),
|
|
/repetido|saldo disponível/,
|
|
);
|
|
const b = pecCase();
|
|
Object.assign(b.b.pecPayments.rows[0], {
|
|
document: b.b.ppc.rows[0].document,
|
|
proofAmount: '500',
|
|
});
|
|
assert.match(
|
|
indeterminate(b).pending.join(' '),
|
|
/naturezas|natureza incompatível/,
|
|
);
|
|
});
|
|
void test('cross-context allocations respect prior use and changing external claims invalidates current result', () => {
|
|
const c = pecCase('100', '0'),
|
|
r = c.b.pecPayments.rows[0];
|
|
r.proofAmount = '500';
|
|
r.previouslyUsed = '100';
|
|
const other = structuredClone(c.b);
|
|
other.pecPayments.rows[0].amount = '300';
|
|
const s = JSON.parse(c.d.modelo_a_snapshot);
|
|
s.irpcAnnual = { 'opportunity:other': other };
|
|
c.d.modelo_a_snapshot = JSON.stringify(s);
|
|
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b, now));
|
|
const saved = annualBook(c.d);
|
|
assert.equal(annualCurrent(c.d, saved), true);
|
|
assert.equal(value(saved.result.model.payable), 1308);
|
|
const edit = JSON.parse(c.d.modelo_a_snapshot);
|
|
edit.irpcAnnual['opportunity:other'].pecPayments.rows[0].amount = '301';
|
|
c.d.modelo_a_snapshot = JSON.stringify(edit);
|
|
assert.equal(annualCurrent(c.d, saved), false);
|
|
assert.match(
|
|
indeterminate({ ...c, b: saved }).pending.join(' '),
|
|
/saldo disponível/,
|
|
);
|
|
edit.irpcAnnual['opportunity:other'].pecPayments.rows[0].amount = '100';
|
|
edit.irpcAnnual['opportunity:other'].pecPayments.rows[0].previouslyUsed =
|
|
'0';
|
|
c.d.modelo_a_snapshot = JSON.stringify(edit);
|
|
indeterminate({ ...c, b: saved });
|
|
});
|
|
void test('other contexts cannot change PEC titular, origin or payment nature', () => {
|
|
for (const change of [{ subjectNuit: '999999999' }, { year: '2025' }]) {
|
|
const c = pecCase('100', '0'),
|
|
other = structuredClone(c.b);
|
|
other.pecPayments.rows[0].proofAmount = '500';
|
|
Object.assign(other.pecPayments.rows[0], change);
|
|
const s = JSON.parse(c.d.modelo_a_snapshot);
|
|
s.irpcAnnual = { 'opportunity:other': other };
|
|
c.d.modelo_a_snapshot = JSON.stringify(s);
|
|
indeterminate(c);
|
|
}
|
|
const c = pecCase('100', '0'),
|
|
other = structuredClone(c.b);
|
|
other.ppc.rows = [
|
|
{ ...c.b.pecPayments.rows[0], amount: '50', proofAmount: '100' },
|
|
];
|
|
other.ppc.state = 'known';
|
|
other.pecPayments.rows = [];
|
|
other.pecPayments.state = 'absent';
|
|
const s = JSON.parse(c.d.modelo_a_snapshot);
|
|
s.irpcAnnual = { 'opportunity:other': other };
|
|
c.d.modelo_a_snapshot = JSON.stringify(s);
|
|
indeterminate(c);
|
|
});
|
|
void test('positive special credits remain scoped and cannot be silently skipped before PEC', () => {
|
|
const c = pecCase();
|
|
c.b.credits.amount = '10';
|
|
assert.match(indeterminate(c).pending.join(' '), /Créditos/);
|
|
assert.equal(value(model(c).pecDeduction.available), 200);
|
|
});
|
|
void test('save/recover/recalculate preserves old annual results and all other fiscal books, plan and reports', () => {
|
|
const c = pecCase(),
|
|
before = JSON.parse(c.d.modelo_a_snapshot),
|
|
plan = c.d.business_plan_snapshot;
|
|
const legacy = structuredClone(c.b);
|
|
delete legacy.pecPayments;
|
|
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, legacy, now));
|
|
const old = structuredClone(annualBook(c.d).result);
|
|
c.b = { ...annualBook(c.d), pecPayments: c.b.pecPayments };
|
|
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b, now));
|
|
const back = annualBook(JSON.parse(JSON.stringify(c.d)));
|
|
assert.deepEqual(back.history, [old]);
|
|
assert.equal(value(back.result.model.payable), 708);
|
|
assert.equal(annualCurrent(c.d, back), true);
|
|
const after = JSON.parse(c.d.modelo_a_snapshot);
|
|
delete after.irpcAnnual;
|
|
assert.deepEqual(after, before);
|
|
assert.equal(c.d.business_plan_snapshot, plan);
|
|
const html = annualHtml(c.d);
|
|
assert.match(html, /PEC — dedução limitada/);
|
|
assert.match(html, /708.00/);
|
|
assert.match(html, /sem reembolso PEC/);
|
|
const dossier = createProjectDossierReport(c.d, {
|
|
selectedSections: new Set(['finance']),
|
|
});
|
|
assert.match(JSON.stringify(dossier), /PEC — dedução limitada/);
|
|
const frozen = structuredClone(back.result);
|
|
back.pecPayments.rows[0].amount = '100';
|
|
c.d.modelo_a_snapshot = saveAnnualBook(c.d, back);
|
|
assert.equal(annualCurrent(c.d, back), false);
|
|
assert.deepEqual(back.result, frozen);
|
|
assert.match(annualReportRows(c.d).flat().join(' '), /Histórico/);
|
|
assert.throws(() => annualHtml(c.d), /fontes actuais/);
|
|
});
|
|
void test('malformed PEC extension never replaces legacy snapshots', () => {
|
|
const c = pecCase(),
|
|
original = c.d.modelo_a_snapshot;
|
|
c.b.pecPayments.rows[0].previouslyUsed = null;
|
|
assert.throws(() => saveAnnualBook(c.d, c.b), /não recuperáveis/);
|
|
assert.equal(c.d.modelo_a_snapshot, original);
|
|
});
|