Files
SGF/scripts/invoice-progress-ui.mjs
2026-11-04 09:37:20 +02:00

24 lines
8.2 KiB
JavaScript

import fs from 'node:fs';import path from 'node:path';import assert from 'node:assert/strict';import {quantity,appendInvoiceMatching,pendingQuantityWarnings} from '../public/invoice-matching.mjs';
const tag=process.argv[2];assert(/^[a-f0-9]{12}$/.test(tag??''));const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=path.join(run.root,'private/invoice-progress-read-'+tag),out=path.join(root,'invoice-progress-ui.json');assert(!fs.existsSync(out));const checks=[],ok=(v,name)=>{assert(v,name);checks.push(name);};
class Element{constructor(tag,text){this.tag=tag;this.textContent=text??'';this.value='';this.checked=false;this.children=[];this.dataset={focusEnabled:'true',context:'qa-context',csrf:'qa-csrf'};this.isConnected=true;}append(...nodes){this.children.push(...nodes);}prepend(...nodes){this.children.unshift(...nodes);}replaceChildren(...nodes){this.children=[...nodes];}setAttribute(k,v){this[k]=v;}dispatchEvent(){} }
globalThis.Option=class extends Element{constructor(text,value){super('option',text);this.value=value;}};globalThis.FormData=class{*[Symbol.iterator](){} };const walk=e=>[e,...e.children.flatMap(walk)],paginator=data=>({data,current_page:1,last_page:1,per_page:25,total:data.length}),rev='a'.repeat(64),calls=[];let elements=new Map,mismatchReceipt=false;
globalThis.document={createElement:tag=>new Element(tag),querySelector:selector=>{if(!elements.has(selector))elements.set(selector,new Element(selector));return elements.get(selector);}};
const request={id:3,number:'REQ-EXACT',status:'approved',items:[],steps:[],total_minor:10000,process_version:3};const invoice={id:77,number:'INV-EXACT',status:'validated',issued_on:'2026-11-03',total_minor:10000,created_by:1,validated_by:2,items:[],purchase_order_id:41};
const responses={
'/api/purchasing-requests':{new_revision:rev,centres:[],can_prepare:false,requests:paginator([])},'/api/purchasing-requests/3':{request,capabilities:{},history:[],revision:rev},
'/api/purchasing-sourcing/requests/3':{request,capabilities:{},comparison:{policy:{minimum_proposals:3,minimum_evaluators:2},ready:true,rows:[],top_ids:[]},quotes:[],award:{id:2,status:'approved',total_minor:10000,approvals:[]},history:[]},
'/api/purchasing-orders/awards/2':{basis:{award:{id:2,status:'approved',approvals:[],total_minor:10000},quote:{supplier:{name:'QA'},items:[],currency:'MZN'}},existing:{order:{id:41,number:'OC-EXACT',items:[]}},can_issue:false},
'/api/purchasing-receipts/orders/41':{order:{id:41,number:'OC-EXACT'},progress:{state:'complete'},history:{...paginator([]),last_page:2},can_confirm:false},
'/api/purchasing-receipts/history/91':{receipt:{id:91,purchase_order_id:41,number:'REC-EXACT'},documents_enabled:false},
'/api/purchasing-invoices/orders/41':{order:{id:41,number:'OC-EXACT',currency:'MZN',items:[]},invoices:[invoice],can_prepare:false,revision:rev,receipt_progress:{},physical_corrections:[]},
'/api/purchasing-obligations/77':{documents:[{local_id:77,number:'INV-EXACT',supplier_name:'QA',total_minor:10000,status:'validated',order_id:41,missing:[],lines:[]}],obligations:[],permissions:{},proposals:[],events:[],revision:rev,pagination:{proposals:{page:1,last_page:1},events:{page:1,last_page:1}}},
'/api/purchasing-dossiers/3':{request,view_revision:rev,_links:[{href:'/purchasing/requests?request=3',label:'Consultar esta requisição'}],stages:Object.fromEntries(['quotes','awards','orders','receipts','invoices','obligations'].map(k=>[k,{accessible:false}])),events:{accessible:true,data:[],pagination:{page:1,last_page:1}}}
};
globalThis.fetch=async(url,options={})=>{calls.push({url,method:options.method??'GET'});const pathname=url.split('?')[0];let data=responses[pathname];if(!data&&(/\/history$/.test(pathname)||/^\/api\/purchasing-(sourcing|orders|receipts|invoices|obligations|dossiers)$/.test(pathname)))data=paginator([]);if(!data)throw Error('Unexpected mock read '+url);if(mismatchReceipt&&pathname==='/api/purchasing-receipts/history/91')data={receipt:{id:91,purchase_order_id:42,number:'REC-MISMATCH'},documents_enabled:false};return {ok:true,json:async()=>structuredClone(data)};};
const settle=()=>new Promise(resolve=>setTimeout(resolve,10));let sequence=0;async function mount(file,query){elements=new Map;calls.length=0;globalThis.location={search:query};await import('../public/'+file+'.mjs?case='+tag+'-'+sequence++);await settle();await settle();return {calls:[...calls],detail:document.querySelector(file==='order-workspace'?'#award-detail':file==='sourcing-workspace'?'#process-detail':'#detail'),status:document.querySelector(file==='receipt-workspace'?'#message':'#status')};}
ok(quantity(0)==='0.000'&&quantity(1)==='0.001'&&quantity(1000)==='1.000'&&quantity(-10)==='-0.010'&&quantity(1234567)==='1234.567','milli quantities render exact three decimals without floating rounding');assert.throws(()=>quantity(1.2));assert.throws(()=>quantity(Number.MAX_SAFE_INTEGER+1));checks.push('unsafe or fractional milli quantities are refused in display');
const row={line_id:9,description:'Material QA',unit:'UN',ordered_milli:1000,accepted_milli:1000,returned_milli:0,reversed_milli:0,accepted_net_milli:1000,validated_milli:400,pending_milli:700,remaining_milli:600,deficit_milli:0};const pending={status:'pending',items:[{purchase_order_item_id:9,quantity_milli:700}]};ok(pendingQuantityWarnings(pending,[row])[0].includes('0.700')&&pendingQuantityWarnings(pending,[row])[0].includes('0.600'),'pending overlap warning compares original quantity with actual remaining quantity');ok(!pendingQuantityWarnings({...pending,status:'validated'},[row]).length,'validated invoice is not warned as a new pending claim');ok(!pendingQuantityWarnings({status:'pending',items:[{purchase_order_item_id:9,quantity_milli:600}]},[row]).length,'other pending documents do not reserve remaining quantity in UI');const box=new Element('section');appendInvoiceMatching(box,[{...row,remaining_milli:0,deficit_milli:100}]);ok(walk(box).some(v=>v.textContent.includes('0.100 UN j')),'quantity deficit remains explicit without negative available value');
const d=responses['/api/purchasing-invoices/orders/41'];d.matching_progress=[row];d.can_prepare=true;d.order.items=[{id:9,description:'Material QA',quantity:'1.000',unit:'UN',unit_price_minor:10000,tax_rate:'0'}];d.invoices[0].status='pending';d.invoices[0].can_validate=true;d.invoices[0].items=pending.items;responses['/api/purchasing-invoices/orders/41/history']={...paginator([]),last_page:3};const result=await mount('supplier-invoice-workspace','?order=41&invoice=77');ok(walk(result.detail).some(v=>v.textContent==='Restante actual para conferir: 0.600 UN'),'real invoice module wires matching capacity next to explicit line choice');ok(walk(result.detail).some(v=>v.role==='note'&&v.textContent.includes('excede o restante actual')),'real invoice module shows overlap note without changing document state');const qtyLabel=walk(result.detail).find(v=>v.tag==='label'&&v.textContent.startsWith('Quantidade facturada'));ok(qtyLabel.children[0].value===''&&qtyLabel.children[0].disabled,'remaining capacity does not prefill or choose invoice quantity');ok(calls.every(v=>v.method==='GET'),'opening matching projection never registers validates or recognises a document');
const next=walk(result.detail).find(v=>v.tag==='button'&&v.textContent==='Seguinte'),pendingReads=[],baseFetch=globalThis.fetch;globalThis.fetch=async(url,options)=>{if(url.includes('/history?page=2'))return await new Promise(resolve=>pendingReads.push(resolve));return baseFetch(url,options);};const first=next.onclick(),second=next.onclick();const payload=marker=>({ok:true,json:async()=>({...paginator([{id:1,created_at:'2026-11-03',action:'invoice.created',user_id:1,before:null,after:{marker}}]),current_page:2,last_page:3})});pendingReads[1](payload('new-history-response'));await second;pendingReads[0](payload('old-history-response'));await first;ok(walk(result.detail).some(v=>v.textContent.includes('new-history-response'))&&!walk(result.detail).some(v=>v.textContent.includes('old-history-response')),'late invoice history response cannot overwrite latest requested page');fs.writeFileSync(out,JSON.stringify({passed:true,checks,dom_simulation:true,business_commands:0,visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length}));