Accept isolated budget versus actual trial with documented capture limitation
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
param([ValidateSet('start','stop')][string]$Action = 'start')
|
||||
param([ValidateSet('start','stop')][string]$Action = 'start', [ValidateRange(1024,65535)][int]$Port = 8001, [switch]$ResumeIndustrialBudget)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sgfProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
if ($sgfProject -ne 'C:\laragon\www\sgf-tempo') { throw 'Unexpected project path.' }
|
||||
@@ -21,13 +21,17 @@ if ($Action -eq 'stop') {
|
||||
Write-Output 'Browser proof server stopped. Synthetic data remains only in sgf_tempo_test until the next test run.'
|
||||
exit 0
|
||||
}
|
||||
if (Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue) { throw 'Port 8001 occupied; no process was stopped.' }
|
||||
if (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) { throw "Port $Port occupied; no process was stopped." }
|
||||
if (Test-Path -LiteralPath $recordFile) { throw 'Browser proof record exists; stop that proof first.' }
|
||||
& $sgfPhp scripts/prepare-browser-fixture.php --env=testing
|
||||
if ($ResumeIndustrialBudget) {
|
||||
& $sgfPhp scripts/industrial-budget-manual-prepare.php --env=testing
|
||||
} else {
|
||||
& $sgfPhp scripts/prepare-browser-fixture.php --env=testing
|
||||
}
|
||||
if ($LASTEXITCODE) { throw 'Could not prepare synthetic fixtures.' }
|
||||
$utf8 = New-Object Text.UTF8Encoding $false
|
||||
$lines = Get-Content -LiteralPath '.env.testing' | Where-Object { $_ -notmatch '^(APP_ENV|APP_URL|SESSION_DRIVER|SESSION_COOKIE|SESSION_FILES|CACHE_STORE|CACHE_FILE_PATH|CACHE_LOCK_PATH)=' }
|
||||
$lines += @('APP_ENV=browser-testing','APP_URL=http://127.0.0.1:8001','SESSION_DRIVER=file','SESSION_COOKIE=sgf_browser_proof',
|
||||
$lines += @('APP_ENV=browser-testing',"APP_URL=http://127.0.0.1:$Port",'SESSION_DRIVER=file','SESSION_COOKIE=sgf_browser_proof',
|
||||
'SESSION_FILES=C:/laragon/www/sgf-tempo/storage/framework/browser-proof/sessions', 'CACHE_STORE=file',
|
||||
'CACHE_FILE_PATH=C:/laragon/www/sgf-tempo/storage/framework/browser-proof/cache', 'CACHE_LOCK_PATH=C:/laragon/www/sgf-tempo/storage/framework/browser-proof/locks')
|
||||
[IO.File]::WriteAllLines((Join-Path $sgfProject '.env.browser-testing'), $lines, $utf8)
|
||||
@@ -36,8 +40,8 @@ $account = [Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
New-Item -ItemType Directory -Path 'storage\framework\browser-proof\sessions','storage\framework\browser-proof\cache','storage\framework\browser-proof\locks' -Force | Out-Null
|
||||
$env:APP_ENV = 'browser-testing'
|
||||
try {
|
||||
if (Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue) { throw 'Port 8001 became occupied; no process stopped.' }
|
||||
$process = Start-Process -FilePath $sgfPhp -ArgumentList @('-S','127.0.0.1:8001','-t','public','scripts/router.php') -WorkingDirectory $sgfProject -WindowStyle Hidden -PassThru -RedirectStandardOutput "$sgfRun\browser-proof.stdout.log" -RedirectStandardError "$sgfRun\browser-proof.stderr.log"
|
||||
@{pid=$process.Id;started=$process.StartTime.ToUniversalTime().ToString('o')} | ConvertTo-Json | Set-Content -LiteralPath $recordFile -Encoding UTF8
|
||||
if (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) { throw "Port $Port became occupied; no process stopped." }
|
||||
$process = Start-Process -FilePath $sgfPhp -ArgumentList @('-S',"127.0.0.1:$Port",'-t','public','scripts/router.php') -WorkingDirectory $sgfProject -WindowStyle Hidden -PassThru -RedirectStandardOutput "$sgfRun\browser-proof.stdout.log" -RedirectStandardError "$sgfRun\browser-proof.stderr.log"
|
||||
@{pid=$process.Id;started=$process.StartTime.ToUniversalTime().ToString('o');port=$Port} | ConvertTo-Json | Set-Content -LiteralPath $recordFile -Encoding UTF8
|
||||
} finally { Remove-Item Env:APP_ENV -ErrorAction SilentlyContinue }
|
||||
Write-Output 'Browser proof: http://127.0.0.1:8001 — synthetic data only, isolated session/cache, CSRF enabled.'
|
||||
Write-Output "Browser proof: http://127.0.0.1:$Port - synthetic data only, isolated session/cache, CSRF enabled."
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use App\Modules\Industry\IndustrialBudgets;
|
||||
use App\Shared\Context\CompanyContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Process\Process;
|
||||
IndustrialBudgets::guard();$f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);$fx=$f['budget_fixture'];$proof=[];$key=fn()=>(string)Str::uuid();
|
||||
$base=['tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit'],'lot'=>$fx['lot']];
|
||||
$pair=function($one,$two)use($base){
|
||||
$a=new Process([PHP_BINARY,'scripts/industrial-budget-worker.php','--env=testing'],base_path(),null,json_encode($one+$base+['pause'=>true]));$b=new Process([PHP_BINARY,'scripts/industrial-budget-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('Budget concurrent lock not observed.');$b->start();$a->wait();$b->wait();$out=[];
|
||||
foreach([$a,$b] as $p){if(!$p->isSuccessful())throw new RuntimeException('Budget 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;
|
||||
};
|
||||
$check=function($out,bool $replay){if($out[0]['status']!==200||$out[1]['status']!==($replay?200:409)||($replay&&(!$out[1]['result']['replayed']||$out[0]['result']['id']!==$out[1]['result']['id'])))throw new RuntimeException('Unexpected concurrent budget outcome.');return $out[0]['result']['id'];};
|
||||
$last=DB::table('ib_budgets')->where('lot_id',$fx['lot'])->orderByDesc('version')->first();
|
||||
$p=['actor'=>$f['manager_id'],'method'=>'propose','key'=>$key(),'input'=>['version'=>($last?->version??0)+1,'previous_id'=>$last?->id,'dimensions'=>app(IndustrialBudgets::class)->dimensions(DB::table('ic_lots')->find($fx['lot'])),'amounts'=>['material'=>'900','labour'=>'600','allocated'=>'300'],'evidence'=>'Successeur synthétique explicitement révisé.']];
|
||||
$id=$check($pair($p,$p),true);$proof['proposal_replay_one_version']=true;
|
||||
$d=['actor'=>$f['reader_id'],'method'=>'decide','key'=>$key(),'input'=>['id'=>$id,'version'=>$p['input']['version'],'decision'=>'approved','reason'=>'Revue synthétique distincte.']];$check($pair($d,$d),true);$proof['decision_replay_one_effect']=true;
|
||||
$result=DB::table('ic_results')->where('lot_id',$fx['lot'])->orderByDesc('version')->first();$lastC=DB::table('ib_comparisons')->where('lot_id',$fx['lot'])->orderByDesc('version')->first();
|
||||
$c=['actor'=>$f['manager_id'],'method'=>'compare','key'=>$key(),'input'=>['version'=>($lastC?->version??0)+1,'previous_id'=>$lastC?->id,'budget_id'=>$id,'budget_version'=>$p['input']['version'],'result_id'=>$result->id,'result_version'=>$result->version]];
|
||||
$cid=$check($pair($c,$c),true);$proof['comparison_replay_one_snapshot']=true;
|
||||
$p['key']=$key();$p['input']['version']++;$p['input']['previous_id']=$id;$other=$p;$other['key']=$key();$other['input']['amounts']['material']='950';$new=$check($pair($p,$other),false);$proof['competing_successors_one_winner']=true;
|
||||
$d['key']=$key();$d['input']['id']=$new;$d['input']['version']=$p['input']['version'];$other=$d;$other['key']=$key();$other['input']['decision']='refused';$check($pair($d,$other),false);$proof['competing_decisions_one_winner']=true;
|
||||
$c['key']=$key();$c['input']['version']++;$c['input']['previous_id']=$cid;$c['input']['budget_id']=$new;$c['input']['budget_version']=$p['input']['version'];$other=$c;$other['key']=$key();$check($pair($c,$other),false);$proof['competing_comparisons_one_revision']=true;
|
||||
file_put_contents(base_path('docs/proofs/industrial-budget-concurrency.json'),json_encode(['policy'=>IndustrialBudgets::POLICY,'database'=>'sgf_tempo_test','overlapping_processes_per_case'=>2,'checks'=>$proof,'synthetic_only'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Six overlapping budget concurrency cases passed.\n";
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Industry\IndustrialBudgets::guard();$file='C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';$base=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);
|
||||
if(\Illuminate\Support\Facades\DB::table('ic_lots')->exists())throw new RuntimeException('Fresh synthetic proof required; nothing deleted.');
|
||||
$f=\Tests\Support\IndustrialBudgetFixture::create($base+['manager'=>\App\Models\User::findOrFail($base['manager_id']),'reader'=>\App\Models\User::findOrFail($base['reader_id'])]);
|
||||
file_put_contents($file,json_encode($base+['budget_fixture'=>array_intersect_key($f,array_flip(['lot','refs','result']))],JSON_THROW_ON_ERROR));
|
||||
echo "Synthetic industrial result validated at 1800; budget and comparisons remain for browser.\n";
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// Read-only credential correspondence check; never prints or changes credentials.
|
||||
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Industry\IndustrialBudgets::guard();
|
||||
$identity=\Illuminate\Support\Facades\DB::selectOne('SELECT current_database() AS db,current_user AS role,inet_server_port() AS port');
|
||||
if($identity->db!=='sgf_tempo_test'||$identity->role!=='sgf_test_runner'||(int)$identity->port!==55432)throw new RuntimeException('Unexpected database; no accounts changed.');
|
||||
$path='C:/laragon/data/sgf-tempo/secrets/industrial-budget-manual-entry.txt';
|
||||
if(!is_file($path))throw new RuntimeException('Entry file missing; no credentials reset.');
|
||||
$content=file_get_contents($path);
|
||||
if(!str_contains($content,'http://127.0.0.1:8002')||!preg_match_all('/(?:Osmane|Samira) synthetic: ([^\r\n]+)\r?\nPassword: ([^\r\n]+)/',$content,$matches,PREG_SET_ORDER)||count($matches)!==2)throw new RuntimeException('Unexpected entry file structure; no accounts changed.');
|
||||
$checks=[];
|
||||
foreach($matches as $match){$email=$match[1];if(!in_array($email,['gestor@example.test','consulta@example.test'],true))throw new RuntimeException('Non-synthetic account refused.');$user=\App\Models\User::where('email',$email)->first();$checks[]=['account'=>$email,'matches_current_test_account'=>$user&&\Illuminate\Support\Facades\Hash::check($match[2],$user->password)];}
|
||||
unset($content,$matches,$match);
|
||||
echo json_encode(['path'=>realpath($path),'database'=>$identity->db,'role'=>$identity->role,'accounts'=>$checks,'read_only'=>true,'credentials_changed'=>false],JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
// Resume only the existing synthetic fixture. Never resets the database or creates grants.
|
||||
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use App\Modules\Industry\IndustrialBudgets;
|
||||
use App\Modules\Identity\Services\Permissions;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
IndustrialBudgets::guard();
|
||||
$identity=DB::selectOne('SELECT current_database() AS db,current_user AS role,inet_server_port() AS port');
|
||||
if($identity->db!=='sgf_tempo_test'||$identity->role!=='sgf_test_runner'||(int)$identity->port!==55432)throw new RuntimeException('Unsafe manual proof destination.');
|
||||
$m=User::where('email','gestor@example.test')->firstOrFail();$r=User::where('email','consulta@example.test')->firstOrFail();
|
||||
if(DB::table('ic_lots')->count()!==1)throw new RuntimeException('Exactly one existing synthetic lot required; nothing deleted.');
|
||||
$lot=DB::table('ic_lots')->first();$result=DB::table('ic_results')->where(['lot_id'=>$lot->id,'version'=>1])->first();
|
||||
if(!$result||!str_starts_with($lot->reference,'LOT-TEST-'))throw new RuntimeException('Expected synthetic result missing.');
|
||||
$s=json_decode($result->snapshot,true,64,JSON_THROW_ON_ERROR);
|
||||
foreach(['material_exact'=>'1000','labour_exact'=>'500','allocated_exact'=>'300','residual_exact'=>'300','selected_total_exact'=>'1800','good_units'=>'10','currency'=>'MZN'] as $k=>$v)if($s[$k]!==$v)throw new RuntimeException('Full fixture differs.');
|
||||
if(!DB::table('ic_decisions')->where(['result_id'=>$result->id,'decision'=>'approved','actor_id'=>$r->id])->exists())throw new RuntimeException('Distinct result approval required.');
|
||||
$p=app(Permissions::class);
|
||||
foreach([$m->id=>['industrial_budgets.view','industrial_budgets.prepare','industrial_budget_comparisons.view','industrial_budget_comparisons.prepare'],$r->id=>['industrial_budgets.view','industrial_budgets.validate','industrial_budget_comparisons.view']] as $u=>$rights)foreach($rights as $right)$p->require($u,$lot->tenant_id,$lot->company_id,$right,$lot->unit_id);
|
||||
if($p->has($m->id,$lot->tenant_id,$lot->company_id,'industrial_budgets.validate',$lot->unit_id)||$p->has($r->id,$lot->tenant_id,$lot->company_id,'industrial_budgets.prepare',$lot->unit_id)||$p->has($r->id,$lot->tenant_id,$lot->company_id,'industrial_budget_comparisons.prepare',$lot->unit_id))throw new RuntimeException('Role separation differs.');
|
||||
$file='C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';$entry='C:/laragon/data/sgf-tempo/secrets/industrial-budget-manual-entry.txt';
|
||||
if(!file_exists($file)){
|
||||
$mp=bin2hex(random_bytes(20));$rp=bin2hex(random_bytes(20));
|
||||
DB::transaction(function()use($m,$r,$mp,$rp){$m->password=$mp;$m->save();$r->password=$rp;$r->save();});
|
||||
$f=['tenant'=>$lot->tenant_id,'company'=>$lot->company_id,'unit'=>$lot->unit_id,'manager_id'=>$m->id,'reader_id'=>$r->id,'budget_fixture'=>['lot'=>$lot->id,'result'=>$result->id,'refs'=>$s['refs']]];
|
||||
file_put_contents($file,json_encode($f,JSON_THROW_ON_ERROR));
|
||||
file_put_contents($entry,"SGF synthetic manual proof only - http://127.0.0.1:8002\r\nOsmane synthetic: gestor@example.test\r\nPassword: ".$mp."\r\nSamira synthetic: consulta@example.test\r\nPassword: ".$rp."\r\nNever paste these passwords into chat or reports.\r\n");unset($mp,$rp);
|
||||
}
|
||||
if(!file_exists($entry))throw new RuntimeException('Protected entry instructions missing; review locally.');
|
||||
echo json_encode(['database'=>$identity->db,'role'=>$identity->role,'port'=>(int)$identity->port,'synthetic_roles_separate'=>true,'lot_id'=>$lot->id,'result_id'=>$result->id,'result_version'=>1,'existing_budgets'=>DB::table('ib_budgets')->count(),'existing_comparisons'=>DB::table('ib_comparisons')->count(),'credentials'=>'protected_local_file_only','database_reset'=>false,'grants_created'=>false],JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
// Read-only status for guiding the isolated manual proof. No credentials or account changes.
|
||||
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Industry\IndustrialBudgets::guard();
|
||||
use Illuminate\Support\Facades\DB;
|
||||
$rows=[];
|
||||
foreach(DB::table('ib_budgets')->orderBy('id')->get() as $b){
|
||||
$d=DB::table('ib_decisions')->where('budget_id',$b->id)->first();
|
||||
$rows[]=['id'=>$b->id,'lot_id'=>$b->lot_id,'version'=>$b->version,'previous_id'=>$b->previous_id,'current'=>!DB::table('ib_budgets')->where('previous_id',$b->id)->exists(),'amounts'=>json_decode($b->amounts,true),'decision'=>$d?->decision,'preparer_id'=>$b->actor_id,'validator_id'=>$d?->actor_id];
|
||||
}
|
||||
echo json_encode(['database'=>DB::selectOne('SELECT current_database() AS db')->db,'budgets'=>$rows,'comparisons'=>DB::table('ib_comparisons')->get(['id','lot_id','version','budget_id','result_id'])->all(),'read_only'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// Reconcile the preserved manual demonstration without changing test data or credentials.
|
||||
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use App\Modules\Industry\IndustrialBudgets;
|
||||
use App\Modules\Identity\Services\Permissions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
function checkManualBudget(bool $ok,string $reason): void {if(!$ok)throw new RuntimeException($reason);}
|
||||
function canonicalManualBudget(mixed $v): mixed {if(!is_array($v))return $v;if(!array_is_list($v))ksort($v);return array_map('canonicalManualBudget',$v);}
|
||||
IndustrialBudgets::guard();
|
||||
$identity=DB::selectOne('SELECT current_database() AS db,current_user AS role,inet_server_port() AS port');
|
||||
checkManualBudget($identity->db==='sgf_tempo_test'&&$identity->role==='sgf_test_runner'&&(int)$identity->port===55432,'Unexpected database or role.');
|
||||
$fixture=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);
|
||||
$lot=$fixture['budget_fixture']['lot'];$manager=$fixture['manager_id'];$reader=$fixture['reader_id'];
|
||||
$comparisons=DB::table('ib_comparisons')->where('lot_id',$lot)->orderBy('version')->get();
|
||||
checkManualBudget($comparisons->count()===2,'Expected exactly two manual comparisons.');
|
||||
$proof=[];
|
||||
foreach($comparisons as $index=>$c){
|
||||
$snapshot=json_decode($c->snapshot,true,64,JSON_THROW_ON_ERROR);$budget=DB::table('ib_budgets')->find($c->budget_id);$result=DB::table('ic_results')->find($c->result_id);
|
||||
checkManualBudget($budget&&$result&&$budget->lot_id===$lot&&$result->lot_id===$lot,'Cross-lot reference.');
|
||||
checkManualBudget($budget->actor_id===$manager&&$c->actor_id===$manager,'Unexpected preparer.');
|
||||
foreach([['ib_decisions','budget_id',$budget],['ic_decisions','result_id',$result]] as [$table,$column,$target]){
|
||||
$decision=DB::table($table)->where([$column=>$target->id,'version'=>$target->version,'decision'=>'approved'])->first();
|
||||
checkManualBudget($decision&&$decision->actor_id===$reader&&$decision->actor_id!==$target->actor_id,'Distinct exact validation missing.');
|
||||
}
|
||||
$actual=json_decode($result->snapshot,true,64,JSON_THROW_ON_ERROR);$amounts=json_decode($budget->amounts,true);
|
||||
checkManualBudget($amounts==['material'=>'900','labour'=>'600','allocated'=>'300'],'Budget amounts differ.');
|
||||
$expected=IndustrialBudgets::variance($amounts,['material'=>$actual['material_exact'],'labour'=>$actual['labour_exact'],'allocated'=>$actual['allocated_exact']]);
|
||||
foreach($expected as $field=>$value)checkManualBudget(canonicalManualBudget($snapshot[$field])===canonicalManualBudget($value),'Comparison arithmetic differs: '.$field);
|
||||
checkManualBudget($snapshot['budget']===['id'=>$budget->id,'version'=>$budget->version]&&$snapshot['result']['id']===$result->id&&$snapshot['result']['version']===$result->version&&$snapshot['result']['refs']===$actual['refs'],'Exact version references differ.');
|
||||
checkManualBudget($snapshot['residual_separate']==='300'&&$actual['residual_exact']==='300'&&$snapshot['actual_reconciliation']===$actual['reconciliation'],'Residual or reconciliation differs.');
|
||||
checkManualBudget($actual['good_units']==='10'&&$actual['currency']==='MZN','Quantity or currency differs.');
|
||||
checkManualBudget($actual['labour_exact']==='500'&&$actual['allocated_exact']==='300','Known components differ.');
|
||||
if($index===0){
|
||||
checkManualBudget($actual['material_exact']==='1000'&&$actual['known_cost_exact']==='1800'&&$actual['selected_total_exact']==='1800'&&$actual['unit_cost_exact']==='180'&&!$expected['incomplete']&&$expected['total_delta']==='0','Full scenario differs.');
|
||||
checkManualBudget($snapshot['actual_reconciliation']['known_origins_exact']==='2100','Full origins differ.');
|
||||
}else{
|
||||
checkManualBudget($actual['material_exact']===null&&$actual['known_cost_exact']==='800'&&$actual['selected_total_exact']===null&&$actual['unit_cost_exact']===null&&$expected['incomplete']&&$expected['total_delta']===null&&$expected['total_percent']===null,'Incomplete scenario differs.');
|
||||
checkManualBudget($expected['rows']['material']['delta']===null&&$expected['paired_delta']==='-100'&&$snapshot['actual_reconciliation']['known_origins_exact']==='1100','Missing material treated incorrectly.');
|
||||
checkManualBudget($result->previous_id===$comparisons[0]->result_id&&$c->previous_id===$comparisons[0]->id&&$c->budget_id===$comparisons[0]->budget_id,'History chain differs.');
|
||||
}
|
||||
$proof[]=['comparison_id'=>$c->id,'version'=>$c->version,'budget_id'=>$budget->id,'budget_version'=>$budget->version,'result_id'=>$result->id,'result_version'=>$result->version,'actual_known'=>$actual['known_cost_exact'],'total_delta'=>$expected['total_delta'],'incomplete'=>$expected['incomplete'],'residual_separate'=>'300','exact_arithmetic_verified'=>true,'distinct_validation_verified'=>true];
|
||||
}
|
||||
$permissions=app(Permissions::class);
|
||||
foreach([$manager=>['industrial_budgets.view','industrial_budgets.prepare','industrial_budget_comparisons.view','industrial_budget_comparisons.prepare'],$reader=>['industrial_budgets.view','industrial_budgets.validate','industrial_budget_comparisons.view']] as $actor=>$rights)foreach($rights as $right)$permissions->require($actor,$fixture['tenant'],$fixture['company'],$right,$fixture['unit']);
|
||||
foreach([[$manager,'industrial_budgets.validate'],[$reader,'industrial_budgets.prepare'],[$reader,'industrial_budget_comparisons.prepare']] as [$actor,$right])checkManualBudget(!$permissions->has($actor,$fixture['tenant'],$fixture['company'],$right,$fixture['unit']),'Unexpected functional power.');
|
||||
checkManualBudget(!DB::table('wo_entries')->exists()&&!DB::table('dc_calculations')->exists(),'Unexpected workshop/direct cost origins.');
|
||||
$report=['policy'=>IndustrialBudgets::POLICY,'date'=>'2026-10-28','database'=>'sgf_tempo_test','method'=>'manual_user_report_and_read_only_database_reconciliation','manual_user_reported_complete_and_incomplete'=>true,'manual_user_confirmed_reader_forms_absent'=>true,'screenshots_received'=>false,'automated_browser_recovered'=>false,'comparisons'=>$proof,'role_separation_verified'=>true,'duplicate_workshop_or_direct_cost_origins'=>false,'test_records_changed'=>false,'credentials_changed'=>false,'user_acceptance'=>false,'closing_checkpoint_created'=>false];
|
||||
file_put_contents(base_path('docs/proofs/industrial-budget-manual-results.json'),json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR).PHP_EOL);
|
||||
echo "Manual report reconciled: two exact comparisons, distinct validations, preserved history, residual and role separation. No database or credential changes. Screenshots remain unavailable.\n";
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// Read-only; deliberately no application branch.
|
||||
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use Illuminate\Support\Facades\DB;
|
||||
if(!\App\Modules\Identity\Services\LocalDatabaseGuard::allows())throw new RuntimeException('Own SGF development application database required.');
|
||||
$rights=[1=>['industrial_budgets.view','industrial_budgets.prepare','industrial_budget_comparisons.view','industrial_budget_comparisons.prepare'],2=>['industrial_budgets.view','industrial_budgets.validate','industrial_budget_comparisons.view']];$grants=[];
|
||||
if(!DB::table('companies')->where(['id'=>1,'tenant_id'=>1,'name'=>'Tempo Lda'])->exists()||!DB::table('units')->where(['id'=>1,'tenant_id'=>1,'company_id'=>1,'name'=>'SEGI'])->exists())throw new RuntimeException('Scope differs.');
|
||||
foreach($rights as $user=>$permissions){if(!DB::table('users')->where('id',$user)->exists()||!DB::table('company_memberships')->where(['user_id'=>$user,'tenant_id'=>1,'company_id'=>1])->exists())throw new RuntimeException('Existing account membership required.');foreach($permissions as $permission){$g=['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>$permission];$grants[]=$g+['already_granted'=>DB::table('permission_grants')->where($g)->exists()];}}
|
||||
try{\App\Modules\Industry\IndustrialBudgets::guard();throw new RuntimeException('Real costing allowed');}catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){if($e->getStatusCode()!==403)throw $e;}
|
||||
echo json_encode(['mode'=>'preview_only','destination'=>'sgf_tempo_dev','migration_110000_applied'=>DB::table('migrations')->where('migration','2026_10_28_110000_create_industrial_budgets')->exists(),'proposed_grants'=>$grants,'real_costing_blocked'=>true,'applied'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Modules\Industry\IndustrialBudgets::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{$out=app(\App\Modules\Industry\IndustrialBudgets::class)->{$in['method']}($in['lot'],$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;}
|
||||
Reference in New Issue
Block a user