16 lines
8.3 KiB
PHP
16 lines
8.3 KiB
PHP
<?php
|
|
require __DIR__.'/sgf-stock-catalogue-proof-bootstrap.php';use Illuminate\Support\Str;
|
|
$before=[];foreach(['stock_items','stock_movements','gl_journals','audit_logs']as$t)$before[$t]=$native->table($t)->orderBy('id')->get()->toJson();$grants=$host->table('permission_grants')->orderBy('id')->get()->toJson();$catalogueBefore=$native->table('sc_catalogue_versions')->count();$commandsBefore=$native->table('sc_catalogue_commands')->count();$host->beginTransaction();$native->beginTransaction();try{
|
|
$key=(string)Str::uuid();$data=['code'=>'QA-CAT-'.strtoupper(substr($key,0,8)),'name'=>'Artigo fictício de conferência','unit'=>'UN'];$prepare=$catalogueInput('prepare',$key,0,$data);$cut=$catalogueCall(2,'preview',$prepare);$ok($cut['status']===200&&!$cut['value']['impact']['creates_article'],'prepare preview no article');$ok($native->table('sc_catalogue_versions')->count()===$catalogueBefore&&$native->table('stock_items')->orderBy('id')->get()->toJson()===$before['stock_items'],'preview rolled back');[$v1,$firstCut,$commandKey]=$catalogueCommand(2,$prepare);$ok($v1['version']===1&&$v1['state']==='prepared'&&$v1['stock_item_id']===null,'prepare persists intention only');$again=$catalogueCall(2,'confirm',['preview'=>$firstCut['preview']]);$ok($again['value']['replayed']&&$again['value']['id']===$v1['id']&&$native->table('sc_catalogue_commands')->count()===$commandsBefore+1,'same key no duplicate preparation');
|
|
$rejectCatalogue(fn()=>$catalogueCall(2,'preview',$catalogueInput('review',$key,1,['decision'=>'approved'])),403,'preparer cannot self-review');$rejectCatalogue(fn()=>$catalogueCall(12,'preview',$prepare),403,'reviewer cannot prepare');$review=$catalogueCall(12,'preview',$catalogueInput('review',$key,1,['decision'=>'approved']));$ok($review['value']['impact']['creates_article']&&!$native->table('stock_items')->where('code',$data['code'])->exists(),'review preview shows creation but rolls back');$data['name']='Designação revista antes da decisão';[$v2]=$catalogueCommand(2,$catalogueInput('prepare',$key,1,$data));$rejectCatalogue(fn()=>$catalogueCall(12,'confirm',['preview'=>$review['value']['preview']]),409,'changed preparation invalidates old review');[$v3]=$catalogueCommand(12,$catalogueInput('review',$key,2,['decision'=>'approved']));$id=$v3['stock_item_id'];$item=$native->table('stock_items')->find($id);$ok($item&&$item->code===$data['code']&&$item->name===$data['name']&&$item->unit==='UN','approved article in canonical catalogue');$ok($native->table('stock_movements')->where('stock_item_id',$id)->count()===0,'new article no initial stock');
|
|
$rejectCatalogue(fn()=>$catalogueCall(2,'preview',$catalogueInput('prepare',(string)Str::uuid(),0,$data)),409,'existing code never automatically associated');$rejectCatalogue(fn()=>$catalogueCall(2,'preview',$catalogueInput('prepare',$key,3,array_replace($data,['unit'=>'KG']))),422,'unit immutable');$rejectCatalogue(fn()=>$catalogueCall(2,'preview',$catalogueInput('prepare',$key,3,array_replace($data,['code'=>'QA-OTHER']))),422,'code immutable');$rejectCatalogue(fn()=>$catalogueCall(2,'preview',$catalogueInput('prepare',$key,3,$data+['stock_item_id'=>1])),422,'item identity injection refused');
|
|
$data['name']='Designação de correcção recusada';[$v4]=$catalogueCommand(2,$catalogueInput('prepare',$key,3,$data));[$v5]=$catalogueCommand(12,$catalogueInput('review',$key,4,['decision'=>'refused']));$ok($native->table('stock_items')->find($id)->name===$item->name,'refusal leaves canonical article intact');$data['name']='Designação corrigida por revisão distinta';[$v6]=$catalogueCommand(2,$catalogueInput('prepare',$key,5,$data));[$v7]=$catalogueCommand(12,$catalogueInput('review',$key,6,['decision'=>'approved']));$renamed=$native->table('stock_items')->find($id);$ok($v7['stock_item_id']===$id&&$renamed->name===$data['name']&&$renamed->code===$item->code&&$renamed->unit===$item->unit,'rename only owned article same code unit');
|
|
$history=$catalogueCall(2,'index',['view'=>'history','record_key'=>$key,'per_page'=>25]);$ok($history['value']['records']['total']===7&&$history['value']['records']['data'][0]['version']===7,'seven immutable versions in history');$list=$catalogueCall(2,'index',['view'=>'items','search'=>$data['code'],'per_page'=>100]);$ok($list['value']['records']['total']===1&&$list['value']['records']['data'][0]['record_key']===$key,'registered article searchable through catalogue');$wild=$catalogueCall(2,'index',['view'=>'items','search'=>'%']);$ok(collect($wild['value']['records']['data'])->every(fn($r)=>str_contains($r['code'],'%')||str_contains($r['name'],'%')),'literal search cannot expand universe');$rejectCatalogue(fn()=>$catalogueCall(2,'index',['view'=>'history','record_key'=>(string)Str::uuid()]),403,'foreign history refused');$rejectCatalogue(fn()=>$catalogueCall(12,'receipt',[],$commandKey),404,'other actor receipt hidden');$own=$catalogueCall(2,'receipt',[],$commandKey,'fresh-session');$ok($own['value']['operation_executed']===false&&$own['value']['result']['id']===$v1['id'],'own historical receipt recovered fresh context');
|
|
$rejectCatalogue(fn()=>$catalogueCall(2,'preview',$prepare+['tenant_id'=>2]),422,'scope override refused');$rejectCatalogue(fn()=>$catalogueCall(2,'index',['per_page'=>1000]),422,'unbounded catalogue refused');
|
|
// Native transaction failure rolls back article, version, audit and receipt together.
|
|
$failureKey=(string)Str::uuid();$failureData=['code'=>'QA-FAIL-'.strtoupper(substr($failureKey,0,8)),'name'=>'Artigo de recuperação de falha','unit'=>'UN'];[$fv1]=$catalogueCommand(2,$catalogueInput('prepare',$failureKey,0,$failureData));$failure=$catalogueCall(12,'preview',$catalogueInput('review',$failureKey,1,['decision'=>'approved']));$native->unprepared("CREATE FUNCTION sc_test_fail_command() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'QA_TEST_FAILURE'; END; \$\$; CREATE TRIGGER sc_test_failure BEFORE INSERT ON sc_catalogue_commands FOR EACH ROW EXECUTE FUNCTION sc_test_fail_command();");$failed=false;try{$catalogueCall(12,'confirm',['preview'=>$failure['value']['preview']]);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&!$native->table('stock_items')->where('code',$failureData['code'])->exists()&&$native->table('sc_catalogue_versions')->where('record_key',$failureKey)->count()===1,'failed receipt rolls back new article and approval');$native->unprepared('DROP TRIGGER sc_test_failure ON sc_catalogue_commands; DROP FUNCTION sc_test_fail_command();');$recovered=$catalogueCall(12,'confirm',['preview'=>$failure['value']['preview']]);$ok($recovered['status']===201&&$native->table('stock_items')->where('code',$failureData['code'])->count()===1,'same confirmation recovers once after failure');
|
|
$stale=$catalogueCall(2,'preview',$catalogueInput('prepare',$key,7,array_replace($data,['name'=>'Designação ainda por decidir'])));$rejectCatalogue(fn()=>$catalogueCall(2,'confirm',['preview'=>$stale['value']['preview']],null,'changed-context'),409,'context generation change invalidates preview');$host->table('permission_grants')->where(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'user_id'=>2,'permission'=>'stock.items.prepare'])->delete();$rejectCatalogue(fn()=>$catalogueCall(2,'confirm',['preview'=>$stale['value']['preview']]),403,'permission revocation revalidated at confirmation');
|
|
$native->transaction(function()use($native,$ok,$v1){$denied=false;try{$native->table('sc_catalogue_versions')->where('id',$v1['id'])->update(['reason'=>'Tentativa de alteração indevida']);}catch(Illuminate\Database\QueryException$e){if(!str_contains($e->getMessage(),'SGF_CATALOGUE_APPEND_ONLY'))throw$e;$denied=true;throw new RuntimeException('EXPECTED_IMMUTABLE');}$ok($denied,'history immutable');});
|
|
}catch(RuntimeException$e){if($e->getMessage()!=='EXPECTED_IMMUTABLE')throw$e;$ok(true,'database refuses history mutation');}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
|
|
foreach($before as$t=>$json)$ok($native->table($t)->orderBy('id')->get()->toJson()===$json,'original '.$t.' unchanged');$ok($host->table('permission_grants')->orderBy('id')->get()->toJson()===$grants,'only approved grants remain');file_put_contents($work.'/stock-catalogue-result.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true,'new_demo'=>false,'new_financial_effects'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/stock-catalogue-result.json']).PHP_EOL;
|