427 lines
14 KiB
JavaScript
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));
|
|
});
|