351 lines
13 KiB
JavaScript
351 lines
13 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { carryCase } from './helpers/irpc-carry-case.mjs';
|
|
import { internationalCase } from './helpers/irpc-international-case.mjs';
|
|
import {
|
|
newCarryUse,
|
|
calculateCarryLot,
|
|
} from '../../resources/js/lib/irpc-credit-carry.ts';
|
|
import {
|
|
calculateAnnual,
|
|
prepareCreditCarry,
|
|
recordAnnual,
|
|
annualBook,
|
|
annualCurrent,
|
|
saveAnnualBook,
|
|
annualHtml,
|
|
annualReportRows,
|
|
} from '../../resources/js/lib/irpc-annual-settlement.ts';
|
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
|
const number = (v) => (v ? Number(v.numerator) / Number(v.denominator) : null);
|
|
const calc = (c) => calculateAnnual(c.d, c.b);
|
|
const lot = (c, i = 0) => c.b.creditCarry.lots[i];
|
|
const pending = (c) => {
|
|
const m = calc(c);
|
|
assert.equal(m.status, 'indeterminate', JSON.stringify(m.pending));
|
|
assert.equal(m.arithmeticBalance, null);
|
|
assert.equal(m.internationalCredit.deducted, null);
|
|
assert.equal(m.pecDeduction.deducted, null);
|
|
return m;
|
|
};
|
|
|
|
void test('2021 international and2023 PEC remain eligible in2026; origin deductions/uses count once:1408-8-50-200-100-500=550', () => {
|
|
const m = calc(carryCase());
|
|
assert.deepEqual(m.pending, []);
|
|
assert.equal(number(m.payable), 550);
|
|
assert.equal(number(m.creditCarry.lots[0].opening), 100);
|
|
assert.equal(number(m.creditCarry.lots[0].used), 30);
|
|
assert.equal(number(m.creditCarry.lots[0].available), 70);
|
|
assert.equal(number(m.creditCarry.lots[0].remaining), 20);
|
|
assert.equal(m.creditCarry.lots[0].lastYear, 2026);
|
|
assert.equal(m.creditCarry.lots[1].lastYear, 2026);
|
|
assert.equal(number(m.creditCarry.lots[1].remaining), 50);
|
|
assert.equal(m.creditCarry.carryCreated, false);
|
|
assert.equal(m.creditCarry.officialBalancesConsumed, false);
|
|
});
|
|
void test('five subsequent exercises differs from PEC three; expired source retained with zero selection', () => {
|
|
for (const [i, year] of [
|
|
[0, '2020'],
|
|
[1, '2022'],
|
|
]) {
|
|
const c = carryCase();
|
|
lot(c, i).year = year;
|
|
if (i === 0) lot(c).uses[0].year = '2023';
|
|
assert.match(pending(c).pending.join(' '), /Prazo ordinário terminado/);
|
|
lot(c, i).selected = '0';
|
|
const m = calc(c);
|
|
assert.deepEqual(m.pending, []);
|
|
assert.equal(m.creditCarry.lots[i].expired, true);
|
|
assert.equal(number(m.creditCarry.lots[i].available), 0);
|
|
assert.ok(number(m.creditCarry.lots[i].remaining) > 0);
|
|
}
|
|
});
|
|
void test('future/current origin, nonordinary periods, currency and wrong subject do not create an eligible carry', () => {
|
|
for (const patch of [
|
|
{ year: '2026' },
|
|
{ year: '2027' },
|
|
{ year: '' },
|
|
{ ordinary: '' },
|
|
{ ordinary: 'no' },
|
|
{ currency: 'USD' },
|
|
{ subjectNuit: '999999999' },
|
|
{ country: 'MZ' },
|
|
{ country: 'ZZ' },
|
|
{ kind: '' },
|
|
]) {
|
|
const c = carryCase();
|
|
Object.assign(lot(c), patch);
|
|
pending(c);
|
|
}
|
|
const c = carryCase();
|
|
lot(c, 1).country = 'ZA';
|
|
pending(c);
|
|
});
|
|
void test('origin eligibility, insufficient collecta and applicable rule require concrete evidence', () => {
|
|
for (const patch of [
|
|
{ originDocument: '' },
|
|
{ originReference: '' },
|
|
{ ruleReference: '' },
|
|
{ insufficient: '' },
|
|
{ insufficient: 'no' },
|
|
{ eligible: '' },
|
|
{ originDeducted: '' },
|
|
{ originDeducted: '201' },
|
|
{ selected: '' },
|
|
{ selected: '70.01' },
|
|
]) {
|
|
const c = carryCase();
|
|
Object.assign(lot(c), patch);
|
|
pending(c);
|
|
}
|
|
});
|
|
void test('unknown uses are not zero; every actual use must fit its origin/window and documentary identity', () => {
|
|
for (const patch of [
|
|
{ usesState: '' },
|
|
{ usesReference: '' },
|
|
{ usesState: 'absent' },
|
|
{ uses: [] },
|
|
{
|
|
uses: [
|
|
{
|
|
...newCarryUse('u'),
|
|
year: '2021',
|
|
amount: '1',
|
|
document: 'D',
|
|
reference: 'R',
|
|
},
|
|
],
|
|
},
|
|
]) {
|
|
const c = carryCase();
|
|
Object.assign(lot(c), patch);
|
|
pending(c);
|
|
}
|
|
for (const patch of [
|
|
{ year: '2027' },
|
|
{ amount: '101' },
|
|
{ document: '' },
|
|
{ reference: '' },
|
|
{ amount: '' },
|
|
]) {
|
|
const c = carryCase();
|
|
Object.assign(lot(c).uses[0], patch);
|
|
pending(c);
|
|
}
|
|
const c = carryCase();
|
|
lot(c).uses.push({
|
|
...lot(c).uses[0],
|
|
id: 'copy',
|
|
year: '2024',
|
|
document: ' synthetic USE2023 ',
|
|
});
|
|
assert.match(pending(c).pending.join(' '), /repetido/);
|
|
});
|
|
void test('actual current-year use is subtracted before the proposed new deduction', () => {
|
|
const c = carryCase();
|
|
lot(c).uses.push({
|
|
id: 'current-actual',
|
|
year: '2026',
|
|
amount: '10',
|
|
document: 'SYNTHETIC current actual use',
|
|
reference: 'SYNTHETIC actual assessment, not this simulation',
|
|
});
|
|
const m = calc(c);
|
|
assert.equal(number(m.creditCarry.lots[0].available), 60);
|
|
assert.equal(number(m.creditCarry.lots[0].remaining), 10);
|
|
assert.equal(number(m.payable), 550);
|
|
});
|
|
void test('original country/nature/year cannot be duplicated by changing document or id', () => {
|
|
const c = carryCase();
|
|
c.b.creditCarry.lots.push({
|
|
...structuredClone(lot(c)),
|
|
id: 'other',
|
|
originDocument: 'OTHER',
|
|
uses: [],
|
|
usesState: 'absent',
|
|
});
|
|
assert.match(pending(c).pending.join(' '), /mesma natureza/);
|
|
});
|
|
void test('cross-context duplicate origin blocks and external edits invalidate saved current result', () => {
|
|
const c = carryCase();
|
|
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recordAnnual(c.d, c.b));
|
|
const b = annualBook(c.d);
|
|
assert.equal(annualCurrent(c.d, b), true);
|
|
const s = JSON.parse(c.d.modelo_a_snapshot),
|
|
other = structuredClone(c.b);
|
|
other.creditCarry.lots = [
|
|
{
|
|
...structuredClone(lot(c)),
|
|
id: 'copied-origin',
|
|
originDocument: 'OTHER',
|
|
uses: [],
|
|
usesState: 'absent',
|
|
},
|
|
];
|
|
s.irpcAnnual['opportunity:other'] = other;
|
|
c.d.modelo_a_snapshot = JSON.stringify(s);
|
|
assert.equal(annualCurrent(c.d, b), false);
|
|
assert.match(pending(c).pending.join(' '), /noutro mapa/);
|
|
});
|
|
void test('explicit allocation can favour older balances without automatic reduction or FIFO', () => {
|
|
const c = carryCase();
|
|
Object.assign(c.b.creditCarry, {
|
|
currentInternational: '0',
|
|
currentPec: '100',
|
|
});
|
|
const m = calc(c);
|
|
assert.equal(number(m.payable), 658);
|
|
assert.equal(number(m.internationalCredit.deducted), 0);
|
|
assert.equal(number(m.internationalCredit.notSelected), 8);
|
|
assert.equal(m.internationalCredit.insufficientCollecta, null);
|
|
assert.equal(number(m.pecDeduction.deducted), 100);
|
|
assert.equal(number(m.pecDeduction.remaining), 100);
|
|
});
|
|
void test('current selection cannot exceed known availability or total collecta; planned choices are kept on failure', () => {
|
|
for (const patch of [
|
|
{ currentInternational: '9' },
|
|
{ currentPec: '201' },
|
|
{ currentInternational: '' },
|
|
{ currentPec: '' },
|
|
{ allocationReference: '' },
|
|
{ assessmentKey: 'stale' },
|
|
]) {
|
|
const c = carryCase();
|
|
Object.assign(c.b.creditCarry, patch);
|
|
pending(c);
|
|
}
|
|
const c = carryCase();
|
|
lot(c).eligible = '5000';
|
|
lot(c).selected = '1400';
|
|
const before = structuredClone(c.b);
|
|
assert.match(pending(c).pending.join(' '), /ultrapassam a colecta/);
|
|
assert.deepEqual(c.b, before);
|
|
});
|
|
void test('no credit or PEC reimbursement is generated by old balances; PPC refund stays separate', () => {
|
|
const c = carryCase();
|
|
Object.assign(c.b.creditCarry, {
|
|
currentInternational: '8',
|
|
currentPec: '0',
|
|
});
|
|
lot(c).eligible = '2000';
|
|
lot(c).selected = '1300';
|
|
lot(c, 1).selected = '100';
|
|
const m = calc(c);
|
|
assert.equal(number(m.recoverable), 500);
|
|
assert.equal(number(m.pecDeduction.deducted), 0);
|
|
assert.equal(m.creditCarry.carryCreated, false);
|
|
assert.equal(number(m.creditCarry.lots[0].remaining), 570);
|
|
});
|
|
void test('exact foreign-credit fractions are preserved; decimal presentation is never fiscal rounding', () => {
|
|
const c = carryCase();
|
|
Object.assign(lot(c), {
|
|
eligible: '2/3',
|
|
originDeducted: '1/3',
|
|
selected: '1/3',
|
|
usesState: 'absent',
|
|
uses: [],
|
|
});
|
|
Object.assign(c.b.creditCarry, { currentInternational: '1/3' });
|
|
const m = calc(c);
|
|
assert.deepEqual(m.pending, []);
|
|
assert.deepEqual(m.creditCarry.lots[0].available, {
|
|
numerator: '1',
|
|
denominator: '3',
|
|
});
|
|
assert.deepEqual(m.payable, { numerator: '1822', denominator: '3' });
|
|
lot(c).selected = '1/0';
|
|
pending(c);
|
|
});
|
|
void test('missing/absent/known-zero remain distinct; absence does not demand unused allocation fields', () => {
|
|
const c = internationalCase(),
|
|
baseline = calc(c);
|
|
c.b = prepareCreditCarry(c.b);
|
|
pending(c);
|
|
c.b.creditCarry.state = 'absent';
|
|
c.b.creditCarry.reference = 'SYNTHETIC QA documented absence';
|
|
let m = calc(c);
|
|
assert.equal(number(m.payable), number(baseline.payable));
|
|
assert.deepEqual(m.internationalCredit, baseline.internationalCredit);
|
|
c.b.creditCarry.lots.push({ ...carryCase().b.creditCarry.lots[0] });
|
|
pending(c);
|
|
const z = carryCase();
|
|
Object.assign(lot(z), {
|
|
eligible: '0',
|
|
originDeducted: '0',
|
|
selected: '0',
|
|
usesState: 'absent',
|
|
uses: [],
|
|
});
|
|
m = calc(z);
|
|
assert.deepEqual(m.pending, []);
|
|
assert.equal(number(m.creditCarry.lots[0].available), 0);
|
|
});
|
|
void test('optional preparation, recording, JSON recovery, HTML and dossier preserve accepted books and frozen history', () => {
|
|
const c = internationalCase();
|
|
c.d.modelo_a_snapshot = saveAnnualBook(
|
|
c.d,
|
|
recordAnnual(c.d, c.b, '2026-10-27T17:00:00+02:00'),
|
|
);
|
|
const old = annualBook(c.d),
|
|
frozen = structuredClone(old.result),
|
|
existing = JSON.parse(c.d.modelo_a_snapshot),
|
|
plan = c.d.business_plan_snapshot;
|
|
const next = prepareCreditCarry(old);
|
|
next.creditCarry = carryCase().b.creditCarry;
|
|
c.d.modelo_a_snapshot = saveAnnualBook(
|
|
c.d,
|
|
recordAnnual(c.d, next, '2026-10-28T12:00:00+02:00'),
|
|
);
|
|
const recovered = annualBook(JSON.parse(JSON.stringify(c.d)));
|
|
assert.deepEqual(recovered.history, [frozen]);
|
|
assert.equal(number(recovered.result.model.payable), 550);
|
|
assert.equal(annualCurrent(c.d, recovered), true);
|
|
assert.equal(c.d.business_plan_snapshot, plan);
|
|
const after = JSON.parse(c.d.modelo_a_snapshot);
|
|
delete after.irpcAnnual;
|
|
delete existing.irpcAnnual;
|
|
assert.deepEqual(after, existing);
|
|
assert.match(
|
|
annualHtml(c.d),
|
|
/Saldo anterior — Crédito internacional ZA — 2021/,
|
|
);
|
|
assert.match(annualHtml(c.d), /550.00/);
|
|
assert.match(annualHtml(c.d), /2026/);
|
|
assert.match(
|
|
JSON.stringify(
|
|
createProjectDossierReport(c.d, {
|
|
selectedSections: new Set(['finance']),
|
|
}),
|
|
),
|
|
/Afectação escolhida entre exercícios/,
|
|
);
|
|
recovered.creditCarry.lots[0].selected = '49';
|
|
c.d.modelo_a_snapshot = saveAnnualBook(c.d, recovered);
|
|
assert.equal(annualCurrent(c.d, recovered), false);
|
|
assert.deepEqual(recovered.result.model, annualBook(c.d).result.model);
|
|
assert.throws(() => annualHtml(c.d), /fontes actuais/);
|
|
assert.match(annualReportRows(c.d).flat().join(' '), /Histórico/);
|
|
});
|
|
void test('malformed detail and duplicate internal ids cannot overwrite original snapshots', () => {
|
|
for (const mutate of [
|
|
(c) => (c.b.creditCarry.lots[0].uses = null),
|
|
(c) => (c.b.creditCarry.version = 2),
|
|
(c) => (c.b.creditCarry.lots[0].uses[0].amount = null),
|
|
(c) => (c.b.creditCarry.lots[1].id = c.b.creditCarry.lots[0].id),
|
|
]) {
|
|
const c = carryCase(),
|
|
before = c.d.modelo_a_snapshot;
|
|
mutate(c);
|
|
assert.throws(() => saveAnnualBook(c.d, c.b), /recuper|repetido/);
|
|
assert.equal(c.d.modelo_a_snapshot, before);
|
|
}
|
|
});
|
|
void test('standalone expired PEC keeps origin and is not silently converted to international five-year window', () => {
|
|
const c = carryCase(),
|
|
r = { ...lot(c, 1), year: '2022', selected: '0' };
|
|
const m = calculateCarryLot(r, c.b.subjectNuit, c.b.year);
|
|
assert.equal(m.expired, true);
|
|
assert.equal(m.lastYear, 2025);
|
|
assert.equal(number(m.remaining), 150);
|
|
});
|