25 lines
13 KiB
JavaScript
25 lines
13 KiB
JavaScript
import test from 'node:test';import assert from 'node:assert/strict';
|
|
import {fixture} from './fixtures/iva-beverage-fixture.mjs';
|
|
import {recordBeverageRestitution,saveBeverageRestitutionBook,beverageRestitutionBook,beverageRestitutionSources,beverageRestitutionCurrent,beverageRestitutionReportRows} from '../../resources/js/lib/iva-beverage-restitution.ts';
|
|
import {beverageDeductionEvidence} from '../../resources/js/lib/iva-beverage-allocation.ts';
|
|
import {acquisitionBook,recordAcquisitions,saveAcquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
|
|
import {newAcquisitionAllocation} from '../../resources/js/lib/iva-acquisition-allocation.ts';
|
|
import {fiscalCalculationGaps} from '../../resources/js/lib/iva-calculation-gaps.ts';
|
|
import {l3FormPreparations,l3FormPreparationHtml,l3FormalitiesDependencies} from '../../resources/js/lib/l3-form-preparation.ts';
|
|
const n=v=>v&&`${v.numerator}/${v.denominator}`;
|
|
const allocation=(from,to,patch={})=>({from,to,reference:'SYNTHETIC QA repartição documento',deductionFrom:'',deductionTo:'',deductionReference:'',...patch});
|
|
function pending(row,id,from,to){return{...structuredClone(row),id,decision:'pending',decisionReference:'',decisionDate:'',decisionIssuer:'',approvedAmount:'',receipts:[],allocation:allocation(from,to)};}
|
|
function partialPrior(numerator='3',denominator='8',expected='6/1'){const f=fixture(),a=acquisitionBook(f.d);a.rows[0].origin='effective';a.rows[0].allocation='mixed';a.rows[0].bc.beveragePath='deduction';a.rows[0].bc.facts.onlyDeduction='yes';a.rows[0].d={...newAcquisitionAllocation('2026-12'),method:'actual',numerator,denominator,unit:'unidades físicas documentadas',scopeRef:'SYNTHETIC QA parte com direito',source:'SYNTHETIC QA critério e registos',communicationRef:'SYNTHETIC QA comunicação22(2)',noExcludedPayments:'yes',facts:{scopeConfirmed:'yes',rightBasis:'yes',actualScope:'yes',communication:'yes',conditionsMet:'yes'}};let actual=recordAcquisitions(f.d,a);assert.equal(n(actual.result.rows[0].deductibleExact),expected,actual.result.rows[0].reason);f.d.modelo_a_snapshot=saveAcquisitionBook(f.d,actual);actual.rows[0].bc.beveragePath='refund';actual.rows[0].allocation='taxable_only';delete actual.rows[0].d;actual=recordAcquisitions(f.d,actual);f.d.modelo_a_snapshot=saveAcquisitionBook(f.d,actual);f.b.rows[0].source=beverageRestitutionSources(f.d)[0];return f;}
|
|
test('disjoint portions6+10 share one documentary VAT16 without overlap;save/recovery/report preserve prior maps',()=>{const{d,b}=fixture(),before=JSON.parse(d.modelo_a_snapshot);b.rows=[pending(b.rows[0],'portion6','0','6'),pending(b.rows[0],'portion10','6','16')];const book=recordBeverageRestitution(d,b);assert.deepEqual(book.result.rows.map(r=>[r.status,n(r.requested)]),[['determined','6/1'],['determined','10/1']]);d.modelo_a_snapshot=saveBeverageRestitutionBook(d,book);assert.deepEqual(beverageRestitutionBook(d),book);for(const[k,v]of Object.entries(before))assert.deepEqual(JSON.parse(d.modelo_a_snapshot)[k],v,k);assert.ok(beverageRestitutionCurrent(d,book));assert.match(beverageRestitutionReportRows(d).flat().join(' '),/IVA documental:16.00;parcelaIVA:6.00/);});
|
|
test('overlap,negative/zero/outside bounds,missing support and duplicate legacy whole line are rejected',()=>{const{d,b}=fixture();for(const pair of [['0','0'],['-1','6'],['6','5'],['0','16.000001']]){const book={...b,rows:[pending(b.rows[0],'bad',...pair)]};assert.equal(recordBeverageRestitution(d,book).result.rows[0].requested,null);}let book={...b,rows:[pending(b.rows[0],'p1','0','10'),pending(b.rows[0],'p2','9','16')]};assert.ok(recordBeverageRestitution(d,book).result.rows.every(r=>!r.requested));book.rows=[pending(b.rows[0],'p1','0','6'),structuredClone(b.rows[0])];assert.ok(recordBeverageRestitution(d,book).result.rows.every(r=>!r.requested));book.rows=[pending(b.rows[0],'p1','0','6')];book.rows[0].allocation.reference='';assert.equal(recordBeverageRestitution(d,book).result.rows[0].requested,null);});
|
|
test('effective deduction6 must be explicitly reconciled to its own interval;E10 uses only the disjoint remainder',()=>{const{d,b}=partialPrior();assert.equal(n(beverageDeductionEvidence(d,b.rows[0].source).amount),'6/1');b.rows=[pending(b.rows[0],'remainder','6','16')];assert.equal(recordBeverageRestitution(d,b).result.rows[0].requested,null);Object.assign(b.rows[0].allocation,{deductionFrom:'0',deductionTo:'6',deductionReference:'SYNTHETIC QA correspondência da dedução original'});let book=recordBeverageRestitution(d,b);assert.equal(n(book.result.rows[0].requested),'10/1');assert.equal(book.result.rows[0].status,'determined');assert.ok(acquisitionBook(d).history.some(c=>c.rows.some(r=>n(r.deductibleExact)==='6/1')));for(const patch of [{deductionTo:'5'},{deductionFrom:'4',deductionTo:'10'},{deductionReference:''}]){const changed=structuredClone(b);Object.assign(changed.rows[0].allocation,patch);assert.equal(recordBeverageRestitution(d,changed).result.rows[0].requested,null);}});
|
|
test('a rounded tiny deduction,divergent historical amounts and editing away a committed portion never releaseVAT',()=>{const{d,b}=partialPrior(),a=acquisitionBook(d),old=structuredClone(a.history.find(c=>c.rows.some(r=>n(r.deductibleExact)==='6/1')));old.rows.find(r=>r.id==='beverage-purchase').deductibleExact={numerator:'1',denominator:'100000000'};old.rows.find(r=>r.id==='beverage-purchase').deductible='0.00';a.history.push(old);d.modelo_a_snapshot=saveAcquisitionBook(d,a);b.rows=[pending(b.rows[0],'x','0','16')];assert.equal(recordBeverageRestitution(d,b).result.rows[0].requested,null);assert.match(beverageDeductionEvidence(d,b.rows[0].source).reason,/divergentes/);const f=fixture();f.b.rows=[pending(f.b.rows[0],'committed','0','6')];let book=recordBeverageRestitution(f.d,f.b);f.d.modelo_a_snapshot=saveBeverageRestitutionBook(f.d,book);book=recordBeverageRestitution(f.d,{...book,rows:[]});f.d.modelo_a_snapshot=saveBeverageRestitutionBook(f.d,book);const overlap=pending(f.b.rows[0],'new','5','16');assert.equal(recordBeverageRestitution(f.d,{...book,rows:[overlap]}).result.rows[0].requested,null);const edited=pending(f.b.rows[0],'committed','0','7');assert.equal(recordBeverageRestitution(f.d,{...book,rows:[edited]}).result.rows[0].requested,null);});
|
|
test('gap inventory identifies supplyVAT independently from purchase16 and includes implementation limits of margin/travel/events',()=>{const{d}=fixture(),gaps=fiscalCalculationGaps(d);const supply=gaps.find(g=>g.book==='salesOperations'&&g.reason.includes('Bebidas em13'));assert.ok(supply);assert.match(supply.resolution,/IVA da aquisição separado/);const store=JSON.parse(d.modelo_a_snapshot);store.regulatedSecondhands={...store.regulatedSecondhands,[d.context_opportunity]:{rows:[{id:'m',period:'2026-12'}],result:{inputs:[{id:'m',period:'2026-12'}],rows:[{id:'m',status:'determined',baseAmount:'100',vat:null,reason:'Base da margem calculada;IVA não calculado.'}]}}};d.modelo_a_snapshot=JSON.stringify(store);const margin=fiscalCalculationGaps(d).find(g=>g.book==='regulatedSecondhands'&&g.kind==='implementation');assert.ok(margin);assert.match(margin.resolution,/liquidação/);assert.ok(margin.required.length);assert.ok(fiscalCalculationGaps({...d,context_opportunity:'other-context'}).length===0);});
|
|
test('formal preparation maps only identified L3 fields;E keeps16/0/12/7/5 outside27;HTML escapes documents and lists specific dependencies',()=>{const{d,b}=fixture();b.rows[0].entityRef='<script>alert(1)</script>';d.modelo_a_snapshot=saveBeverageRestitutionBook(d,recordBeverageRestitution(d,b));const forms=l3FormPreparations(d),e=forms.find(p=>p.branch==='E');assert.deepEqual(e.fields.map(f=>f.value),['16.00','16.00','0.00','12.00','7.00','5.00']);assert.ok(e.fields.every(f=>f.field==='Dossier'));const html=l3FormPreparationHtml(e);assert.ok(!html.includes('<script>'));assert.ok(html.includes('<script>'));assert.match(html,/modelo oficial específico ainda não identificado/);assert.match(html,/Condição de retoma/);assert.equal(l3FormalitiesDependencies.length,4);assert.ok(forms.filter(p=>p.branch==='A/B').every(p=>p.fields.every(f=>!['05','06'].includes(f.field))));});
|
|
test('export reconciled6deducted/10requested plus gap inventory and preparations for isolated persistence and realUI',async()=>{const{d,b}=partialPrior();b.rows=[pending(b.rows[0],'remainder','6','16')];Object.assign(b.rows[0].allocation,{deductionFrom:'0',deductionTo:'6',deductionReference:'SYNTHETIC QA correspondência da dedução original'});let book=recordBeverageRestitution(d,b);book=recordBeverageRestitution(d,book);d.modelo_a_snapshot=saveBeverageRestitutionBook(d,book);assert.equal(n(book.result.rows[0].requested),'10/1');const fs=await import('node:fs');if(process.env.IVA_E_PARTITION_OUTPUT){fs.writeFileSync(process.env.IVA_E_PARTITION_OUTPUT,JSON.stringify(d));fs.writeFileSync(process.env.IVA_E_PARTITION_OUTPUT.replace('calculated.json','gaps.json'),JSON.stringify(fiscalCalculationGaps(d),null,2));fs.writeFileSync(process.env.IVA_E_PARTITION_OUTPUT.replace('calculated.json','preparations.json'),JSON.stringify(l3FormPreparations(d),null,2));}});
|
|
test('a failed recalculation does not commit its rejected boundaries;correcting support or decimal presentation keeps the accepted reservation',()=>{const{d,b}=fixture();b.rows=[pending(b.rows[0],'stable','0','6')];let book=recordBeverageRestitution(d,b);d.modelo_a_snapshot=saveBeverageRestitutionBook(d,book);const bad=structuredClone(book);bad.rows[0].allocation.to='17';book=recordBeverageRestitution(d,bad);assert.equal(book.result.rows[0].requested,null);d.modelo_a_snapshot=saveBeverageRestitutionBook(d,book);book.rows[0].allocation.to='6.0';book.rows[0].allocation.reference='Updated documented support';book=recordBeverageRestitution(d,book);assert.equal(n(book.result.rows[0].requested),'6/1');assert.equal(book.result.rows[0].status,'determined');});
|
|
test('malformed applicable books are visible errors rather than an empty list;frozen gap labels do not adopt deliberate new edits',()=>{const{d}=fixture(),store=JSON.parse(d.modelo_a_snapshot);store.salesOperations[d.context_opportunity].rows[0].description='Deliberate edited description';d.modelo_a_snapshot=JSON.stringify(store);const gaps=fiscalCalculationGaps(d);assert.ok(gaps.some(g=>g.book==='salesOperations'&&g.kind==='stale'));assert.ok(gaps.filter(g=>g.book==='salesOperations'&&g.kind!=='stale').every(g=>!g.calculation.includes('Deliberate edited description')));store.books[d.context_opportunity]='malformed';assert.throws(()=>fiscalCalculationGaps({...d,modelo_a_snapshot:JSON.stringify(store)}),/não recuperável/);});
|
|
test('identified AB C D fields use their actual frozen books;general request600000 never becomes approval550000 or receipt450000',()=>{const{d}=fixture();const forms=l3FormPreparations({...d,context_opportunity:'opportunity:373'}),march=forms.find(p=>p.branch==='A/B'&&p.period==='2026-03'),sub=forms.find(p=>p.branch==='C');assert.ok(march);assert.equal(march.fields.find(f=>f.field==='24').value,'72.00');assert.ok(sub);assert.equal(sub.fields.find(f=>f.field==='24').value,'61.00');const refund=l3FormPreparations({...d,context_opportunity:'opportunity:374'}).find(p=>p.branch==='D');assert.ok(refund);assert.deepEqual(refund.fields.map(f=>[f.field,f.value]),[['25','800000.00'],['26','200000.00'],['27','600000.00']]);assert.match(refund.notes.join(' '),/reconhecido550000.00;recebido450000.00;por receber100000.00/);assert.ok(march.fields.every(f=>!['05','06'].includes(f.field)));assert.match(l3FormPreparationHtml(sub),/ModeloB/);const snapshot=JSON.parse(d.modelo_a_snapshot);snapshot.ivaClosures['opportunity:373'].rows.find(r=>r.period==='2026-03').entityRef='Deliberate edited entity';const retained=l3FormPreparations({...d,context_opportunity:'opportunity:373',modelo_a_snapshot:JSON.stringify(snapshot)}).find(p=>p.branch==='A/B'&&p.period==='2026-03');assert.equal(retained.current,false);assert.notEqual(retained.entity,'Deliberate edited entity');assert.equal(retained.fields.find(f=>f.field==='24').value,'72.00');});
|
|
|
|
test('non-terminating effective deduction16/3 is reconciled exactly and leaves32/3 forE without decimal approximation or excess',()=>{const{d,b}=partialPrior('1','3','16/3');b.rows=[pending(b.rows[0],'exact-remainder','16/3','16')];Object.assign(b.rows[0].allocation,{deductionFrom:'0',deductionTo:'16/3',deductionReference:'SYNTHETIC QA exact allocation'});let book=recordBeverageRestitution(d,b);assert.equal(book.result.rows[0].status,'determined',book.result.rows[0].reason);assert.equal(n(book.result.rows[0].requested),'32/3');const changed=structuredClone(b);changed.rows[0].allocation.from='5.333333';assert.equal(recordBeverageRestitution(d,changed).result.rows[0].requested,null);changed.rows[0].allocation.from='1/0';assert.equal(recordBeverageRestitution(d,changed).result.rows[0].requested,null);});
|