302 lines
11 KiB
JavaScript
302 lines
11 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,
|
|
prepareMrfta,
|
|
recordMrfta,
|
|
mrftaCurrent,
|
|
mrftaHtml,
|
|
} from '../../resources/js/lib/irpc-mrfta.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 row = (id = 'principal') => ({
|
|
...newIrpcRow(id),
|
|
counterparty: 'QA contraparte',
|
|
documentRef: 'QA-' + id,
|
|
base: '100000',
|
|
kind: 'royalty',
|
|
recipientIrpc: 'yes',
|
|
payerIrpc: 'yes',
|
|
residence: 'nonresident',
|
|
permanentEstablishment: 'no',
|
|
mozambique: 'yes',
|
|
qualificationRef: 'QA contrato e residência',
|
|
specialRelief: 'none',
|
|
eventsComplete: 'yes',
|
|
events: [{ kind: 'cost', date: '2026-10-02', reference: 'QA custo' }],
|
|
settlement: 'complete',
|
|
settlementRef: 'QA reconciliação',
|
|
allocations: [{ paymentId: 'p', amount: '3000' }],
|
|
ordinary: {
|
|
...emptyIrpcOrdinaryFacts(),
|
|
nonDigital: 'yes',
|
|
reference: 'QA royalties não digitais',
|
|
},
|
|
});
|
|
const input = () => ({
|
|
...newMrftaInput(),
|
|
period: '2026-10',
|
|
declaration: 'initial',
|
|
timing: 'late',
|
|
nuit: '999999999',
|
|
name: 'QA sujeito passivo',
|
|
commercialName: 'QA',
|
|
taxArea: 'QA área fiscal',
|
|
address: 'QA domicílio',
|
|
contact: 'qa@example.invalid',
|
|
completeness: 'yes',
|
|
completenessRef: 'QA conferência do período e livros',
|
|
otherRubrics: 'none',
|
|
otherRubricsRef:
|
|
'QA ausência documentada de IRPS/tributação autónoma/outros valores',
|
|
codes: [
|
|
{
|
|
rowId: 'principal',
|
|
cer: '999',
|
|
reference: 'QA CER fictício, apenas teste',
|
|
},
|
|
],
|
|
interest: {
|
|
...newMrftaInput().interest,
|
|
mode: 'assessment',
|
|
period: '2026-10',
|
|
amount: '500',
|
|
date: '2026-11-25',
|
|
reference: 'QA liquidação de juros compensatórios',
|
|
},
|
|
});
|
|
function book(rows = [row()]) {
|
|
return {
|
|
...recordIrpc(draft(), {
|
|
version: 1,
|
|
rows,
|
|
payments: [
|
|
{
|
|
id: 'p',
|
|
kind: 'remittance',
|
|
reference: 'QA entrega IRPC',
|
|
date: '2026-11-15',
|
|
amount: '3000',
|
|
currency: 'MZN',
|
|
},
|
|
],
|
|
}),
|
|
mrfta: input(),
|
|
};
|
|
}
|
|
|
|
void test('MRFTA fills one period with principal, documented interest, payment and balance separately', () => {
|
|
const b = book(),
|
|
m = prepareMrfta(b, true);
|
|
assert.deepEqual(m.pending, []);
|
|
assert.equal(m.principal.numerator, '20000');
|
|
assert.equal(m.paid.numerator, '3000');
|
|
assert.equal(m.interest.numerator, '500');
|
|
assert.equal(m.total.numerator, '17500');
|
|
assert.equal(m.lines[0].rate, 20);
|
|
assert.equal(m.lines[0].cer, '999');
|
|
assert.match(m.interestReason, /transcritos/);
|
|
});
|
|
void test('unknown MAIBOR2026 never becomes zero or MIMO; independent principal remains calculated', () => {
|
|
const b = book();
|
|
b.mrfta.interest.mode = '';
|
|
const m = prepareMrfta(b, true);
|
|
assert.equal(m.interest, null);
|
|
assert.equal(m.total, null);
|
|
assert.equal(m.principal.numerator, '20000');
|
|
assert.equal(b.result.rows[0].status, 'calculated');
|
|
assert.match(m.pending.join(' '), /MAIBOR2026/);
|
|
});
|
|
void test('insufficient CER or contradictory rate per rubric blocks total without losing rows', () => {
|
|
const b = book();
|
|
b.mrfta.codes = [];
|
|
let m = prepareMrfta(b, true);
|
|
assert.equal(m.total, null);
|
|
assert.equal(m.lines.length, 1);
|
|
assert.match(m.pending.join(' '), /CER/);
|
|
const other = structuredClone(b.result.rows[0]);
|
|
other.id = 'other';
|
|
other.rate = 10;
|
|
other.tax = { numerator: '10000', denominator: '1' };
|
|
b.result.rows.push(other);
|
|
b.result.inputs.push({ ...row('other'), allocations: [] });
|
|
b.mrfta.codes = [
|
|
{ rowId: 'principal', cer: '999', reference: 'QA ref' },
|
|
{ rowId: 'other', cer: '999', reference: 'QA ref' },
|
|
];
|
|
m = prepareMrfta(b, true);
|
|
assert.equal(m.lines.length, 2);
|
|
assert.match(m.pending.join(' '), /taxas ou CER distintos/);
|
|
assert.equal(m.total, null);
|
|
});
|
|
void test('other months,suffered withholdings and documented dispensas stay outside payable rows', () => {
|
|
const b = book();
|
|
for (const [id, patch, original] of [
|
|
['other-month', { period: '2026-11' }, {}],
|
|
['suffered', {}, { direction: 'received' }],
|
|
[
|
|
'dispensed',
|
|
{
|
|
treatment: 'dispensed',
|
|
tax: { numerator: '0', denominator: '1' },
|
|
},
|
|
{},
|
|
],
|
|
]) {
|
|
b.result.rows.push({ ...b.result.rows[0], id, ...patch });
|
|
b.result.inputs.push({ ...row(id), ...original });
|
|
}
|
|
const m = prepareMrfta(b, true);
|
|
assert.equal(m.lines.length, 1);
|
|
assert.equal(m.excluded.length, 3);
|
|
assert.equal(m.principal.numerator, '20000');
|
|
assert.equal(m.total.numerator, '17500');
|
|
});
|
|
void test('unsupported digital and indeterminate cases retain explicit gaps without guessed rubric', () => {
|
|
const b = book();
|
|
b.result.inputs[0].kind = 'digital_services';
|
|
let m = prepareMrfta(b, true);
|
|
assert.equal(m.lines.length, 0);
|
|
assert.equal(m.total, null);
|
|
assert.match(m.pending.join(' '), /digital/);
|
|
b.result.inputs[0].kind = 'royalty';
|
|
b.result.rows[0].status = 'indeterminate';
|
|
b.result.rows[0].tax = null;
|
|
m = prepareMrfta(b, true);
|
|
assert.equal(m.total, null);
|
|
assert.match(m.pending.join(' '), /indeterminado/);
|
|
});
|
|
void test('timely remittance only yields zero interest with complete proof and explicit applicable deadline', () => {
|
|
const r = row();
|
|
r.base = '15000';
|
|
const b = book([r]);
|
|
b.mrfta.interest = {
|
|
...b.mrfta.interest,
|
|
mode: 'timely',
|
|
period: '2026-10',
|
|
dueDate: '2026-11-20',
|
|
dueReference: 'QA prazo documentado e calendário',
|
|
};
|
|
assert.equal(prepareMrfta(b, true).interest.numerator, '0');
|
|
assert.equal(prepareMrfta(b, true).total.numerator, '0');
|
|
b.mrfta.interest.dueDate = '2026-11-10';
|
|
assert.equal(prepareMrfta(b, true).interest, null);
|
|
b.mrfta.interest.dueDate = '2026-11-20';
|
|
b.result.rows[0].settled = null;
|
|
assert.equal(prepareMrfta(b, true).interest, null);
|
|
});
|
|
void test('assessment cannot leak into another period or accept negative amount and missing reference', () => {
|
|
const b = book();
|
|
for (const patch of [
|
|
{ period: '2026-11' },
|
|
{ amount: '-10' },
|
|
{ reference: '' },
|
|
{ date: '2026-02-30' },
|
|
]) {
|
|
const copy = structuredClone(b);
|
|
Object.assign(copy.mrfta.interest, patch);
|
|
assert.equal(prepareMrfta(copy, true).interest, null);
|
|
assert.equal(prepareMrfta(copy, true).total, null);
|
|
}
|
|
});
|
|
void test('MRFTA save,recover,history and dossier preserve existing tax maps and freeze evidence', () => {
|
|
const d = draft(),
|
|
prior = irpcBook(d),
|
|
original = JSON.parse(d.modelo_a_snapshot);
|
|
let b = book();
|
|
b.history = [prior.result];
|
|
b = recordMrfta(b, true);
|
|
d.modelo_a_snapshot = saveIrpcBook(d, b);
|
|
const recovered = JSON.parse(JSON.stringify(d)),
|
|
saved = irpcBook(recovered);
|
|
assert.deepEqual(saved, b);
|
|
assert.equal(irpcCurrent(recovered, saved), true);
|
|
assert.equal(mrftaCurrent(saved, true), true);
|
|
for (const [k, v] of Object.entries(original))
|
|
if (k !== 'irpcWithholdings')
|
|
assert.deepEqual(JSON.parse(recovered.modelo_a_snapshot)[k], v, k);
|
|
const report = irpcReportRows(recovered);
|
|
assert.match(JSON.stringify(report), /17500/);
|
|
assert.match(
|
|
JSON.stringify(
|
|
createProjectDossierReport(recovered, {
|
|
selectedSections: new Set(['finance']),
|
|
}),
|
|
),
|
|
/MRFTA/,
|
|
);
|
|
assert.match(mrftaHtml(saved, true), /Quadro7/);
|
|
assert.match(mrftaHtml(saved, true), /QA CER fictício/);
|
|
const before = structuredClone(saved.mrftaResult);
|
|
saved.mrfta.interest.amount = '600';
|
|
assert.equal(mrftaCurrent(saved, true), false);
|
|
assert.throws(() => mrftaHtml(saved, true));
|
|
const next = recordMrfta(saved, true);
|
|
assert.deepEqual(next.mrftaHistory.at(-1), before);
|
|
assert.equal(next.mrftaResult.model.total.numerator, '17600');
|
|
fs.mkdirSync('outputs/irpc-mrfta', { recursive: true });
|
|
fs.writeFileSync(
|
|
'outputs/irpc-mrfta/calculated.json',
|
|
JSON.stringify(recovered),
|
|
);
|
|
fs.writeFileSync('outputs/irpc-mrfta/preparation.html', mrftaHtml(b, true));
|
|
});
|
|
void test('malformed persisted MRFTA rejects recovery without replacing originals', () => {
|
|
const b = book();
|
|
b.mrfta.codes.push({ ...b.mrfta.codes[0] });
|
|
assert.throws(() => saveIrpcBook(draft(), b));
|
|
b.mrfta.codes = [];
|
|
b.mrfta.interest.amount = 5;
|
|
assert.throws(() => saveIrpcBook(draft(), b));
|
|
});
|
|
void test('export escapes identities,documents and assessment references', () => {
|
|
const b = book();
|
|
b.mrfta.name = '<script>QA</script>';
|
|
b.mrfta.interest.reference = '<img src=x onerror=QA>';
|
|
const html = mrftaHtml(recordMrfta(b, true), true);
|
|
assert.ok(!html.includes('<script>QA'));
|
|
assert.ok(html.includes('<script>'));
|
|
assert.ok(html.includes('<img'));
|
|
});
|
|
void test('other tax rubrics cannot silently become zero and partial rows remain available', () => {
|
|
const b = book();
|
|
b.mrfta.otherRubrics = 'pending';
|
|
const m = prepareMrfta(b, true);
|
|
assert.equal(m.total, null);
|
|
assert.equal(m.principal.numerator, '20000');
|
|
assert.match(m.pending.join(' '), /IRPS,tributação autónoma/);
|
|
assert.throws(() => mrftaHtml(recordMrfta(b, false), false));
|
|
});
|
|
void test('same rubric rate and documented CER aggregate without losing document references', () => {
|
|
const other = { ...row('other'), allocations: [], settlement: 'none' },
|
|
b = book([row(), other]);
|
|
b.mrfta.codes.push({
|
|
rowId: 'other',
|
|
cer: '999',
|
|
reference: 'QA segundo documento de CER fictício',
|
|
});
|
|
const m = prepareMrfta(b, true);
|
|
assert.equal(m.lines.length, 1);
|
|
assert.equal(m.lines[0].base.numerator, '200000');
|
|
assert.equal(m.principal.numerator, '40000');
|
|
assert.equal(m.total.numerator, '37500');
|
|
assert.equal(m.lines[0].references.length, 2);
|
|
assert.equal(m.lines[0].documents.length, 2);
|
|
});
|