Files
Investidor-V2/tests/frontend/iva-operations.test.mjs
T

75 lines
9.3 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {closureFixture} from './fixtures/simulator-closure-fixture.mjs';
import {calculateSales,evaluateSalesOperation,newSalesOperation,operationTreatments,recordSales,reuseSales,salesBook,salesCurrent,salesReportRows,saveSalesBook,splitSales} from '../../resources/js/lib/iva-operations.ts';
import {acquisitionBook,calculateAcquisitions,newAcquisition,saveAcquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
export function operationsFixture(){
const draft=closureFixture().draft;draft.project_title='SYNTHETIC QA — L1 operações IVA';draft.province_id='cf5e81f1-ffbc-535d-b1b6-b44b445c4421';draft.selection_labels={province_id:'Manica'};
const rows=operationTreatments.filter(t=>['general','private_health','private_education','private_training','personal_lessons','export_goods','housing_rent','public_health','public_education','public_training','foreign_operation','other'].includes(t.id)).map((t,i)=>({...newSalesOperation(`qa-${i}`,'2026-01','MZN'),description:`SYNTHETIC QA ${t.label}`,establishment:'SYNTHETIC QA estabelecimento Manica',amount:t.id==='general'?'1161':'100',basis:t.id==='general'?'included':'net',treatment:t.id,confirmed:t.conditions.map((_,i)=>i),evidence:'SYNTHETIC QA — pressupostos de ensaio, não documento real'}));
const book={currency:'MZN',rows,periods:[{period:'2026-01',complete:true}]};return {draft,book};
}
test('each enumerated treatment calculates only with its conditions, article and source',()=>{
const {book}=operationsFixture();
for(const row of book.rows){const r=evaluateSalesOperation(row,'MZN');if(['other','foreign_operation'].includes(row.treatment)){assert.equal(r.vat,null);continue;}
assert.notEqual(r.vat,null,row.treatment);assert.ok(r.article.includes('CIVA'));assert.ok(r.source.includes('Lei'));
assert.equal(evaluateSalesOperation({...row,confirmed:[]},'MZN').vat,null,row.treatment);
assert.equal(evaluateSalesOperation({...row,evidence:''},'MZN').vat,null);
}
const by=Object.fromEntries(book.rows.map(r=>[r.treatment,evaluateSalesOperation(r,'MZN')]));assert.equal(by.private_health.vat,'5.00');assert.equal(by.export_goods.field,'05');assert.match(by.export_goods.reason,/não é taxa zero/);assert.equal(by.housing_rent.field,'07');
});
test('VAT-included general uses statutory unit rounding, conserves gross; included5percent presentation is covered by the rational delta',()=>{
const row=operationsFixture().book.rows[0];const r=evaluateSalesOperation(row,'MZN');assert.equal(r.net,'1001.00');assert.equal(r.vat,'160.00');assert.equal(r.gross,'1161.00');
assert.equal(evaluateSalesOperation({...row,amount:'1160'},'MZN').net,'1000.00');
assert.equal(evaluateSalesOperation({...row,basis:'net',amount:'1000.03'},'MZN').vat,'160.0048');
assert.equal(evaluateSalesOperation({...row,basis:'',amount:'0'},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...row,amount:'0'},'MZN').vat,'0.00');
const reduced=operationsFixture().book.rows[1];assert.equal(evaluateSalesOperation({...reduced,basis:'included'},'MZN').vat,'4.761905');
assert.equal(evaluateSalesOperation({...reduced,basis:'included',amount:'105'},'MZN').net,'100.00');
assert.equal(evaluateSalesOperation({...reduced,basis:'included',amount:'105'},'MZN').vat,'5.00');
});
test('missing information, invalid dates/currency and legal ambiguity never become zero',()=>{
const row=operationsFixture().book.rows[0];for(const patch of [{amount:''},{amount:'-1'},{period:'2025-12'},{period:'2026-13'},{establishment:''},{sourceCurrency:'USD'},{treatment:'invented'}])assert.equal(evaluateSalesOperation({...row,...patch},'MZN').vat,null);
const {draft,book}=operationsFixture();book.result=calculateSales(draft,book);assert.equal(book.result.rows[0].vat,'160.00');assert.equal(book.result.periods[0].fields['02'],null);assert.match(book.result.periods[0].reasons.join(' '),/campo06/);
book.rows=book.rows.filter(r=>!['other','foreign_operation'].includes(r.treatment));book.result=calculateSales(draft,book);assert.equal(book.result.periods[0].fields['04'],'20.00');assert.equal(book.result.periods[0].fields['07'],'400.00');assert.equal(book.result.periods[0].fields['05'],'100.00');
book.periods[0].complete=false;assert.equal(calculateSales(draft,book).periods[0].fields['02'],null);
book.rows=[];book.periods[0].complete=true;assert.equal(calculateSales(draft,book).periods[0].fields['02'],'0.00');
});
test('reuse preserves original sales, intentional edits and period; drift makes new calculation indeterminate',()=>{
const {draft}=operationsFixture();let book=reuseSales(draft,salesBook(draft));assert.equal(book.rows[0].amount,'1000.03');assert.equal(book.rows[3].amount,'');assert.equal(book.rows[0].treatment,'other');
book.rows[0].description='Edited';book=reuseSales(draft,book);assert.equal(book.rows.length,4);assert.equal(book.rows[0].description,'Edited');
const template=operationsFixture().book.rows[0];book.rows[0]={...template,...book.rows[0],treatment:'general',confirmed:[0,1],establishment:'QA',evidence:'QA'};assert.notEqual(calculateSales(draft,book).rows[0].vat,null);
book.rows[0].amount='900';assert.equal(calculateSales(draft,book).rows[0].vat,null);
assert.throws(()=>reuseSales({...draft,currency:'USD'},book),/moeda/i);
});
test('opportunity-specific persistence, report and stale history preserve the first when only the second changes',()=>{
const {draft,book}=operationsFixture();draft.context_opportunity='opportunity:1';book.result=calculateSales(draft,book,'2026-10-24T15:00:00Z');draft.modelo_a_snapshot=saveSalesBook(draft,book);const first=JSON.stringify(salesBook(draft));
draft.context_opportunity='opportunity:309';assert.equal(salesBook(draft).rows.length,0);const second=operationsFixture().book;second.rows[0].amount='2320';second.result=calculateSales(draft,second);draft.modelo_a_snapshot=saveSalesBook(draft,second);
const recovered=JSON.parse(JSON.stringify(draft));assert.equal(salesCurrent(recovered,salesBook(recovered)),true);assert.equal(JSON.stringify(salesBook({...recovered,context_opportunity:'opportunity:1'})),first);
assert.match(JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])})),/2320/);assert.match(JSON.stringify(salesReportRows(recovered)),/Manica/);
second.rows[0].amount='1';draft.modelo_a_snapshot=saveSalesBook(draft,second);assert.equal(salesCurrent(draft,second),false);assert.match(JSON.stringify(salesReportRows(draft)),/Histórico/);
assert.throws(()=>salesBook({...draft,modelo_a_snapshot:'broken'}));
assert.throws(()=>salesBook({...draft,modelo_a_snapshot:JSON.stringify({version:1,books:{},salesOperations:[]})}),/preservados/);
});
test('acquisitions use current classified sales, refuse stale absence and do not universalise deduction',()=>{
const {draft,book}=operationsFixture();book.rows=book.rows.filter(r=>!['other','foreign_operation'].includes(r.treatment));book.result=calculateSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);
const a={currency:'MZN',rows:[],periods:['2026-01','2026-02','2026-03','2026-04'].map(period=>({period,complete:true,absent:['03','04','05','06','07','11','12','13']}))};
const result=calculateAcquisitions(draft,a);assert.equal(result.periods[0].fields['04'],'20.00');assert.equal(result.periods[0].fields['14'],'1901.00');assert.equal(result.periods[0].fields['16'],'180.00');
a.result=result;draft.modelo_a_snapshot=saveAcquisitionBook(draft,a);assert.equal(salesCurrent(draft,salesBook(draft)),true);assert.equal(acquisitionBook(draft).result.periods[0].fields['04'],'20.00');
a.rows=[{...newAcquisition('new-context-purchase','2026-01','MZN'),net:'100',vat:'16',gross:'116',documentType:'invoice',documentRef:'SYNTHETIC QA',documentDate:'2026-01-10',supplier:'SYNTHETIC QA',supplierRegistered:'yes',legalDocument:'yes',held:'yes',domestic:'yes',allocation:'taxable_only',exclusion:'none',integrity:'yes'}];a.taxpayerRegistered='yes';
assert.equal(calculateAcquisitions(draft,a).periods[0].fields['09'],null);
a.rows[0].allocation='no_right';assert.equal(calculateAcquisitions(draft,a).periods[0].fields['09'],'0.00');a.rows=[];
book.rows[0].amount='116';draft.modelo_a_snapshot=saveSalesBook(draft,book);assert.equal(calculateAcquisitions(draft,a).periods[0].fields['02'],null);
});
test('split source reconciles every parcel and recalculation keeps the prior frozen version',()=>{
const {draft}=operationsFixture();let book=reuseSales(draft,salesBook(draft));book=splitSales(book,book.rows[0].id,'second-part');
const group=book.rows.filter(r=>r.splitSource);assert.equal(group.length,2);
for(const [i,r] of group.entries())Object.assign(r,{amount:i?'500.03':'500',treatment:'general',confirmed:[0,1],establishment:'QA',evidence:'QA'});
assert.notEqual(calculateSales(draft,book).rows[0].vat,null);
group[1].amount='499';assert.equal(calculateSales(draft,book).rows[0].vat,null);
group[1].amount='500.03';book=recordSales(draft,book);const frozen=JSON.stringify(book.result);book=recordSales(draft,book);assert.equal(JSON.stringify(book.history[0]),frozen);assert.equal(book.result.revision,2);
});
if(process.env.IVA_L1_FIXTURE_OUT){const {draft,book}=operationsFixture();book.result=calculateSales(draft,book,'2026-10-24T15:00:00Z');draft.modelo_a_snapshot=saveSalesBook(draft,book);fs.writeFileSync(process.env.IVA_L1_FIXTURE_OUT,JSON.stringify(draft,null,2));}