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

56 lines
9.6 KiB
PHP

<?php
use Illuminate\Support\Facades\{Auth,DB};use Illuminate\Support\Str;use App\Shared\Context\CompanyContext;
require __DIR__.'/sgf-stock-final-proof-bootstrap.php';
$policy=json_decode(file_get_contents($s['root'].'/stock-completion-binding.json'),true);config(['stock_completion.enabled'=>true,'stock_completion.binding_sha256'=>hash_file('sha256',$s['root'].'/stock-completion-binding.json')]);
$invoke=function($role,$method,$data=[])use($app,$f,$profiles){
Auth::forgetGuards();Auth::shouldUse('web');$user=App\Models\User::findOrFail($role==='warehouse'?$f['validator']:$profiles[$role]['user_id']);Auth::guard('web')->setUser($user);
$r=Illuminate\Http\Request::create('/api/stock-completion',$method==='index'?'GET':'POST',$data);$r->attributes->set('sgf_module','stock');$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>'stock','token'=>'completion-'.$role]);$r->setLaravelSession($session);$r->setUserResolver(fn()=>$user);$app->instance('request',$r);app(CompanyContext::class)->selectFor($user->id,1,2,2);
return app(App\Modules\Inventory\StockCompletionController::class)->$method($r,app(App\Modules\Purchasing\ReceiptRuntime::class))->getData(true);
};
$prepare=function($role,$family,$action,$data=[],$record=null,$version=0)use($invoke){$state=$invoke($role,'index',['family'=>$family]);return $invoke($role,'preview',['family'=>$family,'action'=>$action,'data'=>$data,'record_key'=>$record,'version'=>$version,'operation_key'=>(string)Str::uuid(),'revision'=>$state['revision'],'reason'=>'Conferência QA individual da origem, versão e impacto.']);};
$command=function($role,$family,$action,$data=[],$record=null,$version=0)use($invoke,$prepare){$p=$prepare($role,$family,$action,$data,$record,$version);return [$invoke($role,'confirm',['preview'=>$p['preview']]),$p];};
$host->beginTransaction();$native->beginTransaction();$before=$native->table('stock_movements')->orderBy('id')->get()->toJson();$journals=$native->table('gl_journals')->orderBy('id')->get()->toJson();
try{
$state=$invoke('warehouse','index',['family'=>'policy']);$ok($state['basis']['balance']['quantity']===10000,'selected new article has10UN from original receipt writer');
$reject(fn()=>$invoke('read_only','index',['family'=>'policy']),403,'unbound reader cannot obtain policy authority');
$data=['stock_item_id'=>$policy['stock_item_id'],'warehouse_id'=>$policy['warehouse_id'],'forecast_method'=>'manual','purpose'=>'internal','origin'=>'local','safety_milli'=>2000,'minimum_stock_milli'=>5000,'target_milli'=>10000,'maximum_milli'=>20000,'daily_milli'=>1000,'lead_days'=>2,'approval_days'=>1,'horizon_days'=>14,'minimum_milli'=>1000,'pack_milli'=>1000,'basis'=>'Quantidades artificiais e prazo conhecido para ensaio.'];
$reject(fn()=>$command('warehouse','policy','save',array_replace($data,['stock_item_id'=>999999])),403,'foreign item refused before native preparation');
[$draft]=$command('warehouse','policy','save',$data);$ok($draft['impact']['physical_change_milli']===0,'minimum policy draft does not purchase or move stock');
$reject(fn()=>$command('warehouse','policy','approve',[],$draft['record_key'],1),403,'preparer cannot approve its policy');
[$approved]=$command('workshop_verify','policy','approve',[],$draft['record_key'],1);$ok($approved['version']===2&&$approved['impact']['state']==='approved','distinct native reviewer approves exact policy version');
$event=['kind'=>'supplier_return','stock_item_id'=>$policy['stock_item_id'],'warehouse_id'=>$policy['warehouse_id'],'receipt_item_id'=>$policy['receipt_item_id'],'reference'=>'QA-RETURN-'.Str::uuid(),'quantity'=>'2.000'];
$reject(fn()=>$command('warehouse','supplier_return','save',array_replace($event,['quantity'=>'10.001'])),422,'return cannot exceed physical receipt');
$reject(fn()=>$command('warehouse','supplier_return','save',array_replace($event,['receipt_item_id'=>1])),403,'unselected historical receipt refused');
[$draft]=$command('warehouse','supplier_return','save',$event);$ok($draft['impact']['after_milli']===10000,'return draft retains all10UN');
$stale=$prepare('workshop_verify','supplier_return','approve',[],$draft['record_key'],1);
[$approved]=$command('workshop_verify','supplier_return','approve',[],$draft['record_key'],1);$reject(fn()=>$invoke('workshop_verify','confirm',['preview'=>$stale['preview']]),409,'competing approval invalidates old conference');
$reject(fn()=>$command('workshop_verify','supplier_return','execute',['event_on'=>'2026-11-10','evidence'=>'Saída física comprovada em guia de teste.'],$draft['record_key'],2),403,'reviewer cannot execute its approved return');
$pending=$prepare('warehouse','supplier_return','execute',['event_on'=>'2026-11-10','evidence'=>'Saída física comprovada em guia de teste.'],$draft['record_key'],2);
$ok($native->table('stock_movements')->orderBy('id')->get()->toJson()===$before,'preview does not commit any physical movement');
$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>$f['validator'],'permission'=>'stock.operations.confirm'])->delete();$reject(fn()=>$invoke('warehouse','confirm',['preview'=>$pending['preview']]),403,'revoked authority refuses a prepared physical confirmation');}finally{$host->rollBack();}
$native->unprepared("CREATE FUNCTION sgf_completion_fail() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''SGF_COMPLETION_FAILURE''; END'");$native->unprepared('REVOKE ALL ON FUNCTION sgf_completion_fail() FROM PUBLIC');$native->unprepared("CREATE TRIGGER sgf_completion_fail BEFORE INSERT ON audit_logs FOR EACH ROW WHEN(NEW.action='sgf.stock.completion.command') EXECUTE FUNCTION sgf_completion_fail()");
$failed=false;try{$invoke('warehouse','confirm',['preview'=>$pending['preview']]);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$native->table('stock_movements')->orderBy('id')->get()->toJson()===$before,'audit failure rolls back native return and preserves uncertain retry');
$native->unprepared('DROP TRIGGER sgf_completion_fail ON audit_logs');$native->unprepared('DROP FUNCTION sgf_completion_fail()');
$done=$invoke('warehouse','confirm',['preview'=>$pending['preview']]);$again=$invoke('warehouse','confirm',['preview'=>$pending['preview']]);
$ok($done['impact']['after_milli']===8000&&$done['impact']['physical_change_milli']===-2000,'one native supplier return removes exactly2UN leaving8UN');
$ok($again['replayed']&&$again['record_id']===$done['record_id'],'uncertain response recovers original result without duplication');
$ok($native->table('gl_journals')->orderBy('id')->get()->toJson()===$journals,'physical return does not automatically create credit or accounting');
$bookId=(int)$native->table('gl_stock_books')->where($f['scope'])->where('stock_item_id',$policy['stock_item_id'])->value('id');
$rate=$call('accounting_prepare','prepare',fn($e,$a,$b,$o,$sales,$stock)=>$stock->command('rate-prepare',['book_id'=>$bookId,'version'=>0,'unit_minor'=>2000,'reason'=>'Taxa artificial20MZN por UN para origem nova de fornecedor.'],(string)Str::uuid()));
$call('accounting_review','validate',fn($e,$a,$b,$o,$sales,$stock)=>$stock->command('rate-validate',['rate_id'=>$rate['rate_id'],'version'=>1,'decision'=>'approved','reason'=>'Revisão distinta da taxa20 e da origem nova seleccionada.'],(string)Str::uuid()));
$amounts=[];foreach($native->table('stock_movements')->where('stock_item_id',$policy['stock_item_id'])->orderBy('id')->get() as $m){
$kind=Tempo\Finance\GeneralLedger\StockValuation::TYPES[$m->movement_type];
$proposal=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>$kind,'source_id'=>$m->id,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>$kind,'reason'=>'Valorização individual da entrada ou devolução efectiva ao fornecedor.'],(string)Str::uuid()));
$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$proposal['proposal_id'],'revision'=>$proposal['revision'],'decision'=>'approved','reason'=>'Conferência distinta da origem física, revisão e taxa20.'],(string)Str::uuid()));
$posted=$call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$proposal['proposal_id'],'revision'=>$proposal['revision'],'reason'=>'Aplicação única da origem seleccionada e revista distintamente.'],(string)Str::uuid()));
$amounts[]=(int)$native->table('gl_journals')->where('id',$posted['journal_id'])->value('debit_minor');
}
$ok($amounts===[20000,4000],'same ledger values200MZN receipt and40MZN supplier return');
$asset=$native->table('gl_lines as l')->join('gl_accounts as a','a.id','=','l.account_id')->where('a.code','QA-STOCK')->selectRaw('sum(l.debit_minor-l.credit_minor) amount')->value('amount');
$ok((int)$asset===16000,'new QA book has8UN valued160MZN without supplier debt credit');
$ok($native->table('requisitions')->where('number','like','%AUTO%')->count()===0,'policy and return do not generate automatic purchases');
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok(true,'native deferred physical constraints validated');
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
$ok($native->table('stock_movements')->orderBy('id')->get()->toJson()===$before,'regression mutations fully rolled back');
file_put_contents($work.'/stock-completion-result.json',json_encode(['passed'=>true,'checks'=>$checks,'rolled_back'=>true,'visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/stock-completion-result.json']).PHP_EOL;