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

79 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {acquisitionBook,acquisitionCurrent,acquisitionExcludedFields,acquisitionPlanReferences,acquisitionReportRows,calculateAcquisitions,evaluateAcquisition,newAcquisition,reuseAcquisitions,saveAcquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
import {serializeBusinessPlanSnapshot,restoreBusinessPlanSnapshot} from '../../resources/js/lib/business-financial-plan.ts';
export function acquisitionsFixture(){
const draft=closureFixture().draft;
draft.project_title='SYNTHETIC QA — aquisições e apuramento parcial Modelo A';
draft.province_id='cf5e81f1-ffbc-535d-b1b6-b44b445c4421';draft.district_id='';draft.selection_labels={province_id:'Manica'};
const book={currency:'MZN',taxpayerRegistered:'yes',rows:[{...newAcquisition('qa-inventory','2026-01','MZN'),net:'100',vat:'16',gross:'116',documentType:'invoice',documentRef:'SYNTHETIC QA 001',documentDate:'2026-01-10',supplier:'SYNTHETIC QA supplier',supplierRegistered:'yes',legalDocument:'yes',held:'yes',domestic:'yes',allocation:'taxable_only',exclusion:'none',integrity:'yes',description:'SYNTHETIC QA existências'}],periods:['2026-01','2026-02','2026-03','2026-04'].map(period=>({period,complete:true,absent:[...acquisitionExcludedFields]}))};
return {draft,book};
}
test('a recorded purchase does not establish VAT, legal document or deduction',()=>{
const row=newAcquisition('a','2026-01','MZN');row.net='100';
assert.equal(evaluateAcquisition(row,'MZN','yes').deductible,null);
row.vat='16';let r=evaluateAcquisition(row,'MZN','yes');assert.equal(r.gross,'116.00');assert.equal(r.deductible,null);
row.gross='117';assert.match(evaluateAcquisition(row,'MZN','yes').reason,/corresponder/);
});
test('eligibility handles exclusions, mixed use, document, supplier, date and currency without inventing rules',()=>{
const row=acquisitionsFixture().book.rows[0];
assert.equal(evaluateAcquisition(row,'MZN').deductible,null);
assert.equal(evaluateAcquisition(row,'MZN','yes').deductible,'16.00');
for(const patch of [{documentRef:''},{supplierRegistered:''},{held:''},{allocation:'mixed'},{exclusion:'special'},{documentDate:'2026-02-10'},{documentDate:'2026-99-10'},{domestic:'no'},{sourceCurrency:'USD'}]) assert.equal(evaluateAcquisition({...row,...patch},'MZN','yes').deductible,null,JSON.stringify(patch));
for(const patch of [{supplierRegistered:'no'},{allocation:'no_right'},{exclusion:'excluded'},{integrity:'no'}]) assert.equal(evaluateAcquisition({...row,...patch},'MZN','yes').deductible,'0.00');
assert.equal(evaluateAcquisition({...row,origin:'effective'},'MZN','yes').deductible,'16.00');
assert.equal(evaluateAcquisition({...row,net:'100',vat:'0',gross:'100',documentRef:''},'MZN','yes').deductible,'0.00');
});
test('duplicates, ambiguous opportunity and absent declarations are not silently accepted',()=>{
const {draft,book}=acquisitionsFixture();
book.rows.push({...book.rows[0],id:'duplicate'});assert.throws(()=>calculateAcquisitions(draft,book),/repetido/);
assert.throws(()=>acquisitionBook({...draft,context_opportunity:'',context_applicability_snapshot:JSON.stringify({opportunity_choices:[{},{}]})}),/Escolha a oportunidade/);
const missing=acquisitionsFixture();missing.book.periods[0].absent=[];missing.book.result=calculateAcquisitions(missing.draft,missing.book);missing.draft.modelo_a_snapshot=saveAcquisitionBook(missing.draft,missing.book);
assert.match(JSON.stringify(acquisitionReportRows(missing.draft)),/nenhum/);
assert.equal(missing.book.result.periods[0].fields['17'],null);
});
test('08–10, formulas14–18, decimal precision, missing parcels and explicit absence',()=>{
const {draft,book}=acquisitionsFixture();
book.rows.push({...book.rows[0],id:'asset',field:'08',documentLine:'2',net:'200',vat:'32',gross:'232'},{...book.rows[0],id:'service',field:'10',documentLine:'3',net:'50',vat:'8',gross:'58'});
const result=calculateAcquisitions(draft,book);const f=result.periods[0].fields;
assert.equal(f['08'],'32.00');assert.equal(f['09'],'16.00');assert.equal(f['10'],'8.00');assert.equal(f['14'],'1000.03');assert.equal(f['15'],'56.00');assert.equal(f['16'],'160.0048');assert.equal(f['17'],'104.0048');assert.equal(f['18'],'0.00');
assert.equal(result.periods[2].fields['17'],'0.00');assert.equal(result.periods[3].fields['17'],null);
book.periods[0].absent=[];const incomplete=calculateAcquisitions(draft,book).periods[0];for(const field of ['14','15','16','17','18'])assert.equal(incomplete.fields[field],null);
book.periods[0].absent=[...acquisitionExcludedFields];book.periods[0].complete=false;assert.equal(calculateAcquisitions(draft,book).periods[0].fields['15'],null);
const credit=acquisitionsFixture();credit.book.rows[0]={...credit.book.rows[0],net:'2000',vat:'320',gross:'2320'};assert.equal(calculateAcquisitions(credit.draft,credit.book).periods[0].fields['18'],'159.9952');
assert.throws(()=>calculateAcquisitions({...draft,imports:'5'},book),/importa/);
});
test('reuse keeps source values unclassified, preserves deliberate changes and detects source drift',()=>{
const {draft}=acquisitionsFixture();let book=reuseAcquisitions(draft,acquisitionBook(draft));assert.equal(book.rows[0].sourceAmount,'0.00');assert.equal(book.rows[0].net,'');assert.equal(book.rows[0].sourceBasis,'');
book.rows[0].net='123';book=reuseAcquisitions(draft,book);assert.equal(book.rows[0].net,'123');assert.equal(book.rows.length,4);
const plan=restoreBusinessPlanSnapshot(draft.business_plan_snapshot);plan.monthlyInventoryPurchases='1';const changed={...draft,business_plan_snapshot:serializeBusinessPlanSnapshot(plan)};assert.throws(()=>calculateAcquisitions(changed,book),/plano mudaram/);
});
test('asset and operating budgets can be reused explicitly without becoming VAT or deductions',()=>{
const {draft,book}=acquisitionsFixture();const plan=restoreBusinessPlanSnapshot(draft.business_plan_snapshot);plan.assetInvestment='200';plan.monthlyOperatingExpenses='50';draft.business_plan_snapshot=serializeBusinessPlanSnapshot(plan);
const refs=acquisitionPlanReferences(draft);assert.equal(refs.find(r=>r.kind==='asset').amount,'200');assert.equal(refs.find(r=>r.key==='operating:1').amount,'50');
const row={...newAcquisition('asset-reference','2026-01','MZN'),field:'08',sourceField:'asset',sourceMonth:1,sourceAmount:'200',sourceBasis:'net',net:'200'};book.rows=[row];assert.equal(calculateAcquisitions(draft,book).rows[0].deductible,null);
row.vat='32';assert.equal(calculateAcquisitions(draft,book).rows[0].deductible,null);
assert.equal(calculateAcquisitions(draft,book).rows[0].gross,'232.00');
});
test('order month is retained as origin and never forces invoice or deduction month',()=>{
const {draft}=acquisitionsFixture();const plan=restoreBusinessPlanSnapshot(draft.business_plan_snapshot);plan.useInventoryPurchaseSchedule=true;plan.inventoryPurchaseSchedule=[{amount:'100',leadMonths:0},{amount:'0',leadMonths:0},{amount:'0',leadMonths:0},{amount:'0',leadMonths:0}];draft.business_plan_snapshot=serializeBusinessPlanSnapshot(plan);
let book=reuseAcquisitions(draft,acquisitionBook(draft));book.taxpayerRegistered='yes';book.periods=book.periods.map(p=>({...p,complete:true,absent:[...acquisitionExcludedFields]}));
book.rows=book.rows.map(row=>({...row,net:row.sourceAmount,vat:'0',sourceBasis:'net'}));
book.rows[0]={...book.rows[0],...acquisitionsFixture().book.rows[0],id:'plan-1',sourceMonth:1,sourcePeriod:'2026-01',sourceAmount:'100.00',sourceBasis:'net',period:'2026-02',documentDate:'2026-02-10'};
const result=calculateAcquisitions(draft,book);assert.equal(result.rows[0].deductible,'16.00');assert.equal(result.periods[0].fields['09'],'0.00');assert.equal(result.periods[1].fields['09'],'16.00');assert.equal(result.inputs[0].sourcePeriod,'2026-01');
});
test('second opportunity changes preserve first book, frozen results and report context after recovery',()=>{
const {draft,book}=acquisitionsFixture();draft.context_opportunity='opportunity:1';book.result=calculateAcquisitions(draft,book);draft.modelo_a_snapshot=saveAcquisitionBook(draft,book);const first=JSON.stringify(acquisitionBook(draft));
draft.context_opportunity='opportunity:309';assert.equal(acquisitionBook(draft).rows.length,0);const second=acquisitionsFixture().book;second.rows[0].vat='32';second.rows[0].net='200';second.rows[0].gross='232';second.result=calculateAcquisitions(draft,second);draft.modelo_a_snapshot=saveAcquisitionBook(draft,second);
const recovered=JSON.parse(JSON.stringify(draft));assert.equal(acquisitionCurrent(recovered,acquisitionBook(recovered)),true);assert.equal(acquisitionBook(recovered).result.periods[0].fields['17'],'128.0048');
assert.equal(JSON.stringify(acquisitionBook({...draft,context_opportunity:'opportunity:1'})),first);
const report=JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])}));assert.match(report,/128\.0048/);assert.match(report,/opportunity:309/);assert.doesNotMatch(report,/opportunity:1;/);
const old=JSON.stringify(second.result);second.rows[0].vat='33';draft.modelo_a_snapshot=saveAcquisitionBook(draft,second);assert.equal(acquisitionCurrent(draft,second),false);assert.equal(JSON.stringify(second.result),old);assert.match(JSON.stringify(acquisitionReportRows(draft)),/Histórico desactualizado/);
assert.throws(()=>acquisitionBook({modelo_a_snapshot:'broken'}));
});
if(process.env.IVA_ACQUISITION_FIXTURE_OUT){const {draft,book}=acquisitionsFixture();book.result=calculateAcquisitions(draft,book,'2026-10-24T12:00:00.000Z');draft.modelo_a_snapshot=saveAcquisitionBook(draft,book);fs.writeFileSync(process.env.IVA_ACQUISITION_FIXTURE_OUT,JSON.stringify(draft,null,2));}