79 lines
9.7 KiB
JavaScript
79 lines
9.7 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 {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));}
|