table($t)->orderBy('id')->get()->toJson();$grants=$host->table('permission_grants')->orderBy('id')->get()->toJson();$baseVersions=$native->table('pd_due_versions')->count();$baseCommands=$native->table('pd_due_commands')->count();$id=(int)$native->table('pa_obligations')->orderBy('id')->value('id');$offset=(int)$native->table('pd_due_versions')->where('obligation_id',$id)->max('version');$historyBefore=$native->table('pd_due_versions')->where('obligation_id',$id)->count();$relativeInput=$dueInput;$dueInput=fn($action,$id,$version,$data)=>$relativeInput($action,$id,$version+$offset,$data);$query=['obligation_id'=>$id,'reference_on'=>'2026-11-30'];$data=['due_on'=>'2026-11-30','evidence'=>'QA artificial; termos de teste, não condição real.']; $host->beginTransaction();$native->beginTransaction();try{ $initial=$dueCall(17,'index',$query);$ok($initial['value']['basis']['position']['days_overdue']===null,'original unknown due not zero'); $input=$dueInput('prepare',$id,0,$data);$cut=$dueCall(13,'preview',$input);$ok($cut['status']===200&&$cut['value']['impact']['original_obligation_changed']===false,'preview explicit non-monetary impact');$ok($native->table('pd_due_versions')->count()===$baseVersions,'preview rolled back'); [$v1,$first,$key]=$dueCommand(13,$input);$ok($v1['state']==='prepared'&&$v1['version']===$offset+1,'preparation persisted');$replay=$dueCall(13,'confirm',['preview'=>$first['preview']]);$ok($replay['value']['replayed']&&$replay['value']['id']===$v1['id']&&$native->table('pd_due_commands')->count()===$baseCommands+1,'repetition no duplicate'); $rejectCatalogue(fn()=>$dueCall(13,'preview',$dueInput('review',$id,1,['decision'=>'approved'])),403,'own review refused');$rejectCatalogue(fn()=>$dueCall(14,'preview',$input),403,'reviewer cannot prepare');$rejectCatalogue(fn()=>$dueCall(17,'preview',$input),403,'reader cannot prepare'); $oldReview=$dueCall(14,'preview',$dueInput('review',$id,1,['decision'=>'approved']));[$v2]=$dueCommand(13,$dueInput('prepare',$id,1,$data));$rejectCatalogue(fn()=>$dueCall(14,'confirm',['preview'=>$oldReview['value']['preview']]),409,'stale review refused'); [$v3]=$dueCommand(14,$dueInput('review',$id,2,['decision'=>'approved']));$ok($v3['state']==='approved','distinct review applied');$read=$dueCall(17,'index',$query);$ok($read['value']['days_overdue']===0&&$read['value']['due_status']==='not_overdue'&&$read['value']['basis']['position']['due_on']===null,'same day not overdue original date intact');$later=$dueCall(17,'index',array_replace($query,['reference_on'=>'2026-12-01']));$ok($later['value']['days_overdue']===1,'explicit next day one day overdue');$earlier=$dueCall(17,'index',array_replace($query,['reference_on'=>'2026-11-29']));$ok($earlier['value']['days_overdue']===0,'future due no negative delay'); [$v4]=$dueCommand(13,$dueInput('prepare',$id,3,['due_on'=>null,'evidence'=>'QA correcção explícita para desconhecido.']));[$v5]=$dueCommand(14,$dueInput('review',$id,4,['decision'=>'refused']));$ok($dueCall(17,'index',$query)['value']['reviewed_terms']['due_on']==='2026-11-30','refusal preserves previous terms'); [$v6]=$dueCommand(13,$dueInput('prepare',$id,5,['due_on'=>null,'evidence'=>'QA correcção aprovada para desconhecido.']));[$v7]=$dueCommand(14,$dueInput('review',$id,6,['decision'=>'approved']));$unknown=$dueCall(17,'index',$query);$ok($unknown['value']['days_overdue']===null&&$unknown['value']['due_status']==='unknown','approved clear returns unknown'); $h100=$dueCall(17,'index',$query+['per_page'=>100]);$ok($h100['value']['history']['total']===$historyBefore+7&&$h100['value']['history']['data']===$unknown['value']['history']['data'],'history25/100 equivalent');$empty=$dueCall(17,'index',$query+['page'=>100000]);$ok($empty['value']['history']['data']===[],'history out of range no fallback'); $receipt=$dueCall(13,'receipt',[],$key,'fresh');$ok($receipt['value']['operation_executed']===false&&$receipt['value']['result']['id']===$v1['id'],'own receipt fresh session');$rejectCatalogue(fn()=>$dueCall(14,'receipt',[],$key),404,'other receipt hidden'); $rejectCatalogue(fn()=>$dueCall(17,'index',$query+['per_page'=>1000]),422,'unbounded history refused');$rejectCatalogue(fn()=>$dueCall(17,'index',$query+['tenant_id'=>2]),422,'context override refused');$rejectCatalogue(fn()=>$dueCall(17,'index',array_replace($query,['obligation_id'=>2147483647])),403,'foreign obligation refused');$rejectCatalogue(fn()=>$dueCall(17,'index',$query,null,'due-proof','accounting'),409,'wrong module refused');$rejectCatalogue(fn()=>$dueCall(13,'preview',array_replace($input,['data'=>['due_on'=>'2026-02-30','evidence'=>$data['evidence']]])),422,'impossible date refused'); $fresh=$dueCall(13,'preview',$dueInput('prepare',$id,7,$data));$rejectCatalogue(fn()=>$dueCall(13,'confirm',['preview'=>$fresh['value']['preview']],null,'other-generation'),409,'changed context invalidates confirmation'); // Original source mutation only inside a nested savepoint; no real workflow repeated. $native->beginTransaction();try{$sourceId=$native->table('pa_obligations as o')->join('pa_sources as s','s.id','=','o.source_id')->where('o.id',$id)->value('s.local_source_id');$native->table('invoices')->where('id',$sourceId)->update(['number'=>'QA-ORIGEM-ALTERADA-APENAS-SAVEPOINT']);$rejectCatalogue(fn()=>$dueCall(13,'confirm',['preview'=>$fresh['value']['preview']]),409,'changed financial basis invalidates confirmation');}finally{$native->rollBack();} // Force failure at final receipt insertion and then recover the same cut. $native->unprepared("CREATE FUNCTION pd_test_fail() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'QA_TEST_FAILURE'; END; \$\$; CREATE TRIGGER pd_test_failure BEFORE INSERT ON pd_due_commands FOR EACH ROW EXECUTE FUNCTION pd_test_fail();");$failed=false;try{$dueCall(13,'confirm',['preview'=>$fresh['value']['preview']]);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$native->table('pd_due_versions')->where('obligation_id',$id)->count()===$historyBefore+7,'failed receipt rolls back version and audit');$native->unprepared('DROP TRIGGER pd_test_failure ON pd_due_commands; DROP FUNCTION pd_test_fail();');$recovered=$dueCall(13,'confirm',['preview'=>$fresh['value']['preview']]);$ok($recovered['status']===201&&$recovered['value']['version']===$offset+8,'same cut recovers once after failure'); $next=$dueCall(13,'preview',$dueInput('prepare',$id,8,$data));$host->table('permission_grants')->where(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'user_id'=>13,'permission'=>'supplier_payables.due.prepare'])->delete();$rejectCatalogue(fn()=>$dueCall(13,'confirm',['preview'=>$next['value']['preview']]),403,'revoked right rechecked'); $immutable=false;$native->beginTransaction();try{$native->table('pd_due_versions')->where('id',$v1['id'])->update(['reason'=>'Alteração indevida do histórico']);}catch(Illuminate\Database\QueryException$e){$immutable=str_contains($e->getMessage(),'SGF_PAYABLE_DUE_APPEND_ONLY');}finally{$native->rollBack();}$ok($immutable,'history immutable'); }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,'grants unchanged by tests');$ok($native->table('pd_due_versions')->count()===$baseVersions&&$native->table('pd_due_commands')->count()===$baseCommands,'all regression versions and commands rolled back'); file_put_contents($work.'/payable-due-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.'/payable-due-result.json']).PHP_EOL;