Files
Investidor-V2/tests/frontend/irpc-interest-cer.test.mjs
T

427 lines
14 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {
newIrpcRow,
emptyIrpcOrdinaryFacts,
recordIrpc,
saveIrpcBook,
irpcBook,
irpcCurrent,
irpcReportRows,
} from '../../resources/js/lib/irpc-withholdings.ts';
import {
newMrftaInput,
recordMrfta,
mrftaCurrent,
mrftaHtml,
prepareMrfta,
} from '../../resources/js/lib/irpc-mrfta.ts';
import {
newIrpcInterestRule,
calculateIrpcInterest,
} from '../../resources/js/lib/irpc-interest.ts';
import {
mrftaCases,
mrftaCaseHtml,
} from '../../resources/js/lib/irpc-mrfta-cases.ts';
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
const draft = () =>
JSON.parse(
fs.readFileSync(
new URL('./fixtures/irpc-grouped-preserved.json', import.meta.url),
'utf8',
),
);
const rule = () => ({
...newIrpcInterestRule(),
annualRate: '10',
rateSource: 'QA parâmetro fictício; não é taxa fiscal2026',
fiscalBasis: 'QA fundamento sintético para ensaio aritmético',
validFrom: '2026-01-01',
validTo: '2026-12-31',
dayBasis: '365',
dayBasisRef: 'QA fonte sintética da base365',
rounding: 'total_cent_half_up',
roundingRef: 'QA fonte sintética do arredondamento do total',
retention: 'withheld',
occurrenceRef: 'QA retenção e atraso documentados',
});
const row = (id = 'one') => ({
...newIrpcRow(id),
counterparty: 'QA contraparte',
documentRef: 'QA-' + id,
base: '36500',
kind: 'royalty',
recipientIrpc: 'yes',
payerIrpc: 'yes',
residence: 'nonresident',
permanentEstablishment: 'no',
mozambique: 'yes',
qualificationRef: 'QA incidência e residência',
specialRelief: 'none',
eventsComplete: 'yes',
events: [{ kind: 'cost', date: '2026-10-02', reference: 'QA custo' }],
settlement: 'none',
settlementRef: 'QA sem entregas',
ordinary: {
...emptyIrpcOrdinaryFacts(),
nonDigital: 'yes',
reference: 'QA royalties não digitais',
},
});
const book = (payments = []) => {
const r = row();
if (payments.length) {
r.settlement = 'complete';
r.allocations = payments.map((p) => ({
paymentId: p.id,
amount: p.amount,
}));
}
return recordIrpc(draft(), { version: 1, rows: [r], payments });
};
const payment = (amount, date = '2026-11-25', id = 'p') => ({
id,
kind: 'remittance',
reference: 'QA entrega-' + id,
date,
amount,
currency: 'MZN',
});
const calc = (b = book(), r = rule(), due = '2026-11-20', end = '2026-11-30') =>
calculateIrpcInterest(
b,
true,
'2026-10',
due,
'QA prazo documentado',
end,
r,
);
const input = () => ({
...newMrftaInput(),
period: '2026-10',
declaration: 'initial',
timing: 'late',
nuit: '999999999',
name: 'QA empresa',
taxArea: 'QA área fiscal',
address: 'QA domicílio',
completeness: 'yes',
completenessRef: 'QA reconciliação do período',
otherRubrics: 'none',
otherRubricsRef: 'QA outras rubricas não devidas',
codes: [
{ rowId: 'one', cer: '999', reference: 'QA CER fictício, não oficial' },
],
interest: {
...newMrftaInput().interest,
mode: '',
calculation: 'automatic',
period: '2026-10',
date: '2026-11-30',
dueDate: '2026-11-20',
dueReference: 'QA prazo documentado',
reference: 'QA ocorrência',
automatic: rule(),
},
});
void test('daily interest uses documented parameters and explains principal,days and exact arithmetic', () => {
const v = calc();
assert.deepEqual(v.pending, []);
assert.equal(v.total.numerator, '20');
assert.equal(v.rows[0].segments[0].from, '2026-11-21');
assert.equal(v.rows[0].segments[0].to, '2026-11-30');
assert.equal(v.rows[0].segments[0].days, 10);
assert.equal(v.rows[0].segments[0].principal.numerator, '7300');
});
void test('partial remittance reduces principal after its payment day without duplicate daily interest', () => {
const v = calc(book([payment('3650')]));
assert.equal(v.total.numerator, '15');
assert.deepEqual(
v.rows[0].segments.map((s) => [s.days, s.principal.numerator]),
[
[5, '7300'],
[5, '3650'],
],
);
const sameDay = calc(
book([
payment('1825', '2026-11-25', 'p1'),
payment('1825', '2026-11-25', 'p2'),
]),
);
assert.equal(sameDay.total.numerator, '15');
assert.equal(sameDay.rows[0].segments.length, 2);
});
void test('full payment stops charges and pre-deadline payment reduces the opening debt', () => {
assert.equal(calc(book([payment('7300')])).total.numerator, '10');
assert.equal(
calc(book([payment('3650', '2026-11-18')])).total.numerator,
'10',
);
});
void test('missing rate,reference,day basis or applicable version never becomes a default tax rate', () => {
for (const patch of [
{ annualRate: '' },
{ annualRate: '0' },
{ annualRate: '-1' },
{ rateSource: '' },
{ fiscalBasis: '' },
{ dayBasis: '' },
{ dayBasisRef: '' },
{ rounding: '' },
{ roundingRef: '' },
{ validTo: '2017-12-31' },
{ occurrenceRef: '' },
]) {
const v = calc(book(), { ...rule(), ...patch });
assert.equal(v.total, null);
assert.ok(v.pending.length);
}
assert.equal(
calculateIrpcInterest(
book(),
false,
'2026-10',
'2026-11-20',
'QA prazo',
'2026-11-30',
rule(),
).total,
null,
);
});
void test('on-account not-withheld cases remain separate;definitive not-withheld uses its own supported branch', () => {
const r = row();
r.residence = 'resident';
const b = recordIrpc(draft(), { version: 1, rows: [r], payments: [] });
assert.equal(calc(b, { ...rule(), retention: 'not_withheld' }).total, null);
assert.match(
calc(b, { ...rule(), retention: 'not_withheld' }).pending.join(' '),
/declaração anual/,
);
assert.equal(calc(b).total.numerator, '20');
assert.equal(
calc(book(), { ...rule(), retention: 'not_withheld' }).total.numerator,
'20',
);
});
void test('documented actual-year basis splits year boundary including leap year without floating money', () => {
const r = { ...rule(), validTo: '2028-12-31', dayBasis: 'actual_year' },
v = calc(book(), r, '2027-12-30', '2028-01-02');
assert.deepEqual(
v.rows[0].segments.map((s) => [s.days, s.denominator]),
[
[1, 365],
[2, 366],
],
);
assert.deepEqual(v.total, { numerator: '599', denominator: '100' });
});
void test('rounding is explicit,on total only,and matches the documented convention', () => {
assert.deepEqual(calc(book(), { ...rule(), annualRate: '10.0025' }).total, {
numerator: '2001',
denominator: '100',
});
assert.equal(
calc(book(), {
...rule(),
annualRate: '10.0025',
rounding: 'total_cent_down',
}).total.numerator,
'20',
);
});
void test('unreconciled payments and payments after liquidation keep the affected calculation indeterminate', () => {
assert.equal(calc(book([payment('100', '2026-12-01')])).total, null);
const b = book();
b.result.rows[0].settled = null;
assert.equal(calc(b).total, null);
});
void test('distinct rates and CER produce stable working cases,not invented declarations or merged rates', () => {
const b = { ...book(), mrfta: input() };
const second = { ...b.result.rows[0], id: 'two', rate: 10 };
b.result.rows.push(second);
b.result.inputs.push(row('two'));
b.mrfta.codes.push({
rowId: 'two',
cer: '998',
reference: 'QA second fictionalCER',
});
const m = prepareMrfta(b, true),
cases = mrftaCases(m);
assert.equal(cases.length, 2);
assert.notEqual(cases[0].key, cases[1].key);
assert.deepEqual(
cases.map((c) => [c.rate, c.cer]),
[
[20, '999'],
[10, '998'],
],
);
assert.equal(m.total, null);
const html = mrftaCaseHtml(m, cases[0].key);
assert.match(html, /Não é uma declaração autónoma/);
assert.ok(html.includes('Pendências do período'));
assert.deepEqual(
mrftaCases({ ...m, lines: [...m.lines].reverse() })
.map((c) => c.key)
.sort(),
cases.map((c) => c.key).sort(),
);
});
void test('missing CER references remain visible within each case sheet without presumed correspondence', () => {
const b = { ...book(), mrfta: input() };
b.mrfta.codes[0].reference = '';
const m = prepareMrfta(b, true);
assert.match(mrftaCases(m)[0].pending, /referência por corresponder/);
b.mrfta.codes = [];
assert.match(
mrftaCases(prepareMrfta(b, true))[0].pending,
/sem preenchimento presumido/,
);
});
void test('automatic result persists,recovers and reaches dossier with traceable parameters and history', () => {
const d = draft(),
b = recordMrfta({ ...book([payment('3650')]), mrfta: input() }, true);
d.modelo_a_snapshot = saveIrpcBook(d, b);
const recovered = JSON.parse(JSON.stringify(d)),
saved = irpcBook(recovered);
assert.equal(irpcCurrent(recovered, saved), true);
assert.equal(mrftaCurrent(saved, true), true);
assert.equal(saved.mrftaResult.model.interest.numerator, '15');
assert.equal(saved.mrftaResult.model.total.numerator, '3665');
assert.equal(saved.mrftaResult.model.automatic.rows[0].segments.length, 2);
const report = JSON.stringify(irpcReportRows(recovered));
assert.match(report, /Taxa anual:10%/);
assert.match(report, /QA parâmetro fictício/);
assert.match(report, /5dias\/365/);
assert.match(report, /metade por excesso/);
assert.ok(!report.includes('total_cent_half_up'));
assert.match(
JSON.stringify(
createProjectDossierReport(recovered, {
selectedSections: new Set(['finance']),
}),
),
/3665/,
);
assert.match(
mrftaHtml(saved, true),
/Juros calculados por parâmetros documentados/,
);
assert.match(
mrftaHtml(saved, true),
/simulação pelos pressupostos introduzidos, sem confirmação fiscal/,
);
assert.match(
mrftaHtml(saved, true),
/Códigos provisórios ou de simulação não são correspondências oficiais/,
);
assert.match(report, /sem validação por catálogo oficial/);
assert.match(
mrftaCases(saved.mrftaResult.model)[0].pending,
/Código provisório ou de simulação não é correspondência oficial/,
);
saved.mrfta.interest.automatic.annualRate = '';
assert.equal(mrftaCurrent(saved, true), false);
const changed = recordMrfta(saved, true);
assert.equal(changed.mrftaResult.model.interest, null);
assert.deepEqual(changed.mrftaHistory.at(-1), b.mrftaResult);
fs.mkdirSync('outputs/irpc-interest-cer', { recursive: true });
fs.writeFileSync(
'outputs/irpc-interest-cer/calculated.json',
JSON.stringify(recovered),
);
fs.writeFileSync(
'outputs/irpc-interest-cer/preparation.html',
mrftaHtml(b, true),
);
});
void test('new parameters reject malformed persisted values while the installed snapshot remains current', () => {
const d = JSON.parse(
fs.readFileSync(
new URL('./fixtures/irpc-mrfta-preserved.json', import.meta.url),
'utf8',
),
);
const b = irpcBook(d);
assert.equal(irpcCurrent(d, b), true);
assert.equal(mrftaCurrent(b, true), true);
const invalid = { ...book(), mrfta: input() };
invalid.mrfta.interest.automatic.annualRate = 10;
assert.throws(() => saveIrpcBook(draft(), invalid));
});
void test('two calculated incomes with different treaty/internal rates remain separate without an invented declaration total', () => {
const d = draft(),
first = {
...row(),
settlement: 'complete',
allocations: [{ paymentId: 'p', amount: '3650' }],
},
second = {
...row('two'),
specialRelief: 'claimed',
reliefPreparation: {
kind: 'treaty',
rule: 'ptMz12',
instrument: 'QA CDT PT-MZ e Protocolo',
article: '12',
country: 'PT',
residenceDocument: 'QA certificado Portugal2026',
validFrom: '2026-01-01',
validTo: '2026-12-31',
conditions: 'QA royalties qualificadas semEP',
evidence: 'QA contrato e residência',
facts: {
treatyResident: 'yes',
beneficialOwner: 'yes',
payerResidentMz: 'yes',
armLength: 'yes',
royaltyDefinition: 'yes',
},
},
};
const b = recordMrfta(
{
...recordIrpc(d, {
version: 1,
rows: [first, second],
payments: [payment('3650')],
}),
mrfta: {
...input(),
codes: [
...input().codes,
{
rowId: 'two',
cer: '998',
reference: 'QA CER998 fictício, não oficial',
},
],
},
},
true,
);
const m = b.mrftaResult.model;
assert.deepEqual(
b.result.rows.map((r) => r.rate),
[20, 10],
);
assert.equal(m.interest.numerator, '25');
assert.equal(m.total, null);
assert.deepEqual(
mrftaCases(m).map((c) => [c.rate, c.cer]),
[
[20, '999'],
[10, '998'],
],
);
assert.match(m.pending.join(' '), /taxas ou CER distintos/);
d.modelo_a_snapshot = saveIrpcBook(d, b);
fs.writeFileSync('outputs/irpc-interest-cer/mixed.json', JSON.stringify(d));
});