269 lines
9.5 KiB
JavaScript
269 lines
9.5 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,
|
|
irpcPreparationHtml,
|
|
} from '../../resources/js/lib/irpc-withholdings.ts';
|
|
import { newIrpcReliefPreparation } from '../../resources/js/lib/irpc-relief-preparation.ts';
|
|
import {
|
|
createProjectDossierReport,
|
|
createProjectDossierPrintHtml,
|
|
} 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 = 'dispensa') => ({
|
|
...newIrpcRow(id),
|
|
counterparty: 'QA sociedade',
|
|
documentRef: 'QA-' + id,
|
|
base: '100000',
|
|
kind: 'services',
|
|
recipientIrpc: 'yes',
|
|
payerIrpc: 'yes',
|
|
residence: 'resident',
|
|
mozambique: 'yes',
|
|
qualificationRef: 'QA contrato e cadastro do titular',
|
|
specialRelief: 'claimed',
|
|
eventsComplete: 'yes',
|
|
events: [{ kind: 'cost', date: '2026-10-02', reference: 'QA custo' }],
|
|
ordinary: {
|
|
...emptyIrpcOrdinaryFacts(),
|
|
nonDigital: 'yes',
|
|
reference: 'QA contrato de serviços',
|
|
},
|
|
reliefPreparation: {
|
|
...newIrpcReliefPreparation(),
|
|
rule: 'cirpc68d',
|
|
instrument: 'CIRPC Lei34/2007',
|
|
article: '68(d)',
|
|
validFrom: '2026-01-01',
|
|
validTo: '2026-12-31',
|
|
conditions: 'QA titular e rendimento sujeitos a IRPC',
|
|
evidence: 'QA cadastro e contrato existentes',
|
|
facts: { subjectToIrpc: 'yes' },
|
|
},
|
|
});
|
|
const treaty = (id = 'convention') => ({
|
|
...row(id),
|
|
kind: 'royalty',
|
|
residence: 'nonresident',
|
|
permanentEstablishment: 'no',
|
|
reliefPreparation: {
|
|
...row().reliefPreparation,
|
|
kind: 'treaty',
|
|
rule: 'ptMz12',
|
|
instrument: 'CDT PT-MZ RAR36/1992 e Protocolo2009',
|
|
article: '12',
|
|
country: 'PT',
|
|
residenceDocument: 'QA certificado fiscal Portugal 2026',
|
|
facts: {
|
|
treatyResident: 'yes',
|
|
beneficialOwner: 'yes',
|
|
payerResidentMz: 'yes',
|
|
armLength: 'yes',
|
|
royaltyDefinition: 'yes',
|
|
},
|
|
},
|
|
});
|
|
const calc = (r) =>
|
|
recordIrpc(draft(), { version: 1, rows: [r], payments: [] }).result.rows[0];
|
|
|
|
void test('68d dispenses resident and attributable EP services/equipment without exempting the income', () => {
|
|
for (const kind of ['services', 'equipment'])
|
|
for (const patch of [
|
|
{},
|
|
{
|
|
residence: 'nonresident',
|
|
permanentEstablishment: 'yes',
|
|
attributable: 'yes',
|
|
},
|
|
]) {
|
|
const v = calc({ ...row(), kind, ...patch });
|
|
assert.equal(v.status, 'calculated');
|
|
assert.equal(v.treatment, 'dispensed');
|
|
assert.equal(v.nature, 'on_account');
|
|
assert.equal(v.tax.numerator, '0');
|
|
assert.equal(v.outstanding.numerator, '0');
|
|
assert.match(v.reason, /não é isenção/);
|
|
}
|
|
});
|
|
void test('68f requires own-property management and absence of fiscal transparency', () => {
|
|
const r = row();
|
|
r.kind = 'rent';
|
|
r.ordinary.rentOwnPropertyManagement = 'yes';
|
|
r.reliefPreparation.rule = 'cirpc68f';
|
|
r.reliefPreparation.article = '68(f)';
|
|
r.reliefPreparation.facts.notTransparent = 'yes';
|
|
assert.equal(calc(r).treatment, 'dispensed');
|
|
for (const value of ['', 'no']) {
|
|
r.reliefPreparation.facts.notTransparent = value;
|
|
assert.equal(calc(r).tax, null);
|
|
}
|
|
r.reliefPreparation.facts.notTransparent = 'yes';
|
|
r.ordinary.rentOwnPropertyManagement = 'no';
|
|
assert.equal(calc(r).tax, null);
|
|
});
|
|
void test('definitive nonresident equipment is never dispensed under68d', () => {
|
|
for (const patch of [
|
|
{ permanentEstablishment: 'no' },
|
|
{ permanentEstablishment: 'yes', attributable: 'no' },
|
|
{ permanentEstablishment: '' },
|
|
])
|
|
assert.equal(
|
|
calc({
|
|
...row(),
|
|
kind: 'equipment',
|
|
residence: 'nonresident',
|
|
...patch,
|
|
}).tax,
|
|
null,
|
|
);
|
|
});
|
|
void test('Portugal royalties and qualified equipment use10% gross definitive limit', () => {
|
|
for (const kind of ['royalty', 'equipment']) {
|
|
const v = calc({ ...treaty(), kind });
|
|
assert.equal(v.status, 'calculated');
|
|
assert.equal(v.treatment, 'treaty');
|
|
assert.equal(v.rate, 10);
|
|
assert.equal(v.tax.numerator, '10000');
|
|
assert.equal(v.nature, 'definitive');
|
|
}
|
|
});
|
|
void test('missing,negative,expired or contradictory evidence remains indeterminate,never zero', () => {
|
|
const samples = [
|
|
(r) => (r.reliefPreparation.evidence = ''),
|
|
(r) => (r.reliefPreparation.residenceDocument = ''),
|
|
(r) => (r.reliefPreparation.validTo = '2026-09-30'),
|
|
(r) => (r.reliefPreparation.validFrom = '2026-02-30'),
|
|
(r) => (r.reliefPreparation.article = '7'),
|
|
(r) => (r.reliefPreparation.country = 'ZA'),
|
|
(r) => (r.permanentEstablishment = 'yes'),
|
|
(r) => (r.kind = 'digital_services'),
|
|
];
|
|
for (const fact of [
|
|
'treatyResident',
|
|
'beneficialOwner',
|
|
'payerResidentMz',
|
|
'armLength',
|
|
'royaltyDefinition',
|
|
])
|
|
for (const value of ['', 'no'])
|
|
samples.push((r) => (r.reliefPreparation.facts[fact] = value));
|
|
for (const mutate of samples) {
|
|
const r = treaty();
|
|
mutate(r);
|
|
const v = calc(r);
|
|
assert.equal(v.status, 'indeterminate');
|
|
assert.equal(v.tax, null);
|
|
assert.equal(v.rate, null);
|
|
}
|
|
});
|
|
void test('independent rows calculate beside insufficient information; save recover report export and history', () => {
|
|
const d = draft(),
|
|
prior = irpcBook(d),
|
|
insufficient = treaty('missing');
|
|
insufficient.reliefPreparation.residenceDocument = '';
|
|
const rent = row('property-dispensation');
|
|
rent.kind = 'rent';
|
|
rent.ordinary.rentOwnPropertyManagement = 'yes';
|
|
rent.reliefPreparation.rule = 'cirpc68f';
|
|
rent.reliefPreparation.article = '68(f)';
|
|
rent.reliefPreparation.facts.notTransparent = 'yes';
|
|
const b = recordIrpc(d, {
|
|
...prior,
|
|
rows: [...prior.rows, row(), rent, treaty(), insufficient],
|
|
});
|
|
d.modelo_a_snapshot = saveIrpcBook(d, b);
|
|
const recovered = JSON.parse(JSON.stringify(d)),
|
|
book = irpcBook(recovered);
|
|
assert.deepEqual(book, b);
|
|
assert.equal(irpcCurrent(recovered, book), true);
|
|
assert.deepEqual(book.history.at(-1), prior.result);
|
|
assert.deepEqual(
|
|
book.result.rows.slice(-3).map((r) => r.status),
|
|
['calculated', 'calculated', 'indeterminate'],
|
|
);
|
|
const report = irpcReportRows(recovered).slice(-3);
|
|
assert.match(report[0][2], /Dispensa de retenção/);
|
|
assert.match(report[1][2], /Limite convencional/);
|
|
assert.match(report[2][2], /Indeterminado/);
|
|
assert.match(report[1][3], /QA certificado fiscal Portugal 2026/);
|
|
assert.match(irpcPreparationHtml(recovered), /10000/);
|
|
book.rows.at(-2).reliefPreparation.facts.beneficialOwner = '';
|
|
assert.equal(irpcCurrent(recovered, book), false);
|
|
assert.equal(recordIrpc(recovered, book).result.rows.at(-2).tax, null);
|
|
fs.mkdirSync('outputs/irpc-reliefs', { recursive: true });
|
|
fs.writeFileSync(
|
|
'outputs/irpc-reliefs/calculated.json',
|
|
JSON.stringify(recovered),
|
|
);
|
|
fs.writeFileSync(
|
|
'outputs/irpc-reliefs/preparation.html',
|
|
irpcPreparationHtml(recovered),
|
|
);
|
|
});
|
|
void test('dispensa with payment attached requires reconciliation and retains the original proof', () => {
|
|
const r = row();
|
|
r.allocations = [{ paymentId: 'p', amount: '100' }];
|
|
const p = {
|
|
id: 'p',
|
|
kind: 'remittance',
|
|
reference: 'QA prova',
|
|
date: '2026-10-20',
|
|
amount: '100',
|
|
currency: 'MZN',
|
|
};
|
|
const b = recordIrpc(draft(), { version: 1, rows: [r], payments: [p] });
|
|
assert.equal(b.result.rows[0].status, 'indeterminate');
|
|
assert.deepEqual(b.payments, [p]);
|
|
});
|
|
void test('unsupported relief and malformed persisted conditions are not accepted as a benefit', () => {
|
|
const r = row();
|
|
r.reliefPreparation.rule = '';
|
|
assert.equal(calc(r).tax, null);
|
|
r.reliefPreparation.facts.subjectToIrpc = 'no';
|
|
assert.equal(calc(r).tax, null);
|
|
r.reliefPreparation.facts.subjectToIrpc = 42;
|
|
assert.throws(() =>
|
|
saveIrpcBook(draft(), { version: 1, rows: [r], payments: [] }),
|
|
);
|
|
});
|
|
void test('financial dossier includes each IRPC result, evidence and insufficient information', async () => {
|
|
const d = draft(),
|
|
missing = treaty('missing');
|
|
missing.reliefPreparation.residenceDocument = '';
|
|
d.modelo_a_snapshot = saveIrpcBook(
|
|
d,
|
|
recordIrpc(d, {
|
|
version: 1,
|
|
rows: [row(), treaty(), missing],
|
|
payments: [],
|
|
}),
|
|
);
|
|
const report = createProjectDossierReport(d, {
|
|
selectedSections: new Set(['finance']),
|
|
}),
|
|
text = JSON.stringify(report);
|
|
for (const label of [
|
|
'Dispensa de retenção',
|
|
'Limite convencional',
|
|
'Indeterminado',
|
|
'QA certificado fiscal Portugal 2026',
|
|
])
|
|
assert.ok(text.includes(label), label);
|
|
const html = await createProjectDossierPrintHtml(report);
|
|
assert.match(html, /Dispensa de retenção/);
|
|
assert.match(html, /prova de residência/);
|
|
});
|