Files
SGF/scripts/ledger-flow-http.mjs
T
2026-11-04 09:37:20 +02:00

46 lines
9.4 KiB
JavaScript

import fs from 'node:fs';import path from 'node:path';import net from 'node:net';import {spawn} from 'node:child_process';import assert from 'node:assert/strict';import crypto from 'node:crypto';
const read=p=>JSON.parse(fs.readFileSync(p,'utf8')),state=read('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'),tag=process.argv[2];
if(state.port!==55439||!/^[a-f0-9]{12}$/.test(tag??''))throw Error('Owned QA tag required');
const root=path.join(state.root,'private/ledger-'+tag),settings=path.join(root,'settings.json'),owned=read(settings),fixture=read(path.join(root,'fixture.json')),g=read(path.join(root,'ledger-fixture.json')),output=path.join(root,'ledger-flow-http.json');
if(!g.integrated_flow||owned.tag!==tag||owned.cluster!=='sgf_modular_regression_'+state.id||fs.existsSync(output))throw Error('Fresh dedicated integrated proof required');
await new Promise((resolve,reject)=>{const p=net.createServer();p.once('error',reject);p.listen(8047,'127.0.0.1',()=>p.close(resolve));});
const log=fs.openSync(path.join(root,'ledger-flow-http-server.log'),'a'),server=spawn('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['-S','127.0.0.1:8047',path.resolve('scripts/ledger-http-router.php')],{cwd:process.cwd(),env:{...process.env,LEDGER_QA_SETTINGS:settings},windowsHide:true,stdio:['ignore',log,log]});
let checks=0;const check=(ok,why)=>{checks++;assert(ok,why);};
const client=()=>{let auth;const jar=new Map();const request=async(url,{body,key,headers:extra={}}={})=>{const headers={Accept:'application/json',Cookie:[...jar].map(([k,v])=>k+'='+v).join('; '),'X-SGF-Context':auth?.context??'',...extra};if(body)Object.assign(headers,{'Content-Type':'application/json','X-CSRF-TOKEN':auth?.csrf??'','Idempotency-Key':key??crypto.randomUUID()},extra);const r=await fetch('http://127.0.0.1:8047'+url,{method:body?'POST':'GET',headers,body:body?JSON.stringify(body):undefined,redirect:'manual',signal:AbortSignal.timeout(10000)});for(const c of r.headers.getSetCookie()){const p=c.split(';')[0],at=p.indexOf('=');jar.set(p.slice(0,at),p.slice(at+1));}const text=await r.text();let data;try{data=JSON.parse(text);}catch{}return{status:r.status,text,data};};return{request,enter:async role=>{const r=await request('/qa/ledger-enter/'+role);assert.equal(r.status,200);auth=r.data;},switch:async module=>{const old=auth.context,r=await request('/api/context',{body:{...fixture.scope,module_id:module}});check(r.status===200&&r.data.context.module_id===module,'Existing explicit module switch '+module);auth={csrf:r.data.csrf,context:r.data.context.token};check(auth.context!==old,'Context generation rotates');return old;}};};
const source=g.local_source_id,flow='/general-ledger/flow?source='+source;
try{
const prep=client(),review=client(),reader=client();let ready=false;for(let n=0;n<50&&!ready;n++){try{ready=(await prep.request(flow)).status===401;}catch{}if(!ready)await new Promise(r=>setTimeout(r,100));}check(ready,'Unauthenticated flow refused');
await prep.enter('prepare');await review.enter('validate');await reader.enter('read');
let r=await prep.request(flow);check(r.status===200&&r.text.includes('Ainda não reconhecida')&&r.text.includes('Sem proposta contabilística'),'Starts before recognition');
check((await reader.request(flow)).status===200,'Read-only role sees evidence');
for(const query of ['source=999999','source=0','source%5Bx%5D=1','source='+source+'&company_id=999']){r=await prep.request('/general-ledger/flow?'+query);check([403,422].includes(r.status),'Unscoped/injected source refused: '+query);}
check((await prep.request('/api/general-ledger?obligation_id=999999')).status===403,'Foreign obligation focus refused');
const early={...g.body,obligation_id:999999};check((await prep.request('/api/general-ledger/prepare',{body:early})).status===409,'No posting proposal before recognition');
const stale=await prep.switch('purchasing');
check((await prep.request('/api/supplier-payables?source='+source,{headers:{'X-SGF-Context':stale}})).status===409,'Old tab generation refused after handoff');
check((await prep.request(flow)).status===409,'Active module isolation preserved');
r=await prep.request('/supplier-payables?source='+source);check(r.status===200&&r.text.includes('ledger_flow_url')&&r.text.includes('QA-LEDGER-SOURCE-60'),'Financial UI links the same source back to the flow');
let listing=await prep.request('/api/supplier-payables?source='+source);check(listing.status===200&&listing.data.obligations.length===0,'No initial obligation');
const fp={id:source,reason:'Integrated synthetic source preparation.'},fk=crypto.randomUUID();r=await prep.request('/api/supplier-payables/prepare',{body:fp,key:fk});check(r.status===200&&r.data.proposal_id,'Financial prepare through actual route');const financialProposal=r.data.proposal_id;
r=await prep.request('/api/supplier-payables/prepare',{body:fp,key:fk});check(r.status===200&&r.data.replayed,'Financial prepare retry recovers receipt');
await reader.switch('purchasing');check((await reader.request('/api/supplier-payables/validate',{body:{id:financialProposal,reason:'Reader cannot recognise.'}})).status===403,'Reader cannot recognise debt');
await review.switch('purchasing');r=await review.request('/api/supplier-payables/validate',{body:{id:financialProposal,reason:'Independent financial recognition of exact source.'}});check(r.status===200&&r.data.obligation_id,'Independent recognition through actual route');const obligation=r.data.obligation_id;
await prep.switch('accounting');r=await prep.request(flow);check(r.status===200&&r.text.includes('Obrigação #'+obligation)&&r.text.includes('Saldo 60,00 MZN'),'Recognition appears in the same chain');
r=await prep.request('/general-ledger?obligation_id='+obligation);check(r.status===200&&r.text.includes('name="obligation_id" type="number" min="1" value="'+obligation+'"'),'Obligation handoff prefills exact source');
const body={...g.body,obligation_id:obligation},pk=crypto.randomUUID();r=await prep.request('/api/general-ledger/prepare',{body,key:pk});check(r.status===200&&r.data.state==='prepared','Accounting proposal created from recognised obligation');const proposal=r.data.proposal_id,revision=r.data.revision;
r=await prep.request('/api/general-ledger/prepare',{body,key:pk});check(r.status===200&&r.data.replayed&&r.data.proposal_id===proposal,'Accounting prepare retry has no second capture');
const post={proposal_id:proposal,revision,reason:'Apply reviewed integrated synthetic impact once.'};check((await prep.request('/api/general-ledger/post',{body:post})).status===409,'Posting before independent review refused');
await review.switch('accounting');const approval={proposal_id:proposal,revision,decision:'approved',reason:'Independent review of integrated source and exact debit credit.'};
check((await review.request('/api/general-ledger/validate',{body:{...approval,revision:'0'.repeat(64)}})).status===409,'Stale accounting revision refused');
r=await review.request('/api/general-ledger/validate',{body:approval});check(r.status===200&&r.data.state==='approved','Independent exact accounting review');
const postKey=crypto.randomUUID();r=await prep.request('/api/general-ledger/post',{body:post,key:postKey});check(r.status===200&&r.data.journal_id,'Final actual HTTP posting succeeds');const journal=r.data.journal_id;
r=await prep.request('/api/general-ledger/post',{body:post,key:postKey});check(r.status===200&&r.data.replayed&&r.data.journal_id===journal,'Same key recovers same journal');
r=await prep.request('/api/general-ledger/post',{body:post});check(r.status===200&&r.data.journal_id===journal,'Different key does not duplicate effect');
check((await prep.request('/api/general-ledger/post',{body:{...post,reason:'Different requested posting content.'}})).status===409,'Conflicting second content refused');
r=await prep.request('/api/general-ledger?obligation_id='+obligation);check(r.status===200&&r.data.journals.length===1&&r.data.proposals.length===1&&r.data.events.length===3&&r.data.lines.length===2&&r.data.totals.debit_minor===6000&&r.data.totals.credit_minor===6000,'Focused journal evidence and all event kinds reconciled');
r=await prep.request(flow);check(r.status===200&&r.text.includes('Lançamento #'+journal)&&r.text.includes('Saldo 60,00 MZN')&&r.text.includes('Débito 60,00 MZN / Crédito 60,00 MZN'),'Complete chain retains financial balance and balanced accounting');
await reader.switch('accounting');r=await reader.request('/general-ledger?obligation_id='+obligation);check(r.status===200&&r.text.includes('Conferir percurso e origem')&&!r.text.includes('data-action="post"'),'Reader can trace origin without posting action');
await reader.switch('purchasing');r=await reader.request('/api/supplier-payables/report?source='+source);check(r.status===200&&r.data.totals.count===1&&r.data.totals.principal_minor===6000&&r.data.totals.balance_minor===6000,'Financial report still one principal and unchanged balance after journal');
fs.writeFileSync(output,JSON.stringify({passed:true,checks,tag,source_id:source,financial_proposal_id:financialProposal,obligation_id:obligation,ledger_proposal_id:proposal,journal_id:journal,principal_minor:6000,balance_minor:6000,debit_minor:6000,credit_minor:6000,actual_http_kernel:true,existing_context_selection:true,complete_financial_to_ledger_chain:true,live_purchasing_source:false,visual_review:false},null,2)+'\n');console.log(JSON.stringify({passed:true,checks,tag,journal_id:journal}));
}finally{if(server.exitCode===null){server.kill();await new Promise(resolve=>server.once('exit',resolve));}fs.closeSync(log);}