Files
SGF/scripts/sgf-receivables-workflow.php
2026-11-11 09:00:17 +02:00

32 lines
5.8 KiB
PHP

<?php
use Illuminate\Support\Facades\{DB,Auth};
use Illuminate\Support\Str;
use App\Shared\Context\{CompanyContext,ModuleRecords};
use App\Modules\Services\ServiceReceivables;
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';
$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];$commercial=json_decode(file_get_contents($s['root'].'/single-commercial.json'),true);
config(['sgf_single_installation.enabled'=>true,'sgf_single_installation.schema_sha256'=>$host->table('sgf_composition_schema')->value('source_hash')]);
$target=$s['root'].'/single-receivables.json';if(is_file($target)){echo json_encode(['passed'=>true,'book'=>$book,'replayed'=>true,'new_business_commands'=>0]).PHP_EOL;exit;}
$select=function($role,$module='finance')use($app,$profiles,$f){$u=App\Models\User::findOrFail($profiles[$role]['user_id']);Auth::forgetGuards();Auth::shouldUse('web');Auth::guard('web')->setUser($u);$request=Illuminate\Http\Request::create('/api/service-receivables','POST');$request->attributes->set('sgf_module',$module);$request->setUserResolver(fn()=>$u);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>$module,'token'=>'fixture-'.$role]);$request->setLaravelSession($session);$app->instance('request',$request);app(CompanyContext::class)->selectFor($u->id,$f['tenant'],$f['company'],$f['unit']);return $u->id;};
$result=$host->transaction(function()use($host,$profiles,$commercial,$f,$select){
// Explicit per-record decisions only for records created by this known fixture.
// Never loop over legacy clients or propagate an association to unknown dependants.
foreach(['clients'=>'client','services'=>'service','service_drafts'=>'source','billing_drafts'=>'draft'] as $area=>$key){$id=$commercial[$key];$row=$host->table('a_'.$area)->where('id',$id)->first();if(!$row||!$host->table('a_history')->where(['area'=>$area,'record_id'=>$id,'version'=>1,'actor_id'=>$commercial['preparer']])->exists())throw new LogicException('New fixture origin required');foreach(['finance','accounting'] as $module){$selection=['tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>in_array($area,['clients','services'],true)?null:$f['unit']];ModuleRecords::associate($area,$id,$commercial['preparer'],$selection,$module,'Conferência individual deste novo registo QA para o percurso integrado autorizado.',false,(int)$row->version);}}
$service=app(ServiceReceivables::class);$prepare=$profiles['finance_prepare']['user_id'];$review=$profiles['finance_review']['user_id'];$invoice=$commercial['invoice'];
$select('finance_prepare');$ob=$service->propose($prepare,['kind'=>'obligation','invoice_id'=>$invoice,'currency'=>'MZN','source'=>'SIM-INTEGRADO','external_id'=>'SIM-ABERTURA-'.$invoice,'revision'=>0,'due_date'=>null],(string)Str::uuid())['id'];
$revision=fn()=>(int)$host->table('sr_effects')->where('obligation_id',$ob)->count();
$select('finance_review');$service->decide($ob,$review,['version'=>1,'revision'=>$revision(),'decision'=>'approved','reason'=>'Abertura conferida por pessoa distinta; vencimento não inventado.'],(string)Str::uuid());
$receipts=[];$allocations=[];
foreach(['400.00','600.00'] as $amount){
$select('finance_prepare');$receipt=$service->propose($prepare,['kind'=>'receipt','invoice_id'=>$invoice,'obligation_id'=>$ob,'currency'=>'MZN','amount'=>$amount,'source'=>'SIM-INTEGRADO','external_id'=>'SIM-RECEBIMENTO-'.$invoice.'-'.$amount,'revision'=>$revision(),'date'=>'2026-11-10','evidence'=>'Facto exclusivamente sintético; sem dinheiro real.'],(string)Str::uuid())['id'];
$select('finance_review');$service->decide($receipt,$review,['version'=>1,'revision'=>$revision(),'decision'=>'approved','reason'=>'Confirmação sintética independente do recebimento.'],(string)Str::uuid());
$select('finance_prepare');$key=(string)Str::uuid();$d=['amount'=>$amount,'version'=>1,'revision'=>$revision()];$allocation=$service->allocate($receipt,$prepare,$d,$key);$again=$service->allocate($receipt,$prepare,$d,$key);if($again['id']!==$allocation['id'])throw new LogicException('Duplicate receivable allocation');$receipts[]=$receipt;$allocations[]=$allocation['id'];
}
$select('finance_prepare');$beforeReversal=$service->position($ob,$prepare);if($beforeReversal['balance']!=='0.00')throw new LogicException('Expected full1000 allocation');
$correction=$service->propose($prepare,['kind'=>'correction','invoice_id'=>$invoice,'obligation_id'=>$ob,'currency'=>'MZN','source'=>'SIM-INTEGRADO','external_id'=>'SIM-REVERSAO-'.$invoice,'revision'=>$revision(),'action'=>'reverse_allocation','target_effect_id'=>$allocations[0],'reason'=>'Reversão explícita da afectação sintética400, preservando o original.'],(string)Str::uuid())['id'];
$select('finance_review');$service->decide($correction,$review,['version'=>1,'revision'=>$revision(),'decision'=>'approved','reason'=>'Conferência independente da reversão de400MZN.'],(string)Str::uuid());
$select('finance_prepare');$after=$service->position($ob,$prepare);if($after['balance']!=='400.00')throw new LogicException('Expected400 outstanding after reversal');
return ['invoice'=>$invoice,'obligation'=>$ob,'receipts'=>$receipts,'allocations'=>$allocations,'reversal_effect'=>(int)$host->table('sr_effects')->where('record_id',$correction)->where('kind','reverse_allocation')->value('id'),'before_reversal_balance'=>'0.00','balance'=>'400.00','synthetic_only'=>true,'distinct_actors'=>[$prepare,$review]];
});
file_put_contents($target,json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));file_put_contents($work.'/receivables-result.json',json_encode(['passed'=>true]+$result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['passed'=>true,'book'=>$book,'balance'=>'400.00','proof'=>$work.'/receivables-result.json']).PHP_EOL;