494 lines
18 KiB
JavaScript
494 lines
18 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import fs from 'node:fs';
|
||
import {
|
||
normalBook,
|
||
normalCurrent,
|
||
calculateNormal,
|
||
recordNormal,
|
||
saveNormalBook,
|
||
normalHtml,
|
||
normalFinancialSource,
|
||
} from '../../resources/js/lib/irpc-normal.ts';
|
||
import {
|
||
newNormalFormPreparation,
|
||
reuseDirectFormAllocations,
|
||
normalFormRepresentation,
|
||
prepareNormalForms,
|
||
} from '../../resources/js/lib/irpc-normal-form-preparation.ts';
|
||
import { fraction } from '../../resources/js/lib/fiscal-rational.ts';
|
||
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||
const seed = () =>
|
||
JSON.parse(
|
||
fs.readFileSync(
|
||
new URL(
|
||
'./fixtures/irpc-periods-fx-preserved.json',
|
||
import.meta.url,
|
||
),
|
||
'utf8',
|
||
),
|
||
),
|
||
val = (f) => (f ? Number(f.numerator) / Number(f.denominator) : null);
|
||
function setup() {
|
||
const d = seed(),
|
||
b = normalBook(d);
|
||
b.forms = {
|
||
...newNormalFormPreparation(),
|
||
edition: 'accepted_pdf_and_m20a1',
|
||
reference:
|
||
'QA PDFM22/Q10–12 e M20A.1/Q4;edições inventariadas distintas',
|
||
completenessReference:
|
||
'QA contas completas da oportunidade/sujeito no período;sem outros custos ou proveitos omitidos',
|
||
allocations: [
|
||
...reuseDirectFormAllocations(normalFinancialSource(d)),
|
||
{
|
||
id: 'revenue',
|
||
sourceCode: 'revenue',
|
||
account: '7.2',
|
||
amount: '6000',
|
||
reference:
|
||
'QA prestação de serviços documentada,sem vendas de bens',
|
||
},
|
||
{
|
||
id: 'opex',
|
||
sourceCode: 'operating_expenses',
|
||
account: '6.3',
|
||
amount: '600',
|
||
reference:
|
||
'QA fornecimentos e serviços de terceiros completos no período',
|
||
},
|
||
],
|
||
incomeTax: {
|
||
state: 'absent',
|
||
amount: '',
|
||
placement: '',
|
||
reference:
|
||
'QA ausência documentada de imposto contabilizado;não inferir gasto da colecta',
|
||
},
|
||
reducedRate: {
|
||
state: 'absent',
|
||
reference:
|
||
'QA sujeito/período sem parcela de imposto a taxa reduzida',
|
||
},
|
||
};
|
||
return { d, b };
|
||
}
|
||
const field = (m, code) =>
|
||
m.formPreparation.fields.find((f) => f.code === code);
|
||
void test('exact representation keeps signedfraction,approximation,zero,missing and parentheses distinct without fiscal rounding', () => {
|
||
for (const f of [
|
||
fraction(1n, 3n),
|
||
fraction(-1n, 300n),
|
||
fraction(123456789012345678901n, 100n),
|
||
]) {
|
||
const r = normalFormRepresentation(f, 'whole');
|
||
assert.equal(r.exact, f.numerator + '/' + f.denominator + ' MZN');
|
||
assert.equal(r.represented, null);
|
||
assert.equal(r.ready, false);
|
||
}
|
||
assert.match(normalFormRepresentation(fraction(1n, 3n)).display, /≈/);
|
||
assert.equal(normalFormRepresentation(fraction(0n)).represented, '0');
|
||
assert.equal(normalFormRepresentation(null).display, 'Indeterminado');
|
||
assert.equal(
|
||
normalFormRepresentation(fraction(-20n), 'whole', true).represented,
|
||
'(20)',
|
||
);
|
||
assert.match(
|
||
normalFormRepresentation(fraction(1n, 2n)).reason,
|
||
/exacto ao cêntimo/,
|
||
);
|
||
});
|
||
void test('optional9 prepares M20 classifications/netresult and M22 mappedadjustments while preserving all existing totals', () => {
|
||
const { d, b } = setup(),
|
||
before = normalBook(d).result.model,
|
||
m = calculateNormal(d, b);
|
||
assert.equal(m.version, 'IRPC-normal-adjustments/9');
|
||
for (const k of [
|
||
'accountingProfit',
|
||
'taxableProfit',
|
||
'taxableBase',
|
||
'colecta',
|
||
])
|
||
assert.deepEqual(m[k], before[k]);
|
||
assert.equal(val(m.formPreparation.netResult), 3120);
|
||
assert.equal(val(field(m, '200').value), 3120);
|
||
assert.equal(field(m, '200').transcriptionReady, false);
|
||
assert.equal(val(field(m, '213').value), 0);
|
||
assert.equal(val(field(m, '206').value), 10);
|
||
assert.equal(val(field(m, '238').value), 3130);
|
||
assert.equal(val(field(m, '258').value), 0);
|
||
assert.equal(m.formPreparation.reconciliation.matches, true);
|
||
assert.equal(field(m, '259').application, 'not_applicable');
|
||
assert.equal(field(m, '260').transcriptionReady, true);
|
||
assert.equal(val(field(m, '302').value), 1001.6);
|
||
assert.equal(field(m, '302').representation.ready, true);
|
||
assert.equal(field(m, '309–316').value, null);
|
||
assert.equal(m.assessment.finalBalance.payable, null);
|
||
});
|
||
void test('missingorcontradictory booktax blocks only netresult and its fieldreconciliation; knownnormalprofit/base/colecta preserved', () => {
|
||
for (const tax of [
|
||
{ state: '', amount: '', placement: '', reference: '' },
|
||
{
|
||
state: 'absent',
|
||
amount: '320',
|
||
placement: 'outside',
|
||
reference: 'QA',
|
||
},
|
||
{
|
||
state: 'known',
|
||
amount: '320',
|
||
placement: 'included',
|
||
reference: 'QA',
|
||
},
|
||
]) {
|
||
const { d, b } = setup();
|
||
b.forms.incomeTax = tax;
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(m.formPreparation.netResult, null);
|
||
assert.equal(field(m, '200').value, null);
|
||
assert.equal(val(field(m, '260').value), 3130);
|
||
assert.equal(val(field(m, '300').value), 1001.6);
|
||
assert.equal(val(m.colecta), 1001.6);
|
||
}
|
||
});
|
||
void test('incomeTaxalreadyincluded reconciles account8.5 and mappedtax without subtracting it again', () => {
|
||
const { d, b } = setup();
|
||
b.forms.incomeTax = {
|
||
state: 'known',
|
||
amount: '320',
|
||
placement: 'included',
|
||
reference: 'QA imposto reconhecido no plano',
|
||
};
|
||
b.forms.allocations.find((a) => a.id === 'opex').amount = '280';
|
||
b.forms.allocations.push({
|
||
id: 'tax',
|
||
sourceCode: 'operating_expenses',
|
||
account: '8.5',
|
||
amount: '320',
|
||
reference: 'QA imposto já no custo de origem',
|
||
});
|
||
b.schedules.rows.push({
|
||
id: 'tax',
|
||
rule: 'tax_own',
|
||
amount: '320',
|
||
sourceCode: 'operating_expenses',
|
||
document: 'QA-tax',
|
||
line: '1',
|
||
reference: 'QA imposto sobre lucro já incluído',
|
||
fiscalYear: '',
|
||
conditions: 'yes',
|
||
});
|
||
Object.assign(b.profit.topics.expenseCorrections, {
|
||
state: 'known',
|
||
amount: '320',
|
||
reference: 'QA corresponde ao imposto contabilizado já no resultado',
|
||
});
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(val(m.formPreparation.netResult), 3120);
|
||
assert.equal(val(field(m, '213').value), 0);
|
||
assert.equal(val(m.taxableProfit), 3450);
|
||
assert.equal(field(m, '238').value, null);
|
||
assert.equal(m.formPreparation.reconciliation.matches, false);
|
||
assert.ok(m.formPreparation.unmapped.some((e) => e.reason.includes('213')));
|
||
});
|
||
void test('incomplete revenueclassification does not erase directM20 fields or independentM22 calculations', () => {
|
||
const { d, b } = setup();
|
||
b.forms.allocations.find((a) => a.id === 'revenue').amount = '5999';
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(
|
||
m.formPreparation.m20.find((f) => f.account === '7.2').value,
|
||
null,
|
||
);
|
||
assert.equal(
|
||
val(m.formPreparation.m20.find((f) => f.account === '6.5').value),
|
||
60,
|
||
);
|
||
assert.equal(m.formPreparation.netResult, null);
|
||
assert.equal(val(m.taxableProfit), 3130);
|
||
assert.equal(val(field(m, '300').value), 1001.6);
|
||
});
|
||
void test('fractional M20 allocations remain exact; no truncation or rounding changes source or net/taxable totals', () => {
|
||
const { d, b } = setup();
|
||
b.forms.allocations.find((a) => a.id === 'opex').amount = '300.50';
|
||
b.forms.allocations.push({
|
||
id: 'other',
|
||
sourceCode: 'operating_expenses',
|
||
account: '6.8',
|
||
amount: '299.50',
|
||
reference: 'QA gastos operacionais restantes documentados',
|
||
});
|
||
const m = calculateNormal(d, b),
|
||
f = m.formPreparation.m20.find((f) => f.account === '6.3');
|
||
assert.deepEqual(f.value, fraction(601n, 2n));
|
||
assert.equal(f.representation.ready, false);
|
||
assert.equal(f.representation.represented, null);
|
||
assert.equal(val(m.formPreparation.netResult), 3120);
|
||
assert.equal(val(m.colecta), 1001.6);
|
||
});
|
||
void test('lossuses positiveM22 lossfield and M20parentheses; no loss in profitfield and no negative taxablebase', () => {
|
||
const { d, b } = setup(),
|
||
p = JSON.parse(d.business_plan_snapshot);
|
||
p.plan.products[0].unitPrice = '20';
|
||
b.schedules.currentLossReference =
|
||
'QA perda fiscal corrente reconciliada no sujeito/período,sem reporte automático';
|
||
d.business_plan_snapshot = JSON.stringify(p);
|
||
b.forms.allocations.find((a) => a.id === 'revenue').amount = '1200';
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(val(m.taxableProfit), -1670);
|
||
assert.equal(val(field(m, '259').value), 1670);
|
||
assert.equal(field(m, '260').application, 'not_applicable');
|
||
assert.equal(m.formPreparation.netRepresentation.represented, '(1680)');
|
||
assert.equal(val(m.taxableBase), 0);
|
||
assert.equal(val(m.colecta), 0);
|
||
});
|
||
void test('unmapped priorquota deduction keeps demonstrated effect and existing260/278/300; no guessedblankfield', () => {
|
||
const { d, b } = setup(),
|
||
p = JSON.parse(d.business_plan_snapshot);
|
||
p.plan.assetDepreciation = '48';
|
||
d.business_plan_snapshot = JSON.stringify(p);
|
||
Object.assign(b.depreciation.assets[0], {
|
||
accounting: '24',
|
||
start: '2025-01-01',
|
||
previousAccounting: '120',
|
||
previousFiscal: '80',
|
||
priorAvailable: '40',
|
||
});
|
||
b.forms.allocations.find((a) => a.sourceCode === 'depreciation').amount =
|
||
'24';
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(val(m.depreciation.adjustment), -26);
|
||
assert.equal(val(m.colecta), 1001.6);
|
||
assert.ok(
|
||
m.formPreparation.unmapped.some((e) =>
|
||
e.origin.startsWith('Mapa de amortizações'),
|
||
),
|
||
);
|
||
assert.equal(field(m, '258').value, null);
|
||
assert.equal(field(m, '238').value, null);
|
||
assert.equal(val(field(m, '260').value), 3130);
|
||
assert.equal(
|
||
m.formPreparation.entries.find((e) =>
|
||
e.origin.startsWith('Mapa de amortizações'),
|
||
).code,
|
||
null,
|
||
);
|
||
});
|
||
void test('reducedrate unknown or knowninL7 blocks only302,notnormal300 and not finalL8 assumptions', () => {
|
||
for (const state of ['', 'known']) {
|
||
const { d, b } = setup();
|
||
b.forms.reducedRate.state = state;
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(field(m, '302').value, null);
|
||
assert.equal(val(field(m, '300').value), 1001.6);
|
||
assert.equal(val(m.colecta), 1001.6);
|
||
assert.equal(m.assessment.finalBalance.recoverable, null);
|
||
}
|
||
});
|
||
void test('save/recover/report preserves8history and originalplan and never claims officialrounding or crosseditiontransfer', () => {
|
||
const { d, b } = setup(),
|
||
original = d.business_plan_snapshot,
|
||
old = structuredClone(b.result);
|
||
assert.equal(normalCurrent(d, normalBook(d)), true);
|
||
d.modelo_a_snapshot = saveNormalBook(
|
||
d,
|
||
recordNormal(d, b, '2026-10-26T10:00:00+02:00'),
|
||
);
|
||
const back = normalBook(JSON.parse(JSON.stringify(d)));
|
||
assert.equal(normalCurrent(d, back), true);
|
||
assert.deepEqual(back.history.at(-1), old);
|
||
assert.equal(d.business_plan_snapshot, original);
|
||
assert.match(normalHtml(d), /campo213/);
|
||
assert.match(normalHtml(d), /transporte automático M20/);
|
||
assert.match(normalHtml(d), /exacto ao cêntimo/);
|
||
assert.match(
|
||
JSON.stringify(
|
||
createProjectDossierReport(d, {
|
||
selectedSections: new Set(['finance']),
|
||
}),
|
||
),
|
||
/campo302/,
|
||
);
|
||
back.forms.allocations[0].reference += 'changed';
|
||
assert.equal(normalCurrent(d, back), false);
|
||
});
|
||
void test('fivenewcases write privateAPI proof inputs and reports without rerunning accepted functionalsuites', () => {
|
||
const cases = {
|
||
mapped: setup(),
|
||
fractional: setup(),
|
||
missing: setup(),
|
||
unmapped: setup(),
|
||
booktax: setup(),
|
||
};
|
||
cases.booktax.b.forms.incomeTax = {
|
||
state: 'known',
|
||
amount: '320',
|
||
placement: 'outside',
|
||
reference:
|
||
'QA gasto contabilístico exterior ao plano;campo próprio pendente',
|
||
};
|
||
cases.fractional.b.forms.allocations.find((a) => a.id === 'opex').amount =
|
||
'300.50';
|
||
cases.fractional.b.forms.allocations.push({
|
||
id: 'other',
|
||
sourceCode: 'operating_expenses',
|
||
account: '6.8',
|
||
amount: '299.50',
|
||
reference: 'QA parcela contabilística com centavos',
|
||
});
|
||
cases.missing.b.forms.incomeTax = {
|
||
state: '',
|
||
amount: '',
|
||
placement: '',
|
||
reference: '',
|
||
};
|
||
const u = cases.unmapped,
|
||
p = JSON.parse(u.d.business_plan_snapshot);
|
||
p.plan.assetDepreciation = '48';
|
||
u.d.business_plan_snapshot = JSON.stringify(p);
|
||
Object.assign(u.b.depreciation.assets[0], {
|
||
accounting: '24',
|
||
start: '2025-01-01',
|
||
previousAccounting: '120',
|
||
previousFiscal: '80',
|
||
priorAvailable: '40',
|
||
});
|
||
u.b.forms.allocations.find((a) => a.sourceCode === 'depreciation').amount =
|
||
'24';
|
||
fs.mkdirSync('outputs/irpc-l6-values-forms', { recursive: true });
|
||
for (const [name, { d, b }] of Object.entries(cases)) {
|
||
d.modelo_a_snapshot = saveNormalBook(
|
||
d,
|
||
recordNormal(d, b, '2026-10-26T10:00:00+02:00'),
|
||
);
|
||
fs.writeFileSync(
|
||
'outputs/irpc-l6-values-forms/' + name + '.json',
|
||
JSON.stringify(d),
|
||
);
|
||
fs.writeFileSync(
|
||
'outputs/irpc-l6-values-forms/' + name + '.html',
|
||
normalHtml(d),
|
||
);
|
||
}
|
||
});
|
||
|
||
void test('newlyidentified includedtax mismatch blocks only dependent213/reconciliation without revising demonstratedtax', () => {
|
||
const { d, b } = setup();
|
||
b.forms.incomeTax = {
|
||
state: 'known',
|
||
amount: '320',
|
||
placement: 'included',
|
||
reference: 'QA imposto reconhecido no plano',
|
||
};
|
||
b.forms.allocations.find((a) => a.id === 'opex').amount = '280';
|
||
b.forms.allocations.push({
|
||
id: 'tax',
|
||
sourceCode: 'operating_expenses',
|
||
account: '8.5',
|
||
amount: '320',
|
||
reference: 'QA imposto reconhecido no custo da origem',
|
||
});
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(val(m.colecta), 1001.6);
|
||
assert.equal(val(m.formPreparation.netResult), 3120);
|
||
assert.equal(val(field(m, '213').value), 0);
|
||
assert.equal(field(m, '238').value, null);
|
||
assert.equal(val(field(m, '260').value), 3130);
|
||
assert.ok(m.formPreparation.pending.some((s) => s.includes('tax_own')));
|
||
});
|
||
void test('positive and negative asseteffects remain separate; netdepreciation is never all assignedto206', () => {
|
||
const { d, b } = setup(),
|
||
a = structuredClone(b.depreciation.assets[0]);
|
||
Object.assign(b.depreciation.assets[0], { cost: '60', accounting: '36' });
|
||
Object.assign(a, {
|
||
id: 'other',
|
||
assetKey: 'other',
|
||
document: 'QA-other-asset',
|
||
accounting: '24',
|
||
start: '2025-01-01',
|
||
previousAccounting: '120',
|
||
previousFiscal: '80',
|
||
priorAvailable: '40',
|
||
});
|
||
b.depreciation.assets.push(a);
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(val(m.depreciation.adjustment), 4);
|
||
assert.equal(val(field(m, '206').value), 30);
|
||
assert.equal(val(m.taxableProfit), 3124);
|
||
assert.equal(m.formPreparation.unmapped.length, 1);
|
||
assert.equal(val(m.formPreparation.unmapped[0].value), -26);
|
||
assert.equal(field(m, '238').value, null);
|
||
});
|
||
void test('acceptedcostproof feeds independentM22 labels without applying printedpercentages again', () => {
|
||
const d = JSON.parse(
|
||
fs.readFileSync(
|
||
new URL(
|
||
'./fixtures/irpc-costs-preserved.json',
|
||
import.meta.url,
|
||
),
|
||
'utf8',
|
||
),
|
||
),
|
||
b = normalBook(d),
|
||
m = b.result.model,
|
||
{ b: input } = setup();
|
||
const mapped = prepareNormalForms(input.forms, m, normalFinancialSource(d));
|
||
for (const [rule, code] of [
|
||
['allowance', '219'],
|
||
['representation', '220'],
|
||
['advertising', '221'],
|
||
['credit', '209'],
|
||
]) {
|
||
const row = m.costs.rows.find((r) => r.rule === rule);
|
||
assert.ok(row);
|
||
const sum = m.costs.rows
|
||
.filter((r) => r.rule === rule)
|
||
.reduce((s, r) => s + val(r.adjustment), 0);
|
||
const value = mapped.fields.find((f) => f.code === code).value;
|
||
if (code !== '209') assert.equal(val(value), sum);
|
||
else
|
||
assert.equal(
|
||
val(value),
|
||
m.costs.rows
|
||
.filter((r) =>
|
||
['credit', 'stock', 'litigation'].includes(r.rule),
|
||
)
|
||
.reduce((s, r) => s + val(r.adjustment), 0),
|
||
);
|
||
}
|
||
assert.deepEqual(b.result.model, m);
|
||
});
|
||
|
||
void test('absence of booktax cannot erase previouslyidentified ownincomeTax cost; independentknowncolecta remains', () => {
|
||
const { d, b } = setup();
|
||
b.schedules.rows.push({
|
||
id: 'tax',
|
||
rule: 'tax_own',
|
||
amount: '320',
|
||
sourceCode: 'operating_expenses',
|
||
document: 'QA-tax',
|
||
line: '1',
|
||
reference: 'QA imposto já incluído',
|
||
fiscalYear: '',
|
||
conditions: 'yes',
|
||
});
|
||
Object.assign(b.profit.topics.expenseCorrections, {
|
||
state: 'known',
|
||
amount: '320',
|
||
reference: 'QA mapa do imposto próprio',
|
||
});
|
||
b.forms.incomeTax = {
|
||
state: 'absent',
|
||
amount: '',
|
||
placement: '',
|
||
reference: 'QA ausência a conferir',
|
||
};
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(m.formPreparation.netResult, null);
|
||
assert.ok(
|
||
m.formPreparation.pending.some((s) =>
|
||
s.includes('contradiz imposto próprio'),
|
||
),
|
||
);
|
||
assert.equal(val(field(m, '300').value), 1104);
|
||
});
|