Files
SGF/scripts/sgf-workshop-stock-bridge-proof.php
T
2026-11-11 09:00:17 +02:00

35 lines
10 KiB
PHP

<?php
use Illuminate\Support\Facades\Auth;use Illuminate\Support\Str;use App\Shared\Context\CompanyContext;
require __DIR__.'/sgf-ledger-proof-bootstrap.php';$input=json_decode(file_get_contents($s['root'].'/single-workshop-stock-input.json'),true);$physical=json_decode(file_get_contents($s['root'].'/single-stock-physical-input.json'),true);$pins=json_decode(file_get_contents($s['root'].'/single-binding-pins.json'),true);config(['stock_operations.enabled'=>true,'stock_operations.profile_sha256'=>$pins['stock-operations-binding.json'],'workshop_stock.composed_enabled'=>true,'workshop_stock.composed_sha256'=>$pins['workshop-stock-binding.json']]);
$invoke=function($role,$action,$data=[],$id=null,$key=null)use($app,$profiles,$f){Auth::forgetGuards();Auth::shouldUse('web');$user=App\Models\User::findOrFail($role==='warehouse'?$f['validator']:$profiles[$role]['user_id']);Auth::guard('web')->setUser($user);$path=$action==='index'?'/api/workshop-stock':($action==='confirm'?'/api/workshop-stock/'.$id.'/confirm':'/api/workshop-stock/'.$action);$r=Illuminate\Http\Request::create($path,$action==='index'?'GET':'POST',$data,[],[],['HTTP_IDEMPOTENCY_KEY'=>$key??(string)Str::uuid()]);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>'workshop','token'=>'bridge-'.$role]);$r->setLaravelSession($session);$r->setUserResolver(fn()=>$user);$app->instance('request',$r);app(CompanyContext::class)->selectFor($user->id,1,2,2);$c=app(App\Modules\Workshop\Http\WorkshopStockController::class);$stock=app(App\Modules\Workshop\WorkshopStock::class);return match($action){'index'=>$c->index($r,$stock),'allocation'=>$c->prepareAllocation($r,$stock),'return'=>$c->prepareReturn($r,$stock),'confirm'=>$c->confirm($r,$id,$stock)};};
$before=$native->table('stock_movements')->where('stock_item_id','<>',$physical['stock_item'])->orderBy('id')->get()->toJson();$host->beginTransaction();$native->beginTransaction();
try{
$listing=$invoke('workshop_record','index')->getData(true);$ok(count($listing['sources'])===2,'existing workshop screen reads its two explicitly verified consumed/returned observations');
$data=['entry_id'=>$input['entries']['consumed'],'version'=>1,'warehouse_id'=>$physical['warehouse'],'movement_id'=>$physical['issue_movement'],'reason'=>'Associar meia unidade consumida à entrega física já abatida.','evidence'=>'QA-ENTREGA-ORIGINAL'];$key=(string)Str::uuid();$proposal=$invoke('workshop_record','allocation',$data,null,$key)->getData(true);$retry=$invoke('workshop_record','allocation',$data,null,$key)->getData(true);$ok($proposal['id']===$retry['id'],'preparation replay returns original native proposal');$ok($proposal['impact']['physical_after']==='1000'&&$proposal['impact']['quantity_milli']==='0','allocation preview explicitly shows no second physical deduction');
$bad=false;try{$invoke('workshop_record','confirm',['reason'=>'Não confirmar a própria proposta.'],$proposal['id']);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$bad=$e->getStatusCode()===403;}$ok($bad,'technician cannot borrow warehouse physical authority');
$bad=false;try{$invoke('workshop_record','allocation',array_replace($data,['reason'=>'Conteúdo diferente não pode reutilizar a chave.']),null,$key);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$bad=$e->getStatusCode()===409;}$ok($bad,'same proposal key with changed content is refused across the bridge');
$host->beginTransaction();$native->beginTransaction();try{
$entry=$host->table('wo_entries')->where('id',$input['entries']['consumed'])->first();$change=json_decode($entry->data,true);unset($change['policy']);$tech=App\Models\User::findOrFail($profiles['workshop_record']['user_id']);Auth::guard('web')->setUser($tech);app(CompanyContext::class)->selectFor($tech->id,1,2,2);$change=array_replace($change,['version'=>2,'previous_id'=>$entry->id,'revision'=>(int)$host->selectOne('SELECT sgf_wo_revision(?) v',[$input['order']])->v,'correction_reason'=>'Nova versão apenas na transacção de regressão para recusar conferência antiga.']);app(App\Modules\Workshop\WorkshopWorkflow::class)->record($input['order'],$tech->id,$change,(string)Str::uuid());
$bad=false;try{$invoke('warehouse','confirm',['reason'=>'Conferência antiga após nova versão da origem.'],$proposal['id']);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$bad=$e->getStatusCode()===409;}$ok($bad,'new host source version refuses old native conference without physical write');
}finally{$native->rollBack();$host->rollBack();}
$result=$invoke('warehouse','confirm',['reason'=>'Conferência distinta do consumo e da entrega original.'],$proposal['id'])->getData(true);$ok($result['movement_id']===$physical['issue_movement'],'consumption links to the same existing issue movement');
$ok((int)$native->table('stock_movements')->where(['stock_item_id'=>$physical['stock_item'],'warehouse_id'=>$physical['warehouse']])->sum('quantity_milli')===1000,'consumption does not deduct stock twice');
$data['entry_id']=$input['entries']['returned'];$data['reason']='Meia unidade não usada, devolvida ao mesmo armazém com revisão distinta.';$return=$invoke('workshop_record','return',$data)->getData(true);$ok($return['impact']['physical_after']==='1500'&&$return['impact']['return_quantity_milli']==='500','return preview shows exact0.5UN addition and final1.5UN');
$bad=false;try{$invoke('warehouse','confirm',['reason'=>'Falta declarar recepção física.'],$return['id']);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$bad=$e->getStatusCode()===422;}$ok($bad,'return without explicit physical receipt is refused');
$native->unprepared("CREATE FUNCTION ws_bridge_fail() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''WS_BRIDGE_FAILURE''; END'");$native->unprepared('REVOKE ALL ON FUNCTION ws_bridge_fail() FROM PUBLIC');$native->unprepared('CREATE TRIGGER ws_bridge_fail BEFORE INSERT ON ws_returns FOR EACH ROW EXECUTE FUNCTION ws_bridge_fail()');$count=$native->table('stock_movements')->count();
$failed=false;try{$invoke('warehouse','confirm',['reason'=>'Conferência com falha injectada antes do recibo.','received_in_warehouse'=>true],$return['id']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$native->table('stock_movements')->count()===$count,'failure after physical writer rolls back native movement and provenance together');$native->unprepared('DROP TRIGGER ws_bridge_fail ON ws_returns');$native->unprepared('DROP FUNCTION ws_bridge_fail()');
$result=$invoke('warehouse','confirm',['reason'=>'Recepção física independente da meia unidade não usada.','received_in_warehouse'=>true],$return['id'])->getData(true);$retry=$invoke('warehouse','confirm',['reason'=>'Recuperar o recibo já gravado.','received_in_warehouse'=>true],$return['id'])->getData(true);$ok($result['movement_id']===$retry['movement_id']&&$retry['replayed'],'uncertain return confirmation recovers the same physical movement');
$movement=$native->table('stock_movements')->where('id',$result['movement_id'])->first();$ok((int)$movement->quantity_milli===500&&(int)$movement->moved_by===6,'original return writer records exact500 milli under native warehouse identity');
$ok($native->table('stock_movements')->where('stock_item_id',$physical['stock_item'])->count()===3&&(int)$native->table('stock_movements')->where('stock_item_id',$physical['stock_item'])->sum('quantity_milli')===1500,'one book contains actual receipt2, issue1 and return0.5');
$ok($before===$native->table('stock_movements')->where('stock_item_id','<>',$physical['stock_item'])->orderBy('id')->get()->toJson(),'every pre-existing physical movement outside the new article is preserved');
$ok($native->table('ws_allocations')->count()===1&&$native->table('ws_returns')->count()===1,'one native consumption link and one return provenance, no host balance copy');
$bookId=(int)$native->table('gl_stock_books')->where(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'stock_item_id'=>$physical['stock_item']])->value('id');
$rate=$call('accounting_prepare','prepare',fn($e,$a,$b,$o,$s,$stock)=>$stock->command('rate-prepare',['book_id'=>$bookId,'version'=>0,'unit_minor'=>5000,'reason'=>'Taxa artificial50MZN para o artigo físico seleccionado.'],(string)Str::uuid()));
$call('accounting_review','validate',fn($e,$a,$b,$o,$s,$stock)=>$stock->command('rate-validate',['rate_id'=>$rate['rate_id'],'version'=>1,'decision'=>'approved','reason'=>'Revisão independente da taxa50 do livro físico.'],(string)Str::uuid()));
$movements=$native->table('stock_movements')->where('stock_item_id',$physical['stock_item'])->orderBy('id')->get();$amounts=[];
foreach($movements as $movement){$type=Tempo\Finance\GeneralLedger\StockValuation::TYPES[$movement->movement_type];$p=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>$type,'source_id'=>$movement->id,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>$type,'reason'=>'Valorizar individualmente a origem física efectiva: '.$type],(string)Str::uuid()));$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão distinta do movimento físico e da valorização exacta.'],(string)Str::uuid()));$j=$call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Aplicação única após revalidar stock e evidência da Oficina.'],(string)Str::uuid()));$amounts[]=(int)$native->table('gl_journals')->where('id',$j['journal_id'])->value('debit_minor');}
$ok($amounts===[10000,5000,2500],'same ledger values actual receipt2/100, issue1/50 and independently received return0.5/25');
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok(true,'all three physical journals satisfy native balance constraints');
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
file_put_contents($work.'/workshop-stock-bridge-result.json',json_encode(['passed'=>true,'checks'=>$checks,'physical_writers_reused'=>true,'proof_changes_rolled_back'=>true,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/workshop-stock-bridge-result.json']).PHP_EOL;