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;