Files
SGF/scripts/billing-menu-check.mjs
2026-10-29 22:42:37 +02:00

71 lines
8.0 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import {createServer} from 'vite';
const vite=await createServer({server:{middlewareMode:true},appType:'custom'});
try{
console.log('Checking billing modules and rendering…');
const model=await vite.ssrLoadModule('/resources/js/billing-menu.js');
const {navigationGroups}=await vite.ssrLoadModule('/resources/js/WorkspaceNavigation.jsx');
const {BillingDocumentList,default:Workspace}=await vite.ssrLoadModule('/resources/js/BillingMenuWorkspace.jsx');
const workshop=await vite.ssrLoadModule('/resources/js/workshop-menu.js');
const {default:WorkshopMenu}=await vite.ssrLoadModule('/resources/js/WorkshopMenuWorkspace.jsx');
const context={token:'synthetic',company:{name:'Empresa sintética'},unit:{id:7,name:'Unidade sintética'}};
const grants=[['service_invoices.view',7],['billing_drafts.view',7],['billing_drafts.create',7],['service_receivables.view',7],['issuer_documents.view',null]];
const can=(p,u=null)=>grants.some(g=>g[0]===p&&g[1]===u);
assert.deepEqual(navigationGroups(can,7).find(g=>g.id==='billing').items.map(i=>i.label),['Todos os documentos','Facturas','Facturas-recibo','Recibos','Notas de crédito','Orçamentos','Rascunhos']);
assert(!navigationGroups(()=>false,7).some(g=>g.id==='billing'));
assert(!navigationGroups(can,8).flatMap(g=>g.items).some(i=>['service_invoices','billing_drafts'].includes(i.id)));
assert(navigationGroups(can,7).flatMap(g=>g.items).some(i=>i.id==='service_invoice_setup'));
const techCan=(p,u=null)=>u===7&&['workshop.view','workshop.record'].includes(p);
const workshopCan=(p,u=null)=>techCan(p,u)||can(p,u)||(['clients.view','company.view','services.view'].includes(p)&&u===null);
assert.equal(workshop.visibleWorkshopSections(workshopCan,7).length,12);
assert(!workshop.visibleWorkshopSections(techCan,7).some(s=>['workshop_finance','workshop_clients'].includes(s.id)));
assert.deepEqual(workshop.visibleWorkshopSections(techCan,8),[]);
const orders=[{id:1,reference:'OS-SIM-1',client_id:5,state:'open',assigned_to:12,equipment:{id:4,identifier:'BOMBA-SIM',description:'Bomba sintética',serial:'SER-SIM'}},{id:2,reference:'OS-SIM-2',client_id:5,state:'records_verified',assigned_to:12,equipment:{id:4,identifier:'BOMBA-SIM',description:'Bomba sintética',serial:'SER-SIM'}}];
assert.deepEqual(workshop.filterWorkshopOrders(orders,'sintetica','open').map(o=>o.id),[1]);
assert.equal(workshop.filterWorkshopOrders(orders,'SER-SIM').length,2);
assert.deepEqual(workshop.visibleWorkshopEquipment(orders).map(e=>e.orders),[[1,2]]);
for(const section of workshop.workshopSections.filter(s=>s.pending)){
const pending=renderToStaticMarkup(React.createElement(WorkshopMenu,{section:section.id,context,can:workshopCan,api:()=>{},navigate:()=>{}}));
assert(pending.includes('Capacidade ainda não disponível'));assert(!pending.includes('Saldo inicial'));
}
const invoice=model.invoiceRow({id:1,series_code:'TEST-A',number:4,issued_at:'2026-10-29T10:00:00Z'},{snapshot:{issued_local:'2026-10-29 12:00:00',document:{client:{name:'João sintético',data:{nuit:'SIM-001'}},data:{currency:'MZN'}},calculation:{total_document_proposed:'11600.00'}},document_lines:[],corrections:[]});
const draft=model.draftRow({id:2,name:'Proposta sintética',version:3,data:{client_name:'Cliente sintético',currency:'USD'},base_amount:'10.000000000001',actor_id:9});
assert.equal(invoice.total,'11600.00');assert.equal(invoice.balance,null);assert.equal(invoice.dueDate,null);assert.equal(draft.total,null);assert.equal(draft.base,'10.000000000001');
const rows=[invoice,draft];
for(const [filters,expected] of [[{search:'joao'},['invoice:1']],[{search:'SIM-001'},['invoice:1']],[{currency:'USD'},['draft:2']],[{from:'2026-10-01'},['invoice:1']],[{to:'2026-10-01'},[]],[{author:'9'},['draft:2']],[{series:'TEST-A',type:'draft'},[]]])assert.deepEqual(model.filterBillingRows(rows,filters).map(r=>r.key),expected);
const calls=[];
const loaded=await model.loadBillingRows(async path=>{calls.push(path);if(path==='/api/a/billing_drafts')return {records:[]};throw new Error('fixture outage');},can,7);
assert.equal(loaded.sources.find(s=>s.label==='Facturas').error,'fixture outage');assert.equal(loaded.sources.find(s=>s.label==='Rascunhos').error,undefined);
assert.deepEqual(await model.loadBillingRows(()=>{throw Error('Unauthorised fetch');},()=>false,7),{rows:[],sources:[]});
const html=renderToStaticMarkup(React.createElement(BillingDocumentList,{rows,filters:{search:'',type:'',currency:'',series:'',state:'',author:'',from:'',to:''},onFilter:()=>{},onClear:()=>{},onOpen:()=>{}}));
for(const text of ['11600.00','10.000000000001','Consultar percurso financeiro','Por determinar','SIM-001','MZN','USD','Limpar filtros'])assert(html.includes(text));
for(const section of model.billingSections.filter(s=>s.pending)){
const pending=renderToStaticMarkup(React.createElement(Workspace,{section:section.id,context,can,api:()=>{},navigate:()=>{}}));
assert(pending.includes('Capacidade ainda não disponível'));assert(!pending.includes('Confirmar emissão'));
}
// Actual existing isolated SGF host: use the established evaluation entry, no credentials.
console.log('Checking existing isolated HTTP sources…');
const cookies=new Map();
async function request(path,method='GET',body){
const r=await fetch('http://127.0.0.1:8002'+path,{signal:AbortSignal.timeout(15000),method,redirect:'manual',headers:{Connection:'close',Cookie:[...cookies].map(([k,v])=>`${k}=${v}`).join('; '),...(body?{'Content-Type':'application/x-www-form-urlencoded'}:{})},...(body?{body:new URLSearchParams(body)}:{})});
for(const c of r.headers.getSetCookie()){const pair=c.split(';',1)[0],i=pair.indexOf('=');cookies.set(pair.slice(0,i),pair.slice(i+1));}
const text=await r.text();return {status:r.status,text};
}
const landing=await request('/avaliar');assert.equal(landing.status,200);const csrf=landing.text.match(/name="_token" value="([^"]+)"/)?.[1];assert(csrf);
assert.equal((await request('/avaliar/entrar','POST',{role:'preparador',_token:csrf})).status,302);
const session=JSON.parse((await request('/api/session')).text),unit=session.context.unit.id;
const actualCan=(p,u=null)=>session.context.grants.some(g=>g.permission===p&&g.unit_id===u);
const actual=await model.loadBillingRows(async path=>{const r=await request(path);assert.equal(r.status,200,path);return JSON.parse(r.text);},actualCan,unit);
assert(actual.sources.every(s=>!s.error));assert(actual.rows.length>0);
for(const row of actual.rows.filter(r=>r.type==='invoice'))assert.equal(row.total,row.detail.snapshot.calculation.total_document_proposed);
const actualWorkshop=JSON.parse((await request('/api/workshop')).text);assert.equal(actualWorkshop.blocked,false);
assert(actualWorkshop.orders.every(o=>o.equipment&&o.reference&&o.state));
const logout=await fetch('http://127.0.0.1:8002/api/logout',{method:'POST',headers:{Connection:'close',Accept:'application/json',Cookie:[...cookies].map(([k,v])=>`${k}=${v}`).join('; '),'X-CSRF-TOKEN':session.csrf}});assert.equal(logout.status,200);
await fs.writeFile('docs/proofs/billing-menu.json',JSON.stringify({checked_at:new Date().toISOString(),navigation_sections:7,workshop_sections:12,permission_and_unit_checks:true,technician_financial_menu_hidden:true,equipment_identity_preserved:true,filters_checked:true,source_outage_distinguished:true,exact_document_values_preserved:true,actual_host:'http://127.0.0.1:8002',source_counts:actual.sources.map(s=>({source:s.label,count:s.rows.length})),visible_workshop_orders:actualWorkshop.orders.length,business_commands:0,visual_review:false,publication:false},null,2)+'\n');
console.log('Billing menu passed: seven sections, scoped navigation, exact values, combined filters, unknown balances/dates, unavailable sources and actual isolated host reads. Not a browser visual review.');
}finally{await vite.close();}