435 lines
14 KiB
JavaScript
435 lines
14 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import {
|
|
irpcBook,
|
|
newIrpcRow,
|
|
recordIrpc,
|
|
saveIrpcBook,
|
|
reuseIrpcSources,
|
|
irpcCurrent,
|
|
irpcReportRows,
|
|
irpcPreparationHtml,
|
|
} from '../../resources/js/lib/irpc-withholdings.ts';
|
|
import {
|
|
acquisitionBook,
|
|
newAcquisition,
|
|
saveAcquisitionBook,
|
|
} from '../../resources/js/lib/iva-acquisitions.ts';
|
|
const seed = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('./fixtures/ispc-residual-preserved.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
const draft = () => ({ ...structuredClone(seed), fiscal_component: 'irpc' });
|
|
const row = (id = 'digital') => ({
|
|
...newIrpcRow(id),
|
|
counterparty: 'SYNTHETIC QA fornecedor não residente',
|
|
documentRef: 'SYNTHETIC-' + id,
|
|
base: '100000',
|
|
kind: 'digital_services',
|
|
recipientIrpc: 'yes',
|
|
payerIrpc: 'yes',
|
|
residence: 'nonresident',
|
|
permanentEstablishment: 'no',
|
|
mozambique: 'yes',
|
|
qualificationRef:
|
|
'SYNTHETIC QA: SaaS utilizado em Moçambique, devedor nacional,sem EP,Lei12/2025 arts5(5/7)/62(5)b',
|
|
specialRelief: 'none',
|
|
eventsComplete: 'yes',
|
|
events: [
|
|
{ kind: 'cost', date: '2026-10-02', reference: 'SYNTHETIC custo' },
|
|
{
|
|
kind: 'payment',
|
|
date: '2026-11-03',
|
|
reference: 'SYNTHETIC pagamento do rendimento',
|
|
},
|
|
],
|
|
settlement: 'none',
|
|
settlementRef: 'SYNTHETIC sem entrega',
|
|
});
|
|
const book = (r = row()) => ({ version: 1, rows: [r], payments: [] });
|
|
void test('positive digital goods/services and e-money have exact 10 percent and first applicable cost event', () => {
|
|
for (const kind of [
|
|
'digital_goods',
|
|
'digital_services',
|
|
'emoney_commission',
|
|
]) {
|
|
const r = recordIrpc(draft(), book({ ...row(), kind })).result.rows[0];
|
|
assert.equal(r.status, 'calculated');
|
|
assert.deepEqual(r.tax, { numerator: '10000', denominator: '1' });
|
|
assert.equal(r.eventDate, '2026-10-02');
|
|
assert.equal(r.nature, 'definitive');
|
|
assert.equal(r.period, '2026-10');
|
|
}
|
|
});
|
|
void test('unknown territory, recipient, payer, EP, event completeness, relief never become zero or nonapplicable', () => {
|
|
for (const patch of [
|
|
{ mozambique: '' },
|
|
{ recipientIrpc: '' },
|
|
{ payerIrpc: '' },
|
|
{ permanentEstablishment: '' },
|
|
{ eventsComplete: '' },
|
|
{ specialRelief: '' },
|
|
]) {
|
|
const r = recordIrpc(draft(), book({ ...row(), ...patch })).result
|
|
.rows[0];
|
|
assert.equal(r.status, 'indeterminate');
|
|
assert.equal(r.tax, null);
|
|
}
|
|
});
|
|
void test('resident and income attributable to EP preserve concrete 68d mapping gap; no 10 or20 automatic', () => {
|
|
for (const patch of [
|
|
{ residence: 'resident' },
|
|
{ permanentEstablishment: 'yes', attributable: 'yes' },
|
|
]) {
|
|
const r = recordIrpc(draft(), book({ ...row(), ...patch })).result
|
|
.rows[0];
|
|
assert.equal(r.tax, null);
|
|
assert.match(r.reason, /68/);
|
|
}
|
|
});
|
|
void test('nonresident with EP and explicitly nonattributable income has definitive positive branch', () =>
|
|
assert.equal(
|
|
recordIrpc(
|
|
draft(),
|
|
book({
|
|
...row(),
|
|
permanentEstablishment: 'yes',
|
|
attributable: 'no',
|
|
}),
|
|
).result.rows[0].status,
|
|
'calculated',
|
|
));
|
|
void test('ordinary and exceptional income need specific amended rule; no legacy2002 rates', () => {
|
|
for (const kind of [
|
|
'royalty',
|
|
'rent',
|
|
'capital',
|
|
'equipment',
|
|
'services',
|
|
'other',
|
|
]) {
|
|
const r = recordIrpc(draft(), book({ ...row(), kind })).result.rows[0];
|
|
assert.equal(r.tax, null);
|
|
assert.match(r.reason, /20\/2022/);
|
|
}
|
|
});
|
|
void test('event before2026 is not moved to later payment; impossible dates rejected', () => {
|
|
for (const date of ['2025-12-31', '2026-02-30']) {
|
|
const r = row();
|
|
r.events[0].date = date;
|
|
assert.equal(
|
|
recordIrpc(draft(), book(r)).result.rows[0].status,
|
|
'indeterminate',
|
|
);
|
|
}
|
|
});
|
|
void test('foreign amount and missing base remain indeterminate, explicit zero is distinct', () => {
|
|
for (const patch of [{ currency: 'USD' }, { base: '' }, { base: '-1' }])
|
|
assert.equal(
|
|
recordIrpc(draft(), book({ ...row(), ...patch })).result.rows[0]
|
|
.tax,
|
|
null,
|
|
);
|
|
assert.deepEqual(
|
|
recordIrpc(draft(), book({ ...row(), base: '0' })).result.rows[0].tax,
|
|
{ numerator: '0', denominator: '1' },
|
|
);
|
|
});
|
|
void test('partial remittance is separate from tax and recipient payment cannot settle tax', () => {
|
|
const r = {
|
|
...row(),
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p1', amount: '6000' }],
|
|
};
|
|
const b = {
|
|
...book(r),
|
|
payments: [
|
|
{
|
|
id: 'p1',
|
|
kind: 'remittance',
|
|
reference: 'SYNTHETIC-AT1',
|
|
date: '2026-10-20',
|
|
amount: '6000',
|
|
currency: 'MZN',
|
|
},
|
|
],
|
|
};
|
|
const out = recordIrpc(draft(), b).result.rows[0];
|
|
assert.deepEqual(out.settled, { numerator: '6000', denominator: '1' });
|
|
assert.deepEqual(out.outstanding, { numerator: '4000', denominator: '1' });
|
|
b.payments[0].kind = 'beneficiary';
|
|
assert.equal(recordIrpc(draft(), b).result.rows[0].status, 'indeterminate');
|
|
});
|
|
void test('received withholding needs matching proof; no yearly credit or buyer debt generated', () => {
|
|
const r = {
|
|
...row(),
|
|
direction: 'received',
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p1', amount: '1500' }],
|
|
base: '20000',
|
|
};
|
|
const b = {
|
|
...book(r),
|
|
payments: [
|
|
{
|
|
id: 'p1',
|
|
kind: 'withholding_proof',
|
|
reference: 'SYNTHETIC-PROOF1',
|
|
date: '2026-10-20',
|
|
amount: '1500',
|
|
currency: 'MZN',
|
|
},
|
|
],
|
|
};
|
|
const out = recordIrpc(draft(), b).result.rows[0];
|
|
assert.equal(out.direction, 'received');
|
|
assert.deepEqual(out.outstanding, { numerator: '500', denominator: '1' });
|
|
assert.equal('annualCredit' in out, false);
|
|
});
|
|
void test('unknown settlement stays null and no-payment with allocations is contradictory', () => {
|
|
let r = recordIrpc(draft(), book({ ...row(), settlement: '' })).result
|
|
.rows[0];
|
|
assert.equal(r.settled, null);
|
|
assert.equal(r.tax.numerator, '10000');
|
|
r = recordIrpc(
|
|
draft(),
|
|
book({ ...row(), allocations: [{ paymentId: 'p', amount: '1' }] }),
|
|
).result.rows[0];
|
|
assert.equal(r.status, 'indeterminate');
|
|
});
|
|
void test('same remittance can be split but never consumed twice, including other opportunities', () => {
|
|
const p = {
|
|
id: 'p',
|
|
kind: 'remittance',
|
|
reference: 'SYNTHETIC-SHARED',
|
|
date: '2026-10-20',
|
|
amount: '10000',
|
|
currency: 'MZN',
|
|
},
|
|
a = {
|
|
...row('a'),
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p', amount: '5000' }],
|
|
},
|
|
b = {
|
|
...row('b'),
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p', amount: '5000' }],
|
|
};
|
|
let bk = { version: 1, rows: [a, b], payments: [p] };
|
|
assert.ok(
|
|
recordIrpc(draft(), bk).result.rows.every(
|
|
(r) => r.status === 'calculated',
|
|
),
|
|
);
|
|
bk.rows[1].allocations[0].amount = '6000';
|
|
assert.ok(
|
|
recordIrpc(draft(), bk).result.rows.every(
|
|
(r) => r.status === 'indeterminate',
|
|
),
|
|
);
|
|
const d = draft();
|
|
d.modelo_a_snapshot = saveIrpcBook(d, {
|
|
version: 1,
|
|
rows: [a],
|
|
payments: [p],
|
|
});
|
|
d.context_opportunity = 'another';
|
|
assert.equal(
|
|
recordIrpc(d, {
|
|
version: 1,
|
|
rows: [{ ...b, allocations: [{ paymentId: 'p', amount: '6000' }] }],
|
|
payments: [p],
|
|
}).result.rows[0].status,
|
|
'indeterminate',
|
|
);
|
|
});
|
|
void test('duplicate document, duplicate proof ID or duplicate reference do not double totals', () => {
|
|
const r = row(),
|
|
d = draft();
|
|
assert.ok(
|
|
recordIrpc(d, {
|
|
version: 1,
|
|
rows: [r, { ...r, id: 'x' }],
|
|
payments: [],
|
|
}).result.rows.every((r) => r.status === 'indeterminate'),
|
|
);
|
|
const p = {
|
|
id: 'p',
|
|
kind: 'remittance',
|
|
reference: 'SYNTHETIC-SAME',
|
|
date: '2026-10-20',
|
|
amount: '1',
|
|
currency: 'MZN',
|
|
};
|
|
assert.throws(
|
|
() => saveIrpcBook(d, { ...book(), payments: [p, { ...p, id: 'p2' }] }),
|
|
/já registado/,
|
|
);
|
|
});
|
|
void test('reuse is idempotent, inherits supplier/document/amount, preserves deliberate edits and detects source delta', () => {
|
|
const d = draft(),
|
|
ab = acquisitionBook(d),
|
|
a = {
|
|
...newAcquisition('L5-supplier', '2026-10', 'MZN'),
|
|
supplier: 'SYNTHETIC saved supplier',
|
|
documentRef: 'SYNTHETIC invoice',
|
|
documentLine: '1',
|
|
net: '100000',
|
|
description: 'SaaS',
|
|
};
|
|
d.modelo_a_snapshot = saveAcquisitionBook(d, {
|
|
...ab,
|
|
rows: [...ab.rows, a],
|
|
});
|
|
let b = reuseIrpcSources(d, irpcBook(d));
|
|
assert.equal(reuseIrpcSources(d, b).rows.length, b.rows.length);
|
|
let r = b.rows.find((r) => r.source?.id === 'acquisition:L5-supplier');
|
|
assert.equal(r.counterparty, a.supplier);
|
|
assert.equal(r.documentRef, 'SYNTHETIC invoice / linha 1');
|
|
r.base = '90000';
|
|
assert.equal(
|
|
reuseIrpcSources(d, b).rows.find((v) => v.id === r.id).base,
|
|
'90000',
|
|
);
|
|
b = {
|
|
version: 1,
|
|
rows: [{ ...row(), id: r.id, source: r.source }],
|
|
payments: [],
|
|
};
|
|
const current = acquisitionBook(d);
|
|
d.modelo_a_snapshot = saveAcquisitionBook(d, {
|
|
...current,
|
|
rows: current.rows.map((v) =>
|
|
v.id === a.id ? { ...v, net: '120000' } : v,
|
|
),
|
|
});
|
|
assert.match(recordIrpc(d, b).result.rows[0].reason, /origem alterado/);
|
|
});
|
|
void test('save/recover preserves all preexisting maps, history, context separation and frozen report', () => {
|
|
const d = draft(),
|
|
prior = JSON.parse(d.modelo_a_snapshot),
|
|
b = recordIrpc(d, book());
|
|
d.modelo_a_snapshot = saveIrpcBook(d, b);
|
|
const now = JSON.parse(d.modelo_a_snapshot);
|
|
for (const [k, v] of Object.entries(prior)) assert.deepEqual(now[k], v);
|
|
assert.deepEqual(irpcBook(d).result, b.result);
|
|
assert.equal(irpcCurrent(d, b), true);
|
|
b.rows[0].base = '200000';
|
|
assert.equal(irpcCurrent(d, b), false);
|
|
assert.match(
|
|
irpcReportRows({ ...d, modelo_a_snapshot: saveIrpcBook(d, b) })[0][2],
|
|
/10000.00/,
|
|
);
|
|
assert.equal(recordIrpc(d, b).history.length, 1);
|
|
d.context_opportunity = 'other';
|
|
assert.equal(irpcBook(d).rows.length, 0);
|
|
});
|
|
void test('malformed store remains recoverable as explicit error, never overwritten blank', () => {
|
|
assert.throws(() =>
|
|
irpcBook({
|
|
modelo_a_snapshot: '{"version":1,"books":{},"irpcWithholdings":[]}',
|
|
}),
|
|
);
|
|
const d = draft(),
|
|
b = book();
|
|
b.rows[0].events = [{ kind: 'madeup', date: '2026-10-01', reference: 'x' }];
|
|
assert.throws(() => saveIrpcBook(d, b));
|
|
});
|
|
void test('export escapes private inputs, retains period/nature/gaps and excludes submission claims', () => {
|
|
const d = draft(),
|
|
r = { ...row(), counterparty: '<script>alert(1)</script>' };
|
|
d.modelo_a_snapshot = saveIrpcBook(d, recordIrpc(d, book(r)));
|
|
const html = irpcPreparationHtml(d);
|
|
assert.ok(!html.includes('<script>'));
|
|
assert.match(html, /<script>/);
|
|
assert.match(html, /sem submissão/);
|
|
assert.match(html, /CER/);
|
|
assert.match(html, /2026-10/);
|
|
assert.match(html, /Retenção definitiva/);
|
|
});
|
|
if (process.env.IRPC_FIXTURE_OUTPUT) {
|
|
const d = draft(),
|
|
b = recordIrpc(d, {
|
|
version: 1,
|
|
rows: [
|
|
{
|
|
...row(),
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p1', amount: '6000' }],
|
|
},
|
|
{
|
|
...row('commission'),
|
|
base: '20000',
|
|
kind: 'emoney_commission',
|
|
direction: 'received',
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p2', amount: '1500' }],
|
|
},
|
|
],
|
|
payments: [
|
|
{
|
|
id: 'p1',
|
|
kind: 'remittance',
|
|
reference: 'SYNTHETIC-AT1',
|
|
date: '2026-10-20',
|
|
amount: '6000',
|
|
currency: 'MZN',
|
|
},
|
|
{
|
|
id: 'p2',
|
|
kind: 'withholding_proof',
|
|
reference: 'SYNTHETIC-PROOF1',
|
|
date: '2026-10-20',
|
|
amount: '1500',
|
|
currency: 'MZN',
|
|
},
|
|
],
|
|
});
|
|
d.modelo_a_snapshot = saveIrpcBook(d, b);
|
|
fs.writeFileSync(
|
|
process.env.IRPC_FIXTURE_OUTPUT,
|
|
JSON.stringify(d, null, 2) + '\n',
|
|
);
|
|
}
|
|
|
|
void test('allocation changes in another opportunity mark previous result stale only for shared payments', () => {
|
|
const d = draft(),
|
|
p = {
|
|
id: 'p',
|
|
kind: 'remittance',
|
|
reference: 'SYNTHETIC-SHARED',
|
|
date: '2026-10-20',
|
|
amount: '10000',
|
|
currency: 'MZN',
|
|
},
|
|
r = {
|
|
...row(),
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p', amount: '6000' }],
|
|
},
|
|
b = recordIrpc(d, { ...book(r), payments: [p] });
|
|
d.modelo_a_snapshot = saveIrpcBook(d, b);
|
|
assert.equal(irpcCurrent(d, b), true);
|
|
const ctx = d.context_opportunity;
|
|
d.context_opportunity = 'other';
|
|
d.modelo_a_snapshot = saveIrpcBook(d, book(row('unrelated')));
|
|
d.context_opportunity = ctx;
|
|
assert.equal(irpcCurrent(d, b), true);
|
|
d.context_opportunity = 'other';
|
|
d.modelo_a_snapshot = saveIrpcBook(d, {
|
|
...book({
|
|
...row('shared'),
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p', amount: '1000' }],
|
|
}),
|
|
payments: [p],
|
|
});
|
|
d.context_opportunity = ctx;
|
|
assert.equal(irpcCurrent(d, b), false);
|
|
assert.match(irpcReportRows(d)[0][3], /Histórico/);
|
|
});
|