Accept isolated WORKSHOP-RESOURCE-RECORD-v1 delivery
Record explicit workshop acceptance and preserve subsequent synthetic work without accepting receivables. Keep 080000/070000 unapplied, real workshop grants pending and operational issuance blocks intact. Exclude local credentials and real-data preservation reports.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
use App\Modules\Services\ServiceInvoicing;
|
||||
use App\Modules\Services\ServiceReceivables;
|
||||
use App\Shared\Context\CompanyContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Process\Process;
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
ServiceReceivables::guard();
|
||||
$f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);
|
||||
$fx=$f+$f['receivables']+['manager'=>\App\Models\User::findOrFail($f['manager_id']),'reader'=>\App\Models\User::findOrFail($f['reader_id'])];
|
||||
$s=app(ServiceReceivables::class); $manager=$f['manager_id']; $reader=$f['reader_id']; $proof=[];
|
||||
$select=fn($actor)=>app(CompanyContext::class)->selectFor($actor,$f['tenant'],$f['company'],$f['unit']);
|
||||
$rev=fn($ob)=>DB::table('sr_effects')->where('obligation_id',$ob)->count();
|
||||
$base=['tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit']];
|
||||
$pair=function($a,$b)use($base) {
|
||||
$a=new Process([PHP_BINARY,'scripts/receivables-worker.php','--env=testing'],base_path(),null,json_encode($a+$base+['pause'=>true]));
|
||||
$b=new Process([PHP_BINARY,'scripts/receivables-worker.php','--env=testing'],base_path(),null,json_encode($b+$base));
|
||||
$a->start(); if(!$a->waitUntil(fn($type,$buffer)=>str_contains($buffer,'LOCK_HELD'))) throw new RuntimeException('Overlap lock not established.');
|
||||
$b->start(); $a->wait(); $b->wait(); $out=[];
|
||||
foreach([$a,$b] as $p) { if(!$p->isSuccessful()) throw new RuntimeException('Synthetic worker failed.'); $lines=array_values(array_filter(explode("\n",trim($p->getOutput())),fn($v)=>str_starts_with($v,'{'))); $out[]=json_decode(end($lines),true,32,JSON_THROW_ON_ERROR); }
|
||||
return $out;
|
||||
};
|
||||
$obligation=function()use(&$fx,$s,$select,$manager,$reader) {
|
||||
$fx=\Tests\Support\InvoiceFixture::additionalDraft($fx,'862.07'); $select($manager); $si=app(ServiceInvoicing::class);
|
||||
$review=$si->review($fx['draft'],$fx['series'],$fx['rule'],$manager);
|
||||
$invoice=$si->issue($fx['draft'],$manager,['series_id'=>$fx['series'],'rule_id'=>$fx['rule'],'review_hash'=>$review['review_hash']],(string)Str::uuid())['id'];
|
||||
$ob=$s->propose($manager,['kind'=>'obligation','invoice_id'=>$invoice,'currency'=>'MZN','source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'revision'=>0],(string)Str::uuid())['id'];
|
||||
$select($reader); $s->decide($ob,$reader,['version'=>1,'revision'=>0,'decision'=>'approved','reason'=>'Open synthetic concurrency obligation.'],(string)Str::uuid());
|
||||
return [$invoice,$ob];
|
||||
};
|
||||
$receipt=function($invoice,$ob,$amount,$confirm=true)use($s,$select,$manager,$reader,$rev) {
|
||||
$select($manager); $id=$s->propose($manager,['kind'=>'receipt','invoice_id'=>$invoice,'obligation_id'=>$ob,'amount'=>$amount,'currency'=>'MZN','date'=>now(ServiceInvoicing::ZONE)->format('Y-m-d'),'source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'evidence'=>'Artificial concurrency evidence.','revision'=>$rev($ob)],(string)Str::uuid())['id'];
|
||||
if($confirm) { $select($reader); $s->decide($id,$reader,['version'=>1,'revision'=>$rev($ob),'decision'=>'approved','reason'=>'Synthetic concurrency confirmation.'],(string)Str::uuid()); }
|
||||
return $id;
|
||||
};
|
||||
$decision=fn($id,$ob)=>['method'=>'decide','id'=>$id,'actor'=>$reader,'key'=>(string)Str::uuid(),'input'=>['version'=>1,'revision'=>$rev($ob),'decision'=>'approved','reason'=>'Concurrent synthetic decision.']];
|
||||
[$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'400.00',false); $a=$decision($r,$ob); $out=$pair($a,$a);
|
||||
if($out[0]['status']!==200 || $out[1]['status']!==200 || !$out[1]['result']['replayed'] || $out[0]['result']['id']!==$out[1]['result']['id']) throw new RuntimeException('Confirmation replay failed.');
|
||||
$proof['same_confirmation_one_effect_one_replay']=true;
|
||||
[$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'400.00'); $t=$receipt($invoice,$ob,'700.00');
|
||||
$allocate=fn($id,$amount,$ob)=>['method'=>'allocate','id'=>$id,'actor'=>$manager,'key'=>(string)Str::uuid(),'input'=>['version'=>1,'revision'=>$rev($ob),'amount'=>$amount]];
|
||||
$out=$pair($allocate($r,'400.00',$ob),$allocate($t,'700.00',$ob));
|
||||
if(array_column($out,'status')!==[200,409]) throw new RuntimeException('Document overspend race failed.');
|
||||
$proof['document_400_plus_700_one_applied_one_conflict']=true;
|
||||
[$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'700.00');
|
||||
$out=$pair($allocate($r,'400.00',$ob),$allocate($r,'400.00',$ob));
|
||||
if(array_column($out,'status')!==[200,409]) throw new RuntimeException('Receipt overspend race failed.');
|
||||
$proof['same_receipt_available_not_double_spent']=true;
|
||||
// Raw SQL writers share the same company lock and aggregate/version invariants.
|
||||
[$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'700.00'); $t=$receipt($invoice,$ob,'400.00');
|
||||
$sql=fn($id,$amount)=>['method'=>'sql_allocate','actor'=>$manager,'sql'=>['record_id'=>$id,'obligation_id'=>$ob,'receipt_id'=>$id,'kind'=>'allocate','amount'=>$amount,'revision'=>$rev($ob),'data'=>'{}','actor_id'=>$manager,'created_at'=>now()->toIso8601String()]];
|
||||
$out=$pair($sql($r,'700.00'),$sql($t,'400.00'));
|
||||
if(array_column($out,'status')!==[200,409] || !($out[1]['database_guard']??false)) throw new RuntimeException('SQL aggregate race failed.');
|
||||
$proof['raw_sql_concurrent_aggregate_guard']=true;
|
||||
[$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'400.00'); $select($manager); $af=$s->allocate($r,$manager,['version'=>1,'revision'=>$rev($ob),'amount'=>'400.00'],(string)Str::uuid())['id'];
|
||||
$obs=$s->propose($manager,['kind'=>'observation','invoice_id'=>$invoice,'obligation_id'=>$ob,'receipt_id'=>$r,'amount'=>'400.00','currency'=>'MZN','date'=>now(ServiceInvoicing::ZONE)->format('Y-m-d'),'source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'evidence'=>'Artificial observation.','revision'=>$rev($ob)],(string)Str::uuid())['id'];
|
||||
$corr=$s->propose($manager,['kind'=>'correction','action'=>'reverse_allocation','target_effect_id'=>$af,'invoice_id'=>$invoice,'obligation_id'=>$ob,'currency'=>'MZN','source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'reason'=>'Explicit reversal.','revision'=>$rev($ob)],(string)Str::uuid())['id'];
|
||||
$out=$pair($decision($corr,$ob),$decision($obs,$ob));
|
||||
if(array_column($out,'status')!==[200,409]) throw new RuntimeException('Reversal versus reconciliation race failed.');
|
||||
$proof['reversal_reconciliation_stale_decision_refused']=true;
|
||||
// Two incompatible reductions against exactly the same reviewed revision.
|
||||
[$invoice,$ob]=$obligation(); $select($manager); $corrs=[];
|
||||
foreach(['A','B'] as $ref) $corrs[]=$s->propose($manager,['kind'=>'correction','action'=>'reduction','amount'=>'100.00','document_ref'=>'TEST-REDUCTION-CONCURRENT-'.Str::uuid(),'line'=>'1','invoice_id'=>$invoice,'obligation_id'=>$ob,'currency'=>'MZN','source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'reason'=>'Synthetic reduction proposal '.$ref,'revision'=>$rev($ob)],(string)Str::uuid())['id'];
|
||||
$out=$pair($decision($corrs[0],$ob),$decision($corrs[1],$ob));
|
||||
if(array_column($out,'status')!==[200,409]) throw new RuntimeException('Concurrent corrections failed.');
|
||||
$proof['concurrent_corrections_exact_revision']=true;
|
||||
file_put_contents(base_path('docs/proofs/receivables-concurrency.json'),json_encode(['database'=>'sgf_tempo_test','overlapping_processes_per_case'=>2,'checks'=>$proof,'real_operations'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "Six real overlapping-process scenarios passed, including raw SQL aggregate protection; sgf_tempo_test only.\n";
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Services\ServiceReceivables::guard();
|
||||
$file='C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';
|
||||
$f=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);
|
||||
if(\Illuminate\Support\Facades\DB::table('si_proposals')->exists() || \Illuminate\Support\Facades\DB::table('sr_records')->exists()) throw new RuntimeException('Test fixture must be empty; nothing deleted.');
|
||||
$fixture=\Illuminate\Support\Facades\DB::transaction(fn()=>\Tests\Support\ReceivablesFixture::create($f+['manager'=>\App\Models\User::findOrFail($f['manager_id']),'reader'=>\App\Models\User::findOrFail($f['reader_id'])]));
|
||||
$ids=array_intersect_key($fixture,array_flip(['client','service','source','draft','profile','temporal','calculation','series','rule','today','start','end','invoice']));
|
||||
file_put_contents($file,json_encode($f+['receivables'=>$ids],JSON_THROW_ON_ERROR));
|
||||
echo "Receivables fixture ready only in sgf_tempo_test: document 1000.00 MZN; no obligation, receipt or allocation pre-created.\n";
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
// Read-only proposal. Deliberately has no apply mode.
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
if(!\App\Modules\Identity\Services\LocalDatabaseGuard::allows()) throw new RuntimeException('Development read-only scope refused.');
|
||||
$db=\Illuminate\Support\Facades\DB::class;
|
||||
$company=$db::table('companies')->where('id',1)->first(['id','tenant_id','name']);
|
||||
$unit=$db::table('units')->where('id',1)->where('company_id',1)->first(['id','name']);
|
||||
if(!$company || !$unit) throw new RuntimeException('Proposed scope not found; no operation performed.');
|
||||
$rows=[];
|
||||
foreach([1=>['view','prepare','allocate','correction_request'],2=>['view','confirm','reconcile','correction_approve']] as $id=>$rights) {
|
||||
$user=$db::table('users')->where('id',$id)->first(['id','name','email']);
|
||||
if(!$user || !$db::table('company_memberships')->where(['user_id'=>$id,'tenant_id'=>$company->tenant_id,'company_id'=>1])->exists()) throw new RuntimeException('Existing account/scope not confirmed.');
|
||||
foreach($rights as $right) $rows[]=['user_id'=>$id,'name'=>$user->name,'email'=>$user->email,'tenant_id'=>$company->tenant_id,'company_id'=>1,'company'=>$company->name,'unit_id'=>1,'unit'=>$unit->name,'permission'=>'service_receivables.'.$right,'already_granted'=>$db::table('permission_grants')->where(['user_id'=>$id,'company_id'=>1,'unit_id'=>1,'permission'=>'service_receivables.'.$right])->exists()];
|
||||
}
|
||||
echo json_encode(['mode'=>'preview_only','grants'=>$rows,'applied'=>false],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Services\ServiceReceivables::guard();
|
||||
$f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($f['reader_id'],$f['tenant'],$f['company'],$f['unit']);
|
||||
$s=app(\App\Modules\Services\ServiceReceivables::class); $all=$s->listing($f['reader_id']);
|
||||
$record=collect($all['records'])->first(fn($r)=>$r->kind==='obligation' && $r->invoice_id===$f['receivables']['invoice']);
|
||||
$ob=collect($all['obligations'])->firstWhere('id',$record->id);
|
||||
if($ob['balance']!=='0.00' || $ob['received']!=='1000.00' || $ob['allocated']!=='1000.00' || $ob['available']!=='0.00' || count($ob['receipts'])!==2) throw new RuntimeException('Browser scenario totals not confirmed.');
|
||||
foreach($ob['receipts'] as $rc) if(!$rc['reconciled']) throw new RuntimeException('Synthetic reconciliation missing.');
|
||||
$events=\Illuminate\Support\Facades\DB::table('sr_events')->pluck('id'); $delivered=0;
|
||||
foreach($events as $event) if($s->deliver($event)) $delivered++;
|
||||
$out=['database'=>'sgf_tempo_test','simulation_only'=>true,'policy'=>$s::POLICY,'notice'=>$s::NOTICE,'document_id'=>$record->invoice_id,'obligation_id'=>$ob['id'],'original'=>$ob['original'],'received'=>$ob['received'],'allocated'=>$ob['allocated'],'unallocated'=>$ob['available'],'balance'=>$ob['balance'],'receipts'=>array_values($ob['receipts']),'revision'=>$ob['revision'],'real_login_browser'=>true,'csrf_enabled'=>true,'actors_distinct'=>$f['manager_id']!==$f['reader_id'],'durable_events_delivered'=>$delivered,'no_fiscal_receipt_or_new_pdf'=>true];
|
||||
file_put_contents(base_path('docs/proofs/receivables-browser-results.json'),json_encode($out,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "Browser scenario verified: 1000.00 - 400.00 - 600.00 = 0.00; two simulated reconciliations; durable events delivered internally.\n";
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Services\ServiceReceivables::guard();
|
||||
$in=json_decode(stream_get_contents(STDIN),true,32,JSON_THROW_ON_ERROR);
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($in['actor'],$in['tenant'],$in['company'],$in['unit']);
|
||||
if($in['pause']??false) {
|
||||
$paused=false; \Illuminate\Support\Facades\DB::listen(function($q)use(&$paused){
|
||||
if(!$paused && str_contains($q->sql,'"companies"') && str_contains($q->sql,'for update')) { $paused=true; echo "LOCK_HELD\n"; flush(); usleep(1200000); }
|
||||
});
|
||||
}
|
||||
try {
|
||||
$s=app(\App\Modules\Services\ServiceReceivables::class);
|
||||
if($in['method']==='sql_allocate') {
|
||||
$result=\Illuminate\Support\Facades\DB::transaction(function()use($in) {
|
||||
if($in['pause']??false) { \Illuminate\Support\Facades\DB::table('companies')->where('id',$in['company'])->lockForUpdate()->firstOrFail(); }
|
||||
$id=\Illuminate\Support\Facades\DB::table('sr_effects')->insertGetId($in['sql']); return ['id'=>$id];
|
||||
});
|
||||
} else $result=$s->{$in['method']}($in['id'],$in['actor'],$in['input'],$in['key']);
|
||||
echo json_encode(['status'=>200,'result'=>$result],JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
} catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e) {
|
||||
echo json_encode(['status'=>$e->getStatusCode()],JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
} catch(\Illuminate\Database\QueryException $e) {
|
||||
if(!str_contains($e->getMessage(),'SGF_SR_')) throw $e;
|
||||
echo json_encode(['status'=>409,'database_guard'=>true],JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
use App\Modules\Workshop\WorkshopWorkflow;
|
||||
use App\Shared\Context\CompanyContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Process\Process;
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();WorkshopWorkflow::guard();
|
||||
$f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);$fx=$f['workshop'];$w=app(WorkshopWorkflow::class);
|
||||
$base=['tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit']];$proof=[];
|
||||
$select=fn($actor)=>app(CompanyContext::class)->selectFor($actor,$f['tenant'],$f['company'],$f['unit']);
|
||||
$key=fn()=>(string)Str::uuid();$rev=fn($id)=>(int)DB::selectOne('SELECT sgf_wo_revision(?) AS v',[$id])->v;
|
||||
$pair=function($one,$two)use($base){
|
||||
$a=new Process([PHP_BINARY,'scripts/workshop-worker.php','--env=testing'],base_path(),null,json_encode($one+$base+['pause'=>true]));
|
||||
$b=new Process([PHP_BINARY,'scripts/workshop-worker.php','--env=testing'],base_path(),null,json_encode($two+$base));
|
||||
$a->start();if(!$a->waitUntil(fn($t,$v)=>str_contains($v,'LOCK_HELD')))throw new RuntimeException('No overlapping lock.');$b->start();$a->wait();$b->wait();$out=[];
|
||||
foreach([$a,$b] as $p){if(!$p->isSuccessful())throw new RuntimeException('Synthetic worker failed.');$lines=array_values(array_filter(explode("\n",trim($p->getOutput())),fn($v)=>str_starts_with($v,'{')));$out[]=json_decode(end($lines),true,32,JSON_THROW_ON_ERROR);}return $out;
|
||||
};
|
||||
$select($f['manager_id']);
|
||||
$opening=['method'=>'open','actor'=>$f['manager_id'],'key'=>$key(),'input'=>['reference'=>'OT-REPLAY-'.Str::uuid(),'client_id'=>$fx['client'],'equipment_id'=>DB::table('wo_orders')->find($fx['order'])->equipment_id,'request'=>'Abertura concorrente.','location'=>'Local de teste','evidence'=>'Pedido sintético.']];
|
||||
$out=$pair($opening,$opening);if($out[0]['status']!==200||$out[1]['status']!==200||!$out[1]['result']['replayed']||$out[0]['result']['id']!==$out[1]['result']['id'])throw new RuntimeException('Concurrent opening replay failed.');$proof['same_opening_one_order_one_replay']=true;
|
||||
$o=$w->open($f['manager_id'],['reference'=>'OT-CONCURRENT-'.Str::uuid(),'client_id'=>$fx['client'],'equipment_id'=>DB::table('wo_orders')->find($fx['order'])->equipment_id,'request'=>'Prova de concorrência independente.','location'=>'Local de teste','evidence'=>'Somente ensaio isolado.'],$key())['id'];
|
||||
$w->record($o,$f['manager_id'],['kind'=>'assignment','origin'=>'CONCURRENT-ASSIGN','version'=>1,'revision'=>0,'technician_id'=>$fx['technician_id']],$key());$select($fx['technician_id']);
|
||||
$d=['kind'=>'task','origin'=>'CONCURRENT-TASK','version'=>1,'revision'=>$rev($o),'description'=>'Tarefa sintética concorrente.'];
|
||||
$input=['method'=>'record','id'=>$o,'actor'=>$fx['technician_id'],'key'=>$key(),'input'=>$d];$out=$pair($input,$input);
|
||||
if($out[0]['status']!==200||$out[1]['status']!==200||!$out[1]['result']['replayed']||$out[0]['result']['id']!==$out[1]['result']['id'])throw new RuntimeException('Record replay race failed.');
|
||||
$task=$out[0]['result']['id'];$proof['same_record_one_insert_one_replay']=true;
|
||||
$d=['kind'=>'time','task_id'=>$task,'origin'=>'CONCURRENT-TIME-A','version'=>1,'revision'=>$rev($o),'starts_at'=>$fx['day'].'T14:00:00+02:00','ends_at'=>$fx['day'].'T15:00:00+02:00','mode'=>'reported','time_type'=>'work','evidence'=>'Relato retrospectivo sintético.'];
|
||||
$a=['method'=>'record','id'=>$o,'actor'=>$fx['technician_id'],'key'=>$key(),'input'=>$d];$b=$a;$b['key']=$key();$b['input']['origin']='CONCURRENT-TIME-B';$out=$pair($a,$b);
|
||||
if(array_column($out,'status')!==[200,409])throw new RuntimeException('Overlap race failed.');$time=$out[0]['result']['id'];$proof['concurrent_same_technician_not_double_counted']=true;
|
||||
$a=['method'=>'verify','id'=>$time,'actor'=>$fx['reviewer_id'],'key'=>$key(),'input'=>['version'=>1,'revision'=>$rev($o),'decision'=>'acknowledged','reason'=>'Somente auditoria; não autoriza execução.']];$out=$pair($a,$a);
|
||||
if($out[0]['status']!==200||$out[1]['status']!==200||!$out[1]['result']['replayed'])throw new RuntimeException('Verification replay race failed.');$proof['verification_one_decision_one_replay']=true;
|
||||
// A new version and a decision against its old version cannot both win.
|
||||
$original=DB::table('wo_entries')->find($time);$a=['method'=>'record','id'=>$o,'actor'=>$fx['technician_id'],'key'=>$key(),'input'=>array_replace($d,['origin'=>$original->origin,'version'=>2,'previous_id'=>$time,'revision'=>$rev($o),'correction_reason'=>'Correcção concorrente explícita.','ends_at'=>$fx['day'].'T14:30:00+02:00'])];
|
||||
$b=['method'=>'verify','id'=>$time,'actor'=>$fx['reviewer_id'],'key'=>$key(),'input'=>['version'=>1,'revision'=>$rev($o),'decision'=>'refused','reason'=>'Revisão antiga concorrente.']];$out=$pair($a,$b);
|
||||
if(array_column($out,'status')!==[200,409])throw new RuntimeException('Version versus review race failed.');$proof['new_version_invalidates_old_review']=true;
|
||||
$row=(array)DB::table('wo_entries')->find($out[0]['result']['id']);unset($row['id']);$row['origin']='SQL-OVERLAP-A';$row['version']=1;$row['previous_id']=null;$row['starts_at']=$fx['day'].'T16:00:00+02:00';$row['ends_at']=$fx['day'].'T17:00:00+02:00';$row['minutes']=60;$row['revision']=$rev($o);
|
||||
$select($f['manager_id']);
|
||||
$o2=$w->open($f['manager_id'],['reference'=>'OT-CROSS-'.Str::uuid(),'client_id'=>$fx['client'],'equipment_id'=>DB::table('wo_orders')->find($fx['order'])->equipment_id,'request'=>'Segunda ordem para disputa de tempo.','location'=>'Local de teste','evidence'=>'Somente ensaio de concorrência.'],$key())['id'];
|
||||
$w->record($o2,$f['manager_id'],['kind'=>'assignment','origin'=>'CROSS-ASSIGN','version'=>1,'revision'=>0,'technician_id'=>$fx['technician_id']],$key());$select($fx['technician_id']);
|
||||
$t2=$w->record($o2,$fx['technician_id'],['kind'=>'task','origin'=>'CROSS-TASK','version'=>1,'revision'=>1,'description'=>'Tarefa de outra ordem.'],$key())['id'];
|
||||
$a=['method'=>'sql','actor'=>$fx['technician_id'],'sql'=>$row];$b=$a;$b['sql']['origin']='SQL-OVERLAP-B';$b['sql']['order_id']=$o2;$b['sql']['task_id']=$t2;$b['sql']['revision']=$rev($o2);$out=$pair($a,$b);
|
||||
if(array_column($out,'status')!==[200,409]||!($out[1]['database_guard']??false))throw new RuntimeException('SQL concurrency guard failed.');$proof['raw_sql_revision_overlap_guard']=true;
|
||||
file_put_contents(base_path('docs/proofs/workshop-concurrency.json'),json_encode(['database'=>'sgf_tempo_test','overlapping_processes_per_case'=>2,'checks'=>$proof,'synthetic_only'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "Six overlapping-process scenarios passed only in sgf_tempo_test; no duplicate order, record, decision or effective time.\n";
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Workshop\WorkshopWorkflow::guard();
|
||||
$file='C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';
|
||||
$f=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);
|
||||
if(\Illuminate\Support\Facades\DB::table('wo_orders')->exists()) throw new RuntimeException('Workshop proof must be empty; nothing deleted.');
|
||||
$day=now('Africa/Johannesburg')->subDay()->format('Y-m-d');
|
||||
\Carbon\Carbon::setTestNow(\Carbon\Carbon::parse($day.' 08:00:00','Africa/Johannesburg'));
|
||||
try {
|
||||
$fx=\Tests\Support\WorkshopFixture::create($f+['manager'=>\App\Models\User::findOrFail($f['manager_id']),'reader'=>\App\Models\User::findOrFail($f['reader_id'])]);
|
||||
$w=app(\App\Modules\Workshop\WorkshopWorkflow::class); $ctx=app(\App\Shared\Context\CompanyContext::class);
|
||||
$select=fn($actor)=>$ctx->selectFor($actor,$f['tenant'],$f['company'],$f['unit']); $key=fn()=>(string)\Illuminate\Support\Str::uuid();
|
||||
$select($f['manager_id']);
|
||||
$order=$w->open($f['manager_id'],['reference'=>'OT-TEST-PRINCIPAL','client_id'=>$fx['client'],'equipment_identifier'=>'EQ-TEST-PC','equipment_description'=>'Computador sintético para prova','serial'=>'SER-TEST-PC','request'=>'Diagnóstico e reparação sintéticos.','location'=>'Local sintético','evidence'=>'Pedido artificial, sem operação real.'],$key())['id'];
|
||||
$w->record($order,$f['manager_id'],['kind'=>'assignment','origin'=>'TEST-ASSIGN','version'=>1,'revision'=>0,'technician_id'=>$fx['technician']->id],$key());
|
||||
$select($fx['technician']->id);
|
||||
$task=$w->record($order,$fx['technician']->id,['kind'=>'task','origin'=>'TEST-TASK','version'=>1,'revision'=>1,'description'=>'Intervenção sintética principal.'],$key())['id'];
|
||||
$select($f['manager_id']);
|
||||
$auth=$w->record($order,$f['manager_id'],['kind'=>'authorization','origin'=>'TEST-AUTH','version'=>1,'revision'=>2,'task_id'=>$task,'authorized_by'=>'Representante sintético do cliente','capacity'=>'Responsável do cliente apenas no ensaio','authorized_at'=>$day.'T07:59:00+02:00','channel'=>'Documento artificial','scope'=>'Somente a tarefa principal de diagnóstico/reparação.','evidence'=>'TEST-COMPROVATIVO-1'],$key())['id'];
|
||||
$out=['client'=>$fx['client'],'technician_id'=>$fx['technician']->id,'reviewer_id'=>$fx['reviewer']->id,'order'=>$order,'task'=>$task,'auth'=>$auth,'day'=>$day];
|
||||
file_put_contents($file,json_encode($f+['workshop'=>$out],JSON_THROW_ON_ERROR));
|
||||
} finally { \Carbon\Carbon::setTestNow(); }
|
||||
echo "Workshop fixture prepared only in sgf_tempo_test: three synthetic roles and a historical test clock for authorization. No time/material records pre-created.\n";
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// Read-only. No apply branch; technician and verifier real identities remain unassigned.
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
if(!\App\Modules\Identity\Services\LocalDatabaseGuard::allows())throw new RuntimeException('Development read-only scope refused.');
|
||||
$db=\Illuminate\Support\Facades\DB::class;
|
||||
$u=$db::table('users')->where('id',1)->first(['id','name','email']);$c=$db::table('companies')->where('id',1)->first(['id','tenant_id','name']);$unit=$db::table('units')->where(['id'=>1,'company_id'=>1])->first(['id','name']);
|
||||
if(!$u||!$c||!$unit||!$db::table('company_memberships')->where(['user_id'=>1,'tenant_id'=>$c->tenant_id,'company_id'=>1])->exists())throw new RuntimeException('Existing opener/scope not confirmed.');
|
||||
$rights=[];foreach(['view','open','assign','authorize'] as $r)$rights[]=['permission'=>'workshop.'.$r,'already_granted'=>$db::table('permission_grants')->where(['user_id'=>1,'tenant_id'=>$c->tenant_id,'company_id'=>1,'unit_id'=>1,'permission'=>'workshop.'.$r])->exists()];
|
||||
echo json_encode(['mode'=>'preview_only','opener'=>$u,'company'=>$c,'unit'=>$unit,'proposed'=>$rights,'technician'=>'not_identified_no_grant','verifier'=>'not_identified_no_grant','applied'=>false],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Workshop\WorkshopWorkflow::guard();
|
||||
$f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);$fx=$f['workshop'];
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($fx['reviewer_id'],$f['tenant'],$f['company'],$f['unit']);
|
||||
$view=app(\App\Modules\Workshop\WorkshopWorkflow::class)->listing($fx['reviewer_id']);$order=collect($view['orders'])->firstWhere('id',$fx['order']);
|
||||
if(!$order||$order['minutes_reported']!==105||$order['minutes_verified']!==105)throw new RuntimeException('Effective time not verified.');
|
||||
$material=collect($order['materials'])->firstWhere('resource','CABO-TEST');
|
||||
if(!$material||$material['delivered']!=='3'||$material['consumed']!=='2'||$material['returned']!=='1'||$material['unexplained']!=='0'||$material['verified_consumed']!=='2')throw new RuntimeException('Material observations not verified.');
|
||||
$exception=collect($order['entries'])->firstWhere('origin','TEST-EXCEPTION-PAST');
|
||||
if(!$exception||!$exception->outside_authority||$exception->decision?->decision!=='acknowledged')throw new RuntimeException('Historical exception lost or incorrectly authorized.');
|
||||
$refused=collect($order['entries'])->firstWhere('origin','TEST-REVIEW-REFUSAL');
|
||||
if(!$refused||$refused->decision?->decision!=='refused')throw new RuntimeException('Explicit refusal not demonstrated.');
|
||||
$later=collect($order['entries'])->firstWhere('origin','TEST-AUTH-EXTRA');if(!$later)throw new RuntimeException('Later scope authorization missing.');
|
||||
$out=['database'=>'sgf_tempo_test','policy'=>\App\Modules\Workshop\WorkshopWorkflow::POLICY,'synthetic_only'=>true,'real_browser_login'=>true,'csrf_enabled'=>true,'three_distinct_actors'=>count(array_unique([$f['manager_id'],$fx['technician_id'],$fx['reviewer_id']]))===3,'order_id'=>$order['id'],'minutes_reported'=>105,'minutes_verified'=>105,'material'=>$material,'unauthorized_start_refused_in_browser'=>true,'past_exception_preserved_after_later_authorization'=>true,'exception_decision'=>'acknowledged_not_authorized','refused_entry'=>$refused->id,'refusal_reason'=>$refused->decision->reason,'real_operations_enabled'=>false];
|
||||
file_put_contents(base_path('docs/proofs/workshop-browser-results.json'),json_encode($out,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR));
|
||||
echo "Browser workshop proof verified: 105 minutes, 3/2/1 material observations, explicit refusal and historical exception still unauthorized.\n";
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Workshop\WorkshopWorkflow::guard();
|
||||
$in=json_decode(stream_get_contents(STDIN),true,32,JSON_THROW_ON_ERROR);
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($in['actor'],$in['tenant'],$in['company'],$in['unit']);
|
||||
if($in['pause']??false) {
|
||||
$paused=false;\Illuminate\Support\Facades\DB::listen(function($q)use(&$paused){if(!$paused&&str_contains($q->sql,'"companies"')&&str_contains($q->sql,'for update')){$paused=true;echo "LOCK_HELD\n";flush();usleep(1200000);}});
|
||||
}
|
||||
try {
|
||||
$w=app(\App\Modules\Workshop\WorkshopWorkflow::class);
|
||||
if($in['method']==='sql') {
|
||||
$out=\Illuminate\Support\Facades\DB::transaction(function()use($in){if($in['pause']??false)\Illuminate\Support\Facades\DB::table('companies')->where('id',$in['company'])->lockForUpdate()->firstOrFail();return ['id'=>\Illuminate\Support\Facades\DB::table('wo_entries')->insertGetId($in['sql'])];});
|
||||
} elseif($in['method']==='open') $out=$w->open($in['actor'],$in['input'],$in['key']);
|
||||
else $out=$w->{$in['method']}($in['id'],$in['actor'],$in['input'],$in['key']);
|
||||
echo json_encode(['status'=>200,'result'=>$out],JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
} catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){echo json_encode(['status'=>$e->getStatusCode()],JSON_THROW_ON_ERROR).PHP_EOL;}
|
||||
catch(\Illuminate\Database\QueryException $e){if(!str_contains($e->getMessage(),'SGF_WO_'))throw $e;echo json_encode(['status'=>409,'database_guard'=>true],JSON_THROW_ON_ERROR).PHP_EOL;}
|
||||
Reference in New Issue
Block a user