553 lines
19 KiB
JavaScript
553 lines
19 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import {
|
|
irpcBook,
|
|
newIrpcRow,
|
|
recordIrpc,
|
|
saveIrpcBook,
|
|
emptyIrpcOrdinaryFacts,
|
|
irpcCurrent,
|
|
irpcReportRows,
|
|
irpcPreparationHtml,
|
|
} from '../../resources/js/lib/irpc-withholdings.ts';
|
|
import { irpcFxSources, updateIrpcFx } from '../../resources/js/lib/irpc-fx.ts';
|
|
import {
|
|
emptyFxSelection,
|
|
selectFx,
|
|
} from '../../resources/js/lib/iva-fx-selection.ts';
|
|
import {
|
|
currencyEventBook,
|
|
newCurrencyEvent,
|
|
saveCurrencyEventBook,
|
|
} from '../../resources/js/lib/iva-currency-events.ts';
|
|
const accepted = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('./fixtures/irpc-l5-preserved.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
const draft = () => structuredClone(accepted);
|
|
const row = (id = 'normal') => ({
|
|
...newIrpcRow(id),
|
|
counterparty: 'SYNTHETIC IRPC counterparty',
|
|
documentRef: 'SYNTHETIC-' + id,
|
|
base: '100000',
|
|
kind: 'royalty',
|
|
recipientIrpc: 'yes',
|
|
payerIrpc: 'yes',
|
|
residence: 'nonresident',
|
|
permanentEstablishment: 'no',
|
|
mozambique: 'yes',
|
|
qualificationRef:
|
|
'SYNTHETIC classified non-digital income,IRPCpayer/recipient,Mozambique source;residence andEP explicitly recorded',
|
|
specialRelief: 'none',
|
|
eventsComplete: 'yes',
|
|
events: [
|
|
{
|
|
kind: 'cost',
|
|
date: '2026-10-02',
|
|
reference: 'SYNTHETIC applicable cost',
|
|
},
|
|
{ kind: 'payment', date: '2026-11-02', reference: 'SYNTHETIC payment' },
|
|
],
|
|
settlement: 'none',
|
|
settlementRef: 'SYNTHETIC no remittance',
|
|
ordinary: {
|
|
...emptyIrpcOrdinaryFacts(),
|
|
nonDigital: 'yes',
|
|
reference: 'SYNTHETIC exact category facts;CIRPC62/67',
|
|
},
|
|
});
|
|
const book = (r = row()) => ({ version: 1, rows: [r], payments: [] });
|
|
const calc = (r) => recordIrpc(draft(), book(r)).result.rows[0];
|
|
const fx = (patch = {}) => {
|
|
const s = {
|
|
...emptyFxSelection(),
|
|
nature: 'commercial',
|
|
submissionDate: '2026-10-20',
|
|
bulletinDate: '2026-10-20',
|
|
bulletinRef: 'SYNTHETIC fictional BM bulletin for QA',
|
|
source: 'SYNTHETIC archived QA exemplar',
|
|
issuer: 'Banco de Moçambique',
|
|
sourceCurrency: 'USD',
|
|
direction: 'mzn_per_unit',
|
|
mean: '64',
|
|
...patch,
|
|
};
|
|
return { ...s, applied: selectFx(s) };
|
|
};
|
|
void test('ordinary nonresident royalties equipment services and nonlisted capitals use20%,not digital10%', () => {
|
|
for (const kind of ['royalty', 'equipment', 'services', 'capital']) {
|
|
const r = row();
|
|
r.kind = kind;
|
|
if (kind === 'capital') r.ordinary.capitalType = 'unlisted_interest';
|
|
const v = calc(r);
|
|
assert.equal(v.status, 'calculated');
|
|
assert.equal(v.rate, 20);
|
|
assert.equal(v.tax.numerator, '20000');
|
|
assert.equal(v.nature, 'definitive');
|
|
}
|
|
});
|
|
void test('resident royalties and ordinary rent are on account;predial nonresident is also on account', () => {
|
|
for (const patch of [
|
|
{ kind: 'royalty', residence: 'resident' },
|
|
{ kind: 'rent', residence: 'resident' },
|
|
{ kind: 'rent', residence: 'nonresident' },
|
|
]) {
|
|
const r = { ...row(), ...patch };
|
|
r.ordinary.rentOwnPropertyManagement = 'no';
|
|
const v = calc(r);
|
|
assert.equal(v.status, 'calculated');
|
|
assert.equal(v.nature, 'on_account');
|
|
assert.equal(v.tax.numerator, '20000');
|
|
assert.equal('annualCredit' in v, false);
|
|
}
|
|
});
|
|
void test('identified dispensa branches and unknown facts block only their own income line', () => {
|
|
const d = draft(),
|
|
rs = [
|
|
{
|
|
...row('resident-services'),
|
|
residence: 'resident',
|
|
kind: 'services',
|
|
},
|
|
{ ...row('property'), kind: 'rent' },
|
|
{
|
|
...row('capital'),
|
|
residence: 'resident',
|
|
kind: 'capital',
|
|
ordinary: {
|
|
...row().ordinary,
|
|
capitalType: 'unlisted_interest',
|
|
},
|
|
},
|
|
row('independent'),
|
|
];
|
|
const out = recordIrpc(d, { version: 1, rows: rs, payments: [] }).result
|
|
.rows;
|
|
assert.ok(
|
|
out
|
|
.slice(0, 3)
|
|
.every((v) => v.status === 'indeterminate' && v.tax === null),
|
|
);
|
|
assert.equal(out[3].tax.numerator, '20000');
|
|
});
|
|
void test('special categories62(4)a-c retain10% with exact facts,not generic20% or genericinstallation10%', () => {
|
|
for (const kind of [
|
|
'telecom',
|
|
'international_transport',
|
|
'connected_installation',
|
|
'aircraft_maintenance_charter',
|
|
'rural_public_electrification',
|
|
'maritime_fishing_cabotage',
|
|
]) {
|
|
const r = { ...row(), kind };
|
|
r.ordinary.specialFacts = 'yes';
|
|
const v = calc(r);
|
|
assert.equal(v.rate, 10);
|
|
assert.equal(v.tax.numerator, '10000');
|
|
assert.match(v.reason, /62\(4\)/);
|
|
}
|
|
const unknown = { ...row(), kind: 'connected_installation' };
|
|
assert.equal(calc(unknown).tax, null);
|
|
const generic = { ...row(), kind: 'services' };
|
|
assert.equal(calc(generic).rate, 20);
|
|
});
|
|
void test('special62(4) entity condition is distinct from digital noEP/nonattributable branch', () => {
|
|
for (const patch of [
|
|
{ residence: 'resident' },
|
|
{ permanentEstablishment: 'yes', attributable: 'no' },
|
|
]) {
|
|
const r = { ...row(), kind: 'telecom', ...patch };
|
|
r.ordinary.specialFacts = 'yes';
|
|
assert.equal(calc(r).tax, null);
|
|
}
|
|
});
|
|
void test('listed debt and other BMsecurities remain distinct;liquidity repos are not assumed', () => {
|
|
for (const [capitalType, rate] of [
|
|
['listed_debt_mz', 20],
|
|
['listed_other_mz', 10],
|
|
]) {
|
|
const r = { ...row(), kind: 'capital' };
|
|
r.ordinary.capitalType = capitalType;
|
|
r.ordinary.specialFacts = 'yes';
|
|
assert.equal(calc(r).rate, rate);
|
|
}
|
|
const resident = { ...row(), kind: 'capital', residence: 'resident' };
|
|
resident.ordinary.capitalType = 'listed_debt_mz';
|
|
resident.ordinary.capitalSeatAndManagement = 'yes';
|
|
assert.equal(calc(resident).rate, 20);
|
|
assert.equal(calc(resident).nature, 'definitive');
|
|
resident.ordinary.capitalType = 'liquidity_repo';
|
|
assert.equal(calc(resident).rate, 20);
|
|
assert.equal(calc(resident).nature, 'definitive');
|
|
});
|
|
void test('expired2025 agricultural measures do not extend into2026 foreign services/interest', () => {
|
|
for (const kind of ['services', 'capital']) {
|
|
const r = {
|
|
...row(),
|
|
kind,
|
|
description:
|
|
'SYNTHETIC nationalagriculturalcompany / external agricultural financing',
|
|
};
|
|
r.ordinary.capitalType = 'unlisted_interest';
|
|
const v = calc(r);
|
|
assert.equal(v.rate, 20);
|
|
assert.match(v.reason, /31\/12\/2025/);
|
|
}
|
|
const r = row();
|
|
r.events[0].date = '2025-12-31';
|
|
assert.equal(calc(r).tax, null);
|
|
});
|
|
void test('applied existing BMmean converts base, preserves original and keeps event/date separate', () => {
|
|
const r = { ...row(), currency: 'USD', base: '1000', fxSelection: fx() };
|
|
const v = calc(r);
|
|
assert.equal(v.base.numerator, '64000');
|
|
assert.equal(v.tax.numerator, '12800');
|
|
assert.equal(v.period, '2026-10');
|
|
assert.equal(v.eventDate, '2026-10-02');
|
|
assert.equal(v.conversion.date, '2026-10-20');
|
|
assert.equal(v.conversion.originalAmount, '1000');
|
|
assert.equal(v.conversion.originalCurrency, 'USD');
|
|
assert.equal(r.base, '1000');
|
|
assert.equal(r.currency, 'USD');
|
|
});
|
|
void test('purchase/sale perspective,inverse quote and fiscalexception use deliveredselector', () => {
|
|
const opposite = fx({
|
|
nature: 'purchase',
|
|
relation: 'opposite',
|
|
bulletinPerspective: 'SYNTHETIC issuer',
|
|
buy: '63',
|
|
sell: '65',
|
|
});
|
|
let r = { ...row(), currency: 'USD', base: '100', fxSelection: opposite };
|
|
assert.equal(calc(r).base.numerator, '6500');
|
|
const inverse = fx({ direction: 'units_per_mzn', mean: '0.02' });
|
|
r.fxSelection = inverse;
|
|
assert.equal(calc(r).base.numerator, '5000');
|
|
r.fxSelection = fx({
|
|
mode: 'exception',
|
|
exceptionRef: 'SYNTHETIC identifiedfiscalprovision',
|
|
exceptionScope: 'SYNTHETIC retention scope',
|
|
exceptionDate: '2026-10-02',
|
|
exceptionRate: '66',
|
|
});
|
|
assert.equal(calc(r).base.numerator, '6600');
|
|
assert.equal(calc(r).conversion.column, 'exception');
|
|
});
|
|
void test('unapplied,stale,missingmean,wrongcurrency or undated FX remains indeterminate', () => {
|
|
const samples = [
|
|
undefined,
|
|
{ ...fx(), applied: undefined },
|
|
{ ...fx(), mean: '65' },
|
|
{ ...fx(), sourceCurrency: 'EUR' },
|
|
{ ...fx(), bulletinDate: '2026-10-19' },
|
|
];
|
|
for (const f of samples) {
|
|
const v = calc({
|
|
...row(),
|
|
currency: 'USD',
|
|
base: '1000',
|
|
fxSelection: f,
|
|
});
|
|
assert.equal(v.tax, null);
|
|
assert.match(v.reason, /conversão/);
|
|
}
|
|
});
|
|
void test('existing saved FX selections are linked,reused,and changed source marks only linked results stale', () => {
|
|
const d = draft(),
|
|
cb = currencyEventBook(d),
|
|
s = fx(),
|
|
f = {
|
|
...newCurrencyEvent('shared-fx', 'MZN'),
|
|
fxSelection: s,
|
|
sourceCurrency: 'USD',
|
|
fxDirection: s.direction,
|
|
fxRate: s.applied.rate,
|
|
fxDate: s.applied.date,
|
|
fxSource: s.applied.source,
|
|
description: 'SYNTHETIC existing FX',
|
|
};
|
|
d.modelo_a_snapshot = saveCurrencyEventBook(d, {
|
|
...cb,
|
|
rows: [...cb.rows, f],
|
|
});
|
|
const source = irpcFxSources(d).find((s) => s.id === f.id),
|
|
r = {
|
|
...row(),
|
|
currency: 'USD',
|
|
base: '1000',
|
|
fxSelection: source.selection,
|
|
fxLink: { id: source.id, fingerprint: source.fingerprint },
|
|
},
|
|
b = recordIrpc(d, book(r));
|
|
d.modelo_a_snapshot = saveIrpcBook(d, b);
|
|
assert.equal(irpcCurrent(d, b), true);
|
|
const current = currencyEventBook(d);
|
|
d.modelo_a_snapshot = saveCurrencyEventBook(d, {
|
|
...current,
|
|
rows: current.rows.map((v) =>
|
|
v.id === f.id
|
|
? { ...v, description: 'SYNTHETIC revisedsource' }
|
|
: v,
|
|
),
|
|
});
|
|
assert.equal(irpcCurrent(d, b), false);
|
|
assert.match(recordIrpc(d, b).result.rows[0].reason, /origem alterada/);
|
|
const m = recordIrpc(d, book(row('independentMZN')));
|
|
assert.equal(m.result.rows[0].tax.numerator, '20000');
|
|
});
|
|
void test('foreign withholding proof converts available amount once;allocations remainMZN', () => {
|
|
const r = {
|
|
...row(),
|
|
direction: 'received',
|
|
settlement: 'complete',
|
|
settlementRef: 'SYNTHETIC provedpartialwithholding',
|
|
allocations: [{ paymentId: 'p', amount: '640' }],
|
|
},
|
|
p = {
|
|
id: 'p',
|
|
kind: 'withholding_proof',
|
|
reference: 'SYNTHETIC-USDproof',
|
|
date: '2026-10-20',
|
|
currency: 'USD',
|
|
amount: '10',
|
|
fxSelection: fx(),
|
|
};
|
|
const d = draft(),
|
|
b = recordIrpc(d, { ...book(r), payments: [p] });
|
|
assert.equal(b.result.rows[0].settled.numerator, '640');
|
|
assert.equal(b.result.rows[0].outstanding.numerator, '19360');
|
|
assert.equal(b.result.paymentConversions.p.originalAmount, '10');
|
|
d.modelo_a_snapshot = saveIrpcBook(d, b);
|
|
assert.match(irpcReportRows(d)[0][3], /SYNTHETIC-USDproof/);
|
|
assert.match(irpcReportRows(d)[0][3], /Original: 10 USD/);
|
|
});
|
|
void test('foreign payment to beneficiary or purported ATremittance is never converted into taxproof', () => {
|
|
for (const kind of ['beneficiary', 'remittance']) {
|
|
const r = {
|
|
...row(),
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p', amount: '640' }],
|
|
},
|
|
p = {
|
|
id: 'p',
|
|
kind,
|
|
reference: 'SYNTHETIC-wrongpayment',
|
|
date: '2026-10-20',
|
|
currency: 'USD',
|
|
amount: '10',
|
|
fxSelection: fx(),
|
|
};
|
|
assert.equal(
|
|
recordIrpc(draft(), { ...book(r), payments: [p] }).result.rows[0]
|
|
.status,
|
|
'indeterminate',
|
|
);
|
|
}
|
|
});
|
|
void test('converted shared proof cannot exceed availableMZN or change FX between contexts', () => {
|
|
const d = draft(),
|
|
p = {
|
|
id: 'p',
|
|
kind: 'withholding_proof',
|
|
reference: 'SYNTHETIC-sharedUSD',
|
|
date: '2026-10-20',
|
|
currency: 'USD',
|
|
amount: '10',
|
|
fxSelection: fx(),
|
|
},
|
|
r = {
|
|
...row('one'),
|
|
direction: 'received',
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p', amount: '400' }],
|
|
};
|
|
d.modelo_a_snapshot = saveIrpcBook(
|
|
d,
|
|
recordIrpc(d, { ...book(r), payments: [p] }),
|
|
);
|
|
d.context_opportunity = 'other';
|
|
const other = {
|
|
...row('two'),
|
|
direction: 'received',
|
|
settlement: 'complete',
|
|
allocations: [{ paymentId: 'p', amount: '300' }],
|
|
};
|
|
assert.equal(
|
|
recordIrpc(d, { ...book(other), payments: [p] }).result.rows[0].status,
|
|
'indeterminate',
|
|
);
|
|
other.allocations[0].amount = '200';
|
|
assert.equal(
|
|
recordIrpc(d, { ...book(other), payments: [p] }).result.rows[0].status,
|
|
'calculated',
|
|
);
|
|
assert.equal(
|
|
recordIrpc(d, {
|
|
...book(other),
|
|
payments: [{ ...p, fxSelection: fx({ mean: '65' }) }],
|
|
}).result.rows[0].status,
|
|
'indeterminate',
|
|
);
|
|
});
|
|
void test('accepted v1MZN10% results remain current and bytepreserved without recomputation', () => {
|
|
const d = draft(),
|
|
b = irpcBook(d),
|
|
before = JSON.stringify(b);
|
|
assert.equal(irpcCurrent(d, b), true);
|
|
assert.equal(JSON.stringify(irpcBook(d)), before);
|
|
assert.deepEqual(
|
|
irpcBook({ ...d, modelo_a_snapshot: saveIrpcBook(d, b) }),
|
|
b,
|
|
);
|
|
});
|
|
void test('new normal/FX result preserves old maps and acceptedresult in history,report frozen after change', () => {
|
|
const d = draft(),
|
|
b = irpcBook(d),
|
|
old = structuredClone(b.result),
|
|
s = JSON.parse(d.modelo_a_snapshot);
|
|
const next = recordIrpc(d, {
|
|
...b,
|
|
rows: [
|
|
...b.rows,
|
|
{ ...row('fx'), currency: 'USD', base: '1000', fxSelection: fx() },
|
|
],
|
|
});
|
|
d.modelo_a_snapshot = saveIrpcBook(d, next);
|
|
for (const [k, v] of Object.entries(s))
|
|
if (k !== 'irpcWithholdings')
|
|
assert.deepEqual(JSON.parse(d.modelo_a_snapshot)[k], v);
|
|
assert.deepEqual(next.history.at(-1), old);
|
|
next.rows.at(-1).fxSelection.mean = '65';
|
|
assert.equal(irpcCurrent(d, next), false);
|
|
d.modelo_a_snapshot = saveIrpcBook(d, next);
|
|
const report = irpcReportRows(d).at(-1);
|
|
assert.match(report[2], /12800.00/);
|
|
assert.match(report[3], /Histórico/);
|
|
assert.match(report[3], /taxa: 64/);
|
|
});
|
|
void test('export keeps currencies,normal/special distinctions,payment refs and externalformalities separate', () => {
|
|
const d = draft(),
|
|
r = { ...row(), currency: 'USD', base: '1000', fxSelection: fx() };
|
|
d.modelo_a_snapshot = saveIrpcBook(d, recordIrpc(d, book(r)));
|
|
const h = irpcPreparationHtml(d);
|
|
assert.match(h, /Original: 1000 USD/);
|
|
assert.ok(!h.includes('selectionKey'));
|
|
assert.match(h, /20%/);
|
|
assert.match(h, /CER/);
|
|
assert.match(h, /sem submissão/);
|
|
assert.match(h, /2026-10/);
|
|
});
|
|
|
|
if (process.env.IRPC_GROUPED_FIXTURE_OUTPUT) {
|
|
const d = draft(),
|
|
old = irpcBook(d),
|
|
s = fx(),
|
|
f = {
|
|
...newCurrencyEvent('retention-fx', 'MZN'),
|
|
description: 'SYNTHETIC existing BMselection for retention',
|
|
fxSelection: s,
|
|
sourceCurrency: 'USD',
|
|
fxDirection: s.direction,
|
|
fxRate: s.applied.rate,
|
|
fxDate: s.applied.date,
|
|
fxSource: s.applied.source,
|
|
};
|
|
const cb = currencyEventBook(d);
|
|
d.modelo_a_snapshot = saveCurrencyEventBook(d, {
|
|
...cb,
|
|
rows: [...cb.rows, f],
|
|
});
|
|
const source = irpcFxSources(d).find((v) => v.id === f.id);
|
|
const foreign = {
|
|
...row('foreign-royalty'),
|
|
currency: 'USD',
|
|
base: '1000',
|
|
fxSelection: source.selection,
|
|
fxLink: { id: source.id, fingerprint: source.fingerprint },
|
|
settlement: 'complete',
|
|
settlementRef: 'SYNTHETIC IRPC remittance5000',
|
|
allocations: [{ paymentId: 'p3', amount: '5000' }],
|
|
},
|
|
special = {
|
|
...row('special-telecom'),
|
|
kind: 'telecom',
|
|
ordinary: { ...row().ordinary, specialFacts: 'yes' },
|
|
},
|
|
resident = {
|
|
...row('resident-royalty'),
|
|
residence: 'resident',
|
|
base: '50000',
|
|
},
|
|
received = {
|
|
...row('received-foreign-royalty'),
|
|
direction: 'received',
|
|
currency: 'USD',
|
|
base: '100',
|
|
fxSelection: s,
|
|
settlement: 'complete',
|
|
settlementRef: 'SYNTHETIC proof10USD converted640MZN',
|
|
allocations: [{ paymentId: 'p4', amount: '640' }],
|
|
};
|
|
const b = recordIrpc(d, {
|
|
...old,
|
|
rows: [...old.rows, foreign, special, resident, received],
|
|
payments: [
|
|
...old.payments,
|
|
{
|
|
id: 'p3',
|
|
kind: 'remittance',
|
|
reference: 'SYNTHETIC-IRPC-AT5000',
|
|
date: '2026-10-20',
|
|
amount: '5000',
|
|
currency: 'MZN',
|
|
},
|
|
{
|
|
id: 'p4',
|
|
kind: 'withholding_proof',
|
|
reference: 'SYNTHETIC-IRPC-USD10',
|
|
date: '2026-10-20',
|
|
amount: '10',
|
|
currency: 'USD',
|
|
fxSelection: s,
|
|
},
|
|
],
|
|
});
|
|
d.modelo_a_snapshot = saveIrpcBook(d, b);
|
|
fs.writeFileSync(
|
|
process.env.IRPC_GROUPED_FIXTURE_OUTPUT,
|
|
JSON.stringify(d, null, 2) + '\n',
|
|
);
|
|
}
|
|
|
|
void test('62(3) definitive capital needs exact seat/management facts and is not an annual credit', () => {
|
|
const r = { ...row(), residence: 'resident', kind: 'capital' };
|
|
r.ordinary.capitalType = 'listed_debt_mz';
|
|
assert.equal(calc(r).tax, null);
|
|
r.ordinary.capitalSeatAndManagement = 'yes';
|
|
assert.equal(calc(r).nature, 'definitive');
|
|
assert.equal(calc(r).rate, 20);
|
|
assert.equal('annualCredit' in calc(r), false);
|
|
});
|
|
|
|
void test('confirming unchanged FX preserves source link;deliberate input change detaches only that selection', () => {
|
|
const input = {
|
|
currency: 'USD',
|
|
fxSelection: fx(),
|
|
fxLink: { id: 'saved', fingerprint: 'original' },
|
|
};
|
|
const confirmed = {
|
|
...input.fxSelection,
|
|
applied: selectFx(input.fxSelection),
|
|
};
|
|
assert.deepEqual(updateIrpcFx(input, confirmed).fxLink, input.fxLink);
|
|
assert.equal(
|
|
updateIrpcFx(input, { ...input.fxSelection, mean: '65' }).fxLink,
|
|
undefined,
|
|
);
|
|
assert.equal(input.fxSelection.mean, '64');
|
|
assert.equal(input.fxLink.id, 'saved');
|
|
});
|