Files
SGF/scripts/sgc-dispatch-pg-proof.php
2026-11-11 09:00:17 +02:00

54 lines
8.8 KiB
PHP

<?php
require __DIR__.'/sgc-dispatch-pg-bootstrap.php';
use Illuminate\Support\Facades\{DB,Schema};
if(($argv[1]??'')==='--worker'){
$job=json_decode(file_get_contents($root.'/'.basename($argv[2])),true);
try{$row=$service->dispatch($actor,$job['id'],$job['version'],'confirm','Confirmação individual sintética de concorrência.',$job['approval']);$result=['status'=>200,'phase'=>$row->phase,'receipt'=>json_decode($row->receipt,true)];}
catch(Symfony\Component\HttpKernel\Exception\HttpException $e){$result=['status'=>$e->getStatusCode()];}
file_put_contents($root.'/'.basename($argv[3]),json_encode($result,JSON_THROW_ON_ERROR));exit;
}
if(is_file($root.'/result.json'))throw new LogicException('Preserve completed source proof');
$checks=[];$ok=function($v,$name)use(&$checks,$root){if(!$v)throw new RuntimeException($name);$checks[]=$name;file_put_contents($root.'/progress.json',json_encode($checks));};
$snapshot=function($c,?array $tables=null,?array $sequences=null){$tables??=array_column($c->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename');$out=[];foreach($tables as$t){if(!preg_match('/^[a-z_][a-z0-9_]*$/D',$t))throw new LogicException('Table');$out[$t]=(array)$c->selectOne('SELECT count(*) rows,md5(coalesce(string_agg(row_to_json(x)::text,chr(10) ORDER BY row_to_json(x)::text),\'\')) fingerprint FROM "'.$t.'" x');}$q=$c->table('pg_sequences')->where('schemaname','public');if($sequences!==null)$q->whereIn('sequencename',$sequences);$out['sequences']=$q->orderBy('sequencename')->get(['sequencename','last_value']);return hash('sha256',json_encode($out,JSON_THROW_ON_ERROR));};
$selected=json_decode(file_get_contents($run['root'].'/private/receipt-composition-cc50a276c10c/state.json'),true);$receiver=json_decode(file_get_contents($run['root'].'/private/receipt-composition-da77bb730751/state.json'),true);
config(['database.connections.protected_host'=>array_replace(config('database.connections.pgsql'),['host'=>'127.0.0.1','port'=>55439,'database'=>$selected['env']['DB_DATABASE'],'username'=>$selected['env']['DB_USERNAME'],'password'=>$selected['env']['DB_PASSWORD'],'url'=>null]),
'database.connections.protected_native'=>$selected['nativeConnection'],'database.connections.receiver_verify'=>$receiver['nativeConnection']]);
$protectedBefore=[$snapshot(DB::connection('protected_host')),$snapshot(DB::connection('protected_native'))];
$tables=array_column($db->select("SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename NOT IN ('sgf_invoice_dispatches','sgf_invoice_dispatch_events') ORDER BY tablename"),'tablename');
$seq=array_column($db->select("SELECT sequencename FROM pg_sequences WHERE schemaname='public' AND sequencename NOT LIKE 'sgf_invoice_dispatch%' ORDER BY sequencename"),'sequencename');
if(Schema::hasTable('sgf_invoice_dispatches')&&!is_file($root.'/progress.json'))throw new LogicException('Only this interrupted proof may resume; never reset previous fixtures');
$sourceBefore=$snapshot($db,$tables,$seq);if(!Schema::hasTable('sgf_invoice_dispatches')){$m=require $sgc.'/database/migrations/2026_11_10_000001_create_sgf_invoice_dispatches.php';$m->up();}
$ok(Schema::hasTable('sgf_invoice_dispatches')&&Schema::hasTable('sgf_invoice_dispatch_events'),'two source persistence tables installed only in existing regression book');
$receiverDb=DB::connection('receiver_verify');$receivedBefore=$receiverDb->table('invoices')->count();$budgetBefore=$receiverDb->table('budget_consumptions')->sum('amount_minor');$movesBefore=$receiverDb->table('stock_movements')->count();$paymentsBefore=$receiverDb->table('external_payments')->count();
$data=function($suffix)use($root){$ref='QA-SOURCE-'.substr(basename($root),-12).'-'.$suffix;return ['reference'=>$ref,'connection_id'=>'qa','mapping_id'=>'map','reason'=>'Recepção documental sintética, autoridade única SGF.','document'=>[
'number'=>$ref,'issued_on'=>'2026-11-10','subtotal'=>'100.00','tax'=>'0.00','total'=>'100.00','items'=>[['source_line_ref'=>'QA-SOURCE-LINE','quantity'=>'1.000','unit'=>'UN','unit_price'=>'100.00','tax_rate'=>'0.00']]]];};
$existing=$db->table('sgf_invoice_dispatches')->where('tenant_id',1)->where('reference',$data('lost')['reference'])->first();
$id=$existing?->id??$service->create($actor,$data('lost'));$version=$existing?->version??1;$row=$service->dispatch($actor,$id,$version,'preview','Conferir impacto real no livro receptor QA.',null);
$ok($row->phase==='previewed','actual SGC service persists real SGF kernel preview');$key=$row->operation_key;
putenv('SGC_BRIDGE_LOSE=true');$row=$service->dispatch($actor,$id,$row->version,'confirm','Aprovar individualmente o impacto recebido.',$service->presentation($actor,$row)['approval_hash']);
$ok($row->phase==='uncertain'&&(bool)$row->ever_sending,'remote commit with lost response is durably uncertain at source');
$row=$service->dispatch($actor,$id,$row->version,'recover','Recuperar a mesma intenção e o recibo original.',null);
$ok($row->phase==='received_pending'&&$row->operation_key===$key&&json_decode($row->receipt,true)['replayed'],'source recovers one actual pending invoice from receiver');
$ok($receiverDb->table('invoices')->count()===$receivedBefore+1,'first end-to-end intent creates exactly one receiver invoice');
$local=App\Models\PurchaseOrder::where('tenant_id',1)->first();
try{app(App\Services\InvoiceWorkflow::class)->register($actor,$local,['number'=>$data('lost')['document']['number']]);throw new RuntimeException('Unexpected local invoice');}
catch(Illuminate\Validation\ValidationException $e){$ok(str_contains($e->getMessage(),'Documento reservado'),'native SGC writer refuses claimed invoice before any local duplicate');}
$id2=$service->create($actor,$data('race'));$row2=$service->dispatch($actor,$id2,1,'preview','Conferir segundo impacto para concorrência.',null);
file_put_contents($root.'/race.json',json_encode(['id'=>$id2,'version'=>$row2->version,'approval'=>$service->presentation($actor,$row2)['approval_hash']],JSON_THROW_ON_ERROR));
$a=new Symfony\Component\Process\Process([PHP_BINARY,__FILE__,'--worker','race.json','race-a.json'],$repo,['SGC_DISPATCH_PG_ROOT'=>$root,'SGC_BRIDGE_DELAY'=>'true'],null,40);$a->start();
for($i=0;$i<160&&!is_file($root.'/sending-ready');$i++)usleep(50000);if(!is_file($root.'/sending-ready'))throw new RuntimeException('First sender did not reach network');
$b=new Symfony\Component\Process\Process([PHP_BINARY,__FILE__,'--worker','race.json','race-b.json'],$repo,['SGC_DISPATCH_PG_ROOT'=>$root],null,40);$b->start();
foreach([$a,$b]as$p){$p->wait();if(!$p->isSuccessful()){file_put_contents($root.'/worker-error.log',$p->getOutput().$p->getErrorOutput());throw new RuntimeException('Sender worker incomplete');}}
$ra=json_decode(file_get_contents($root.'/race-a.json'),true);$rb=json_decode(file_get_contents($root.'/race-b.json'),true);
$ok($ra['status']===200&&$ra['phase']==='received_pending'&&$rb['status']===409,'concurrent source decisions yield one send and stale/in-flight refusal');
$ok($receiverDb->table('invoices')->count()===$receivedBefore+2,'second end-to-end intent creates only one receiver invoice');
$row2=$service->row($actor,$id2);$ok($row2->phase==='received_pending'&&$db->table('sgf_invoice_dispatch_events')->where('dispatch_id',$id2)->where('action','decision.confirmed')->count()===1,'source has one durable decision and receipt for concurrent intent');
$db->beginTransaction();try{$db->table('sgf_invoice_dispatch_events')->where('dispatch_id',$id2)->update(['action'=>'tampered']);throw new RuntimeException('Audit mutable');}catch(Illuminate\Database\QueryException){$db->rollBack();$ok(true,'PostgreSQL refuses rewriting source event history');}
$ok($snapshot($db,$tables,$seq)===$sourceBefore,'all original source business rows and sequences preserved');
$ok($receiverDb->table('budget_consumptions')->sum('amount_minor')===$budgetBefore&&$receiverDb->table('stock_movements')->count()===$movesBefore&&$receiverDb->table('external_payments')->count()===$paymentsBefore,'receiver budget stock and payments unchanged by pending-document intake');
$ok([$snapshot(DB::connection('protected_host')),$snapshot(DB::connection('protected_native'))]===$protectedBefore,'8085 all host/native rows and sequences unchanged');
$ok($db->table('sgf_invoice_dispatch_events')->where('action','decision.confirmed')->count()===2,'exactly two individually audited decisions for two sources');
file_put_contents($root.'/result.json',json_encode(['passed'=>true,'checks'=>$checks,'source_book'=>'b4093c6f6707','receiver_book'=>'da77bb730751','new_receiver_pending_invoices'=>2,
'source_original_fingerprint'=>$sourceBefore,'protected_8085_fingerprints'=>$protectedBefore,'source_statuses'=>[200,409],'new_database'=>false,'new_role'=>false,'new_server'=>false,'tcp_8085'=>false,
'two_independent_application_kernels'=>true,'pilot_migration'=>false,'activation'=>false,'publication'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL;