import fs from 'node:fs';import path from 'node:path';import net from 'node:net';import crypto from 'node:crypto';import assert from 'node:assert/strict';import {spawn} from 'node:child_process'; 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 sales fixture required'); const root=path.join(state.root,'private/ledger-'+tag),settings=path.join(root,'settings.json'),g=read(path.join(root,'sales-fixture.json')),output=path.join(root,'sales-http.json');if(fs.existsSync(output))throw Error('Completed HTTP proof preserved'); 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,'sales-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-sales-http-router.php')],{cwd:process.cwd(),env:{...process.env,LEDGER_QA_SETTINGS:settings},windowsHide:true,stdio:['ignore',log,log]}); let checks=0;const check=(v,s)=>{checks++;assert(v,s);};const client=()=>{let auth;const jar=new Map();const request=async(url,body,key=crypto.randomUUID(),extra={})=>{const r=await fetch('http://127.0.0.1:8047'+url,{method:body?'POST':'GET',headers:{Accept:'application/json',Cookie:[...jar].map(([k,v])=>k+'='+v).join('; '),'X-SGF-Context':auth?.context??'',...(body?{'Content-Type':'application/json','X-CSRF-TOKEN':auth?.csrf??'','Idempotency-Key':key}:{}),...extra},body:body?JSON.stringify(body):undefined,redirect:'manual',signal:AbortSignal.timeout(12000)});for(const c of r.headers.getSetCookie()){const p=c.split(';')[0],i=p.indexOf('=');jar.set(p.slice(0,i),p.slice(i+1));}const bytes=Buffer.from(await r.arrayBuffer()),text=bytes.toString('utf8');let data;try{data=JSON.parse(text);}catch{}return{status:r.status,data,text,bytes,headers:r.headers};};return{request,enter:async role=>{const r=await request('/qa/sales-enter/'+role);check(r.status===200,'Own sales identity '+role);auth=r.data;check(jar.has('sgf_sales_'+tag)&&!jar.has('XSRF-TOKEN'),'Dedicated session cookie without overwriting shared CSRF');},module:async module=>{const old=auth.context,r=await request('/api/context',{...g.scope,module_id:module});check(r.status===200,'Explicit context selection '+module);auth={csrf:r.data.csrf,context:r.data.context.token};return old;}};}; const flow='/api/general-ledger/sales?invoice_id='+g.invoice_id,html='/general-ledger/sales?invoice_id='+g.invoice_id; const ledger=async(c,action,body,key)=>{const r=await c.request('/api/general-ledger/'+action,body,key);check(r.status===200,'Ledger '+action+' '+r.status+' '+(r.data?.message??''));return r.data;}; try{ const p=client(),v=client(),r=client();let ready=false;for(let n=0;n<50&&!ready;n++){try{ready=(await p.request(flow)).status===401;}catch{}if(!ready)await new Promise(res=>setTimeout(res,100));}check(ready,'Unauthenticated sales read refused');await p.enter('prepare');await v.enter('validate');await r.enter('read'); let data=await r.request(flow,undefined,undefined,{'X-Sales-QA-Proof':'read'});check(data.status===200&&data.data.commercial.position===null&&data.data.book.journals.length===0,'Actual issued invoice starts without financial approval or journal');const invoice=data.data.commercial.invoice; check(invoice.snapshot.calculation.base_exact==='862.07'&&invoice.snapshot.calculation.vat_document_proposed==='137.93'&&invoice.snapshot.calculation.total_document_proposed==='1000.00','Documentary calculation reused exactly'); const mode=read(path.join(root,'sales-http-read.json'));check(mode.transaction.isolation==='repeatable read'&&mode.transaction.read_only==='on'&&mode.writer_locks===0,'Whole sales workspace coherent and read-only without locks'); let view=await p.request(html);check(view.status===200&&view.text.includes('data-finance="propose"')&&!view.text.includes('name="effect_type" value="sales.invoice"'),'Before approval UI offers financial proposal, no premature ledger capture');view=await r.request(html);check(view.status===200&&!view.text.includes('data-finance=')&&!view.text.includes('data-ledger='),'Reader sees no commands'); for(const q of ['invoice_id=999999','invoice_id=0','invoice_id%5Bx%5D=1','invoice_id='+g.invoice_id+'&company_id=9'])check([403,422].includes((await r.request('/api/general-ledger/sales?'+q)).status),'Foreign/injected source refused'); check((await p.request('/api/general-ledger/period-prepare',{period_key:'QA-2026-10',reason:'No CSRF must refuse.'},undefined,{'X-CSRF-TOKEN':''})).status===419,'Real CSRF middleware active in browser-testing QA'); await p.module('commercial');let pdf=await p.request('/api/service-invoices/'+g.invoice_id+'/pdf');check(pdf.status===200&&pdf.bytes.subarray(0,4).toString()==='%PDF'&&crypto.createHash('sha256').update(pdf.bytes).digest('hex')===invoice.pdf_sha256,'Original PDF bytes/hash accessible only in explicit Commercial context');await p.module('accounting'); await p.module('finance');await v.module('finance');await r.module('finance'); const propose=async(c,body,key=crypto.randomUUID())=>{const rr=await c.request('/api/service-receivables/proposals',{invoice_id:g.invoice_id,currency:'MZN',source:'TEST-MANUAL',external_id:key,...body},key);check(rr.status===200,'Financial proposal '+rr.status+' '+(rr.data?.message??''));return rr.data;}; const decide=async(c,id,revision)=>{const rr=await c.request('/api/service-receivables/'+id+'/decision',{version:1,revision,decision:'approved',reason:'Independent exact synthetic financial conference.'});check(rr.status===200,'Independent financial decision '+rr.status+' '+(rr.data?.message??''));return rr.data;}; let financial=await propose(p,{kind:'obligation',revision:0,reason:'Explicit documentary financial opening.'});const ob=financial.id; check((await p.request('/api/service-receivables/'+ob+'/decision',{version:1,revision:0,decision:'approved',reason:'Cannot approve own obligation.'})).status===403,'Financial self-approval refused');await decide(v,ob,0); check((await r.request('/api/service-receivables/proposals',{invoice_id:g.invoice_id,kind:'receipt',currency:'MZN',source:'TEST-MANUAL',external_id:'READ-FORBIDDEN',obligation_id:ob,amount:'400.00',date:'2026-10-29',evidence:'Reader cannot receive.',revision:1})).status===403,'Read-only role has no financial preparation'); await p.module('accounting');await v.module('accounting');await r.module('accounting'); const effectBody=(kind,id,rule)=>({effect_type:kind,source_id:id,journal_on:'2026-10-29',period_key:'QA-2026-10',rule_key:rule,reason:'Capture factual commercial source and exact documentary amounts.'}); const post=async proposal=>{await ledger(v,'validate',{proposal_id:proposal.proposal_id,revision:proposal.revision,decision:'approved',reason:'Independent accounting source and impact review.'});const body={proposal_id:proposal.proposal_id,revision:proposal.revision,reason:'Apply reviewed commercial accounting once.'},key=crypto.randomUUID();const result=await ledger(p,'post',body,key);check((await ledger(p,'post',body,key)).replayed,'Posting retry recovers original receipt');return result;}; const invProposal=await ledger(p,'effect-prepare',effectBody('sales.invoice',g.invoice_id,'QA-SALES'));view=await v.request(html);check(view.status===200&&view.text.includes('QA-CLIENTES')&&view.text.includes('QA-VENDAS')&&view.text.includes('QA-IMPOSTO-TESTE')&&view.text.includes('137,93 MZN'),'Three documentary impacts presented before independent review');await post(invProposal); let revision=1;const allocations=[]; for(const [n,amount] of ['400.00','600.00'].entries()){ await p.module('finance');await v.module('finance');const receipt=await propose(p,{kind:'receipt',obligation_id:ob,amount,date:'2026-10-29',evidence:'Synthetic receipt with no monetary execution.',revision,reason:'Individual synthetic receipt proposal.'});await decide(v,receipt.id,revision);revision++; check((await p.request('/api/service-receivables/'+receipt.id+'/allocate',{amount,version:1,revision:revision-1})).status===409,'Stale allocation revision refused'); const allocation=await p.request('/api/service-receivables/'+receipt.id+'/allocate',{amount,version:1,revision});check(allocation.status===200,'Explicit confirmed-receipt allocation');revision++;allocations.push(allocation.data.id); await p.module('accounting');await v.module('accounting');data=await p.request(flow);check(data.data.commercial.position.balance===(n===0?'600.00':'0.00'),'Authoritative financial balance before ledger posting'); await post(await ledger(p,'effect-prepare',effectBody('receivable.allocated',allocation.data.id,'QA-RECEIVABLE')));data=await p.request(flow);check(data.data.commercial.position.balance===(n===0?'600.00':'0.00'),'Accounting does not allocate again'); } await p.module('finance');await v.module('finance');financial=await propose(p,{kind:'correction',obligation_id:ob,action:'reverse_allocation',target_effect_id:allocations[0],revision,reason:'Reverse integral original allocation400.'});const reverse=await decide(v,financial.id,revision);revision++;await p.module('accounting');await v.module('accounting'); data=await p.request(flow);check(data.data.commercial.position.balance==='400.00'&&data.data.commercial.position.available==='400.00','Reversal restores balance400 and unallocated400, no money return'); check((await p.request('/api/general-ledger/period-prepare',{period_key:'QA-2026-10',reason:'Unaccounted reversal prevents closing.'})).status===409,'Closing detects new unaccounted financial reversal');await post(await ledger(p,'effect-prepare',effectBody('receivable.reversed',reverse.id,'QA-RECEIVABLE-REVERSE'))); const period=await ledger(p,'period-prepare',{period_key:'QA-2026-10',reason:'Capture all confirmed sales effects and configuration.'});check((await v.request('/api/general-ledger/period-validate',{check_id:period.check_id,revision:'0'.repeat(64),decision:'approved',reason:'Stale period evidence must refuse.'})).status===409,'Stale period revision refused');await ledger(v,'period-validate',{check_id:period.check_id,revision:period.revision,decision:'approved',reason:'Independent complete commercial period conference.'});const close={check_id:period.check_id,revision:period.revision,reason:'Close complete artificial sales period.'},key=crypto.randomUUID();const closure=await ledger(p,'period-close',close,key);check((await ledger(p,'period-close',close,key)).replayed,'Closing retry has no extra version');check((await ledger(p,'period-close',close)).closure_id===closure.closure_id,'Different key recovers same close'); data=await r.request(flow);check(data.status===200&&data.data.book.journals.length===4&&data.data.book.lines.length===9&&data.data.book.totals.debit_minor===240000&&data.data.book.totals.credit_minor===240000,'Four journals/nine lines balanced2400 on each side');check(data.data.client_account.net_minor===40000&&data.data.commercial.position.balance==='400.00','Account of clients agrees with authoritative financial balance'); view=await r.request(html);check(view.status===200&&view.text.includes('400,00 MZN')&&view.text.includes('Fecho #')&&!view.text.includes('data-finance=')&&!view.text.includes('data-ledger='),'Final reader view shows close, balance and no actions');check(data.headers.get('cache-control').includes('no-store'),'Private source evidence'); const stale=await p.module('finance');check((await p.request(flow,undefined,undefined,{'X-SGF-Context':stale})).status===409,'Wrong module/old generation cannot reuse accounting source');await p.module('accounting');check((await p.request('/api/general-ledger/effect-prepare',effectBody('sales.invoice',g.invoice_id,'QA-SALES'))).status===409,'Closed period cannot capture another journal'); fs.writeFileSync(output,JSON.stringify({passed:true,checks,actual_http:true,real_csrf:true,read_transaction:mode,invoice_id:g.invoice_id,obligation_id:ob,allocation_ids:allocations,reversal_id:reverse.id,source_pdf_sha256:invoice.pdf_sha256,principal_minor:100000,financial_balance_minor:40000,client_account_minor:40000,journals:4,lines:9,gross_debit_minor:240000,gross_credit_minor:240000,closed:true,visual_review:false,browser_automation:false},null,2)+'\n');console.log(JSON.stringify({passed:true,checks,tag})); }finally{if(server.exitCode===null){server.kill();await new Promise(res=>server.once('exit',res));}fs.closeSync(log);}