Files
SGF/scripts/payables-http-proof.mjs
2026-10-29 18:43:29 +02:00

70 lines
11 KiB
JavaScript

import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
import {randomUUID,createHash} from 'node:crypto';
import {execFile} from 'node:child_process';
import {promisify} from 'node:util';
const run=promisify(execFile), reports=[];
const historyOnly=process.argv.includes('--history-only');
async function probe(mode,job,input={}){
// Use spawned stdin for payloads; credentials stay in protected settings read by PHP.
const {spawn}=await import('node:child_process');
return await new Promise((resolve,reject)=>{const p=spawn(mode==='compras'?'C:/Users/macbook/.config/investmoz/php-8.5.11/php.exe':'C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['scripts/payables-probe.php',mode,job],{cwd:process.cwd(),windowsHide:true});let out='',err='';p.stdout.on('data',d=>out+=d);p.stderr.on('data',d=>err+=d);p.on('error',reject);p.on('close',code=>{try{if(code)throw new Error('QA probe failed: '+out+err);resolve(JSON.parse(out));}catch(e){reject(e);}});p.stdin.end(JSON.stringify(input));});
}
for(const mode of ['compras','sgf']){
const fixture=JSON.parse(await fs.readFile(`C:/laragon/data/sgf-tempo/secrets/payables-proof-${mode}-access.json`,'utf8'));
const base=mode==='compras'?'http://127.0.0.1:8007':'http://127.0.0.1:8008';
function client(){let csrf='',context='';const cookies=new Map();return {
async request(path,method='GET',body,opts={}){
const r=await fetch(base+path,{method,headers:{Accept:'application/json','Content-Type':'application/json',Cookie:[...cookies].map(([k,v])=>`${k}=${v}`).join('; '),'X-CSRF-TOKEN':opts.noCsrf?'':csrf,'X-XSRF-TOKEN':opts.noCsrf?'':decodeURIComponent(cookies.get('XSRF-TOKEN')??''),'X-SGF-Context':opts.stale?'stale':context,...(opts.key?{'Idempotency-Key':opts.key}:{})},...(body===undefined?{}:{body:JSON.stringify(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();let data={};try{data=JSON.parse(text);}catch{}if(data.csrf)csrf=data.csrf;if(data.context?.token)context=data.context.token;return {status:r.status,data,text};
},
async login(role){await this.request(mode==='compras'?'/login':'/api/session');const a=fixture.accounts[role];const r=await this.request(mode==='compras'?'/login':'/api/login','POST',{email:a.email,password:a.password});assert.equal(r.status,200,mode+' login');if(mode==='sgf')assert.equal((await this.request('/api/context','POST',{tenant_id:fixture.scope.tenant_id,company_id:fixture.scope.company_id,unit_id:fixture.scope.unit_id})).status,200);},
async list(){const r=await this.request('/api/supplier-payables');assert.equal(r.status,200,mode+' listing '+r.text.slice(0,180));return r.data;},
async send(action,id,key=randomUUID(),expected=200){const r=await this.request('/api/supplier-payables/'+action,'POST',{id,reason:'Prova sintética de '+action},{key});assert.equal(r.status,expected,mode+' '+action+' '+r.text.slice(0,180));return r.data;}
};}
const anon=client();assert.equal((await anon.request('/api/supplier-payables')).status,401);
const p=client(),v=client(),q=client();await p.login('preparacao');await v.login('validacao');await q.login('consulta');
let list=await q.list();assert.equal(list.obligations.length,0);const doc=list.documents[0];assert.equal(doc.subtotal_minor,1000000);assert.equal(doc.tax_minor,160000);assert.equal(doc.total_minor,1160000);assert.equal(doc.status,'validated');assert(doc.validation_audit_id);
const before=await probe(mode,'inspect');
if(historyOnly){
await probe(mode,'history');const existing=await probe(mode,'inspect');
const pending=await p.send('prepare',fixture.source_id);assert.equal(pending.state,'pending_source');assert(pending.missing.some(x=>x.includes('pagamentos')));
assert.equal((await v.send('validate',pending.proposal_id)).state,'conflict');
const refused=await probe(mode,'inspect');assert.equal(refused.counts.obligations,0);assert.deepEqual(refused.domain_hashes,existing.domain_hashes);if(mode==='compras')assert.equal(refused.legacy_balance,1130000);
reports.push({host:mode,passed:true,core_sha256:refused.core_sha256,source_revision:doc.revision,recognised_obligations:0,checks:['previous_settlement_blocks_recognition','history_unchanged','no_full_unpaid_import'],visual_review:false});continue;
}
const preparerPage=await p.request(mode==='compras'?'/obrigacoes-fornecedores':'/supplier-payables');assert.equal(preparerPage.status,200);assert(preparerPage.text.includes('data-action="prepare"'));
const readerPage=await q.request(mode==='compras'?'/obrigacoes-fornecedores':'/supplier-payables');assert(!readerPage.text.includes('<form data-action='));
const racePrepare=await probe(mode,'race',{role:'preparacao',action:'prepare',id:fixture.source_id});assert.equal(racePrepare.waits,2);assert.deepEqual(racePrepare.results.map(r=>r.state),['prepared','prepared']);
const key=randomUUID(),prepared=await p.send('prepare',fixture.source_id,key);assert.equal(prepared.state,'prepared');assert.equal((await p.send('prepare',fixture.source_id,key)).replayed,true);
const conflictKey=await p.request('/api/supplier-payables/prepare','POST',{id:fixture.source_id,reason:'Conteúdo divergente'},{key});assert.equal(conflictKey.status,409);
await q.send('prepare',fixture.source_id,randomUUID(),403);
await probe(mode,'grant',{role:'preparacao',action:'validate',enabled:true});await p.send('validate',prepared.proposal_id,randomUUID(),409);await probe(mode,'grant',{role:'preparacao',action:'validate',enabled:false});
await probe(mode,'source',{override:{status:'pending'}});const incomplete=await p.send('prepare',fixture.source_id);assert.equal(incomplete.state,'pending_source');assert(incomplete.missing.length>0);assert.equal((await q.list()).obligations.length,0);
assert.equal((await v.send('validate',prepared.proposal_id)).state,'conflict');
await probe(mode,'source');const ready=await p.send('prepare',fixture.source_id);assert.equal(ready.state,'prepared');assert.notEqual(ready.proposal_id,incomplete.proposal_id);
const revokedRace=await probe(mode,'race',{role:'validacao',action:'validate',id:ready.proposal_id,revoke:true});assert.deepEqual(revokedRace.results.map(r=>r.status),[403,403]);assert.equal((await q.list()).obligations.length,0);await probe(mode,'grant',{role:'validacao',action:'validate',enabled:true});
await probe(mode,'fault',{enabled:true});const failedKey=randomUUID();await v.send('validate',ready.proposal_id,failedKey,500);assert.equal((await probe(mode,'inspect')).counts.obligations,0);await probe(mode,'fault',{enabled:false});
const raceValidate=await probe(mode,'race',{role:'validacao',action:'validate',id:ready.proposal_id});assert.deepEqual(raceValidate.results.map(r=>r.state),['recognised','recognised']);
const recognised=await v.send('validate',ready.proposal_id,failedKey);assert.equal(recognised.state,'recognised');assert.equal((await v.send('validate',ready.proposal_id,failedKey)).replayed,true);
assert.equal((await p.send('prepare',fixture.source_id)).obligation_id,recognised.obligation_id);
list=await q.list();assert.equal(list.obligations.length,1);const o=list.obligations[0];assert.equal(o.principal_minor,1160000);assert.equal(o.balance_minor,1160000);assert.equal(o.due_on,null);assert.equal(o.days_overdue,null);assert.equal(o.due_status,'unknown');
const future=await probe(mode,'future');assert.equal(future[0].days_overdue,null);assert.equal(future[0].due_on,null);assert.equal(future[0].principal_minor,1160000);
const page=await q.request(mode==='compras'?'/obrigacoes-fornecedores':'/supplier-payables');assert(page.text.includes('11 600.00 MZN'));assert(page.text.includes('Principal reconhecido — vencimento pendente'));assert(page.text.includes('Dias de atraso: sem valor'));
const foreign=await probe(mode,'foreign');assert.equal(foreign.refused,true);
const foreignActor=await probe(mode,'foreign_actor');assert.equal(foreignActor.refused,true);
await probe(mode,'source',{override:{total_minor:1160001}});assert.equal((await p.send('prepare',fixture.source_id)).state,'conflict');assert.equal((await q.list()).obligations[0].principal_minor,1160000);assert.equal((await q.list()).obligations[0].balance_minor,null);await probe(mode,'source');
const after=await probe(mode,'inspect');assert.deepEqual(after.domain_hashes,before.domain_hashes);assert.equal(after.counts.obligations,1);assert.equal(after.recognised_events,1);assert.equal(after.counts.events,after.counts.outbox);if(mode==='compras')assert.equal(after.legacy_balance,1160000);
await probe(mode,'history');const historyBefore=await probe(mode,'inspect');
const historyAttempt=await p.send('prepare',fixture.source_id);assert.equal(historyAttempt.state,'conflict');
const historyAfter=await probe(mode,'inspect');assert.equal(historyAfter.counts.obligations,1);assert.equal(historyAfter.obligations[0].balance_minor,null);assert.equal(historyAfter.obligations[0].principal_minor,1160000);
assert.deepEqual(historyBefore.domain_hashes,historyAfter.domain_hashes);if(mode==='compras')assert.equal(historyAfter.legacy_balance_blocked,409);
const denied=await v.request('/api/supplier-payables/validate','POST',{id:ready.proposal_id,reason:'Falta CSRF'},{key:randomUUID(),noCsrf:true});assert.equal(denied.status,419);
if(mode==='sgf'){assert.equal((await v.request('/api/supplier-payables/validate','POST',{id:ready.proposal_id,reason:'Contexto antigo'},{key:randomUUID(),stale:true})).status,409);assert.equal((await q.request('/api/context','POST',{tenant_id:fixture.scope.tenant_id,company_id:fixture.scope.company_id,unit_id:fixture.scope.unit_id-1})).status,403);}
reports.push({host:mode,passed:true,core_sha256:after.core_sha256,source_revision:doc.revision,principal_minor:o.principal_minor,due_on:null,days_overdue:null,races:[racePrepare,revokedRace,raceValidate],checks:['genuine_conferred_invoice','no_duplicate_new_keys','conflicting_payload','distinct_person','incomplete_source_pending','source_revision_conflict','revocation_while_waiting','event_fault_atomic_rollback','retry_after_failure','unknown_due_even_future','foreign_scope_refused','no_stock_budget_payment_effect','existing_settlement_history_refused_and_preserved','durable_events_outbox','read_only_ui','csrf_active'],visual_review:false});
}
assert.equal(reports[0].core_sha256,reports[1].core_sha256);assert.equal(reports[0].source_revision,reports[1].source_revision);
await fs.writeFile(historyOnly?'docs/proofs/payables-history.json':'docs/proofs/payables-http.json',JSON.stringify({version:'COMMON-PAYABLE-SOURCE-SIM-v1',passed:true,checked_at:new Date().toISOString(),team_budget_preserved:true,hosts:reports},null,2)+'\n');
console.log('Two payables hosts passed: one 11600 MZN obligation, unknown due date, concurrency, faults/recovery, versions, isolation and unchanged budget/stock/payments.');