first commit

This commit is contained in:
2026-10-27 12:26:52 +02:00
parent 7628645fb6
commit ea9dcb3314
19 changed files with 701 additions and 1 deletions
+10
View File
@@ -0,0 +1,10 @@
<?php
require __DIR__.'/../vendor/autoload.php';
$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
\App\Modules\Workshop\DirectCosts::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('wo_orders')->exists()||\Illuminate\Support\Facades\DB::table('dc_rates')->exists())throw new RuntimeException('Fresh isolated proof required; nothing deleted.');
$f=\Tests\Support\DirectCostFixture::create($base+['manager'=>\App\Models\User::findOrFail($base['manager_id']),'reader'=>\App\Models\User::findOrFail($base['reader_id'])]);
$safe=array_intersect_key($f,array_flip(['order','time','material','day','task','auth']));
file_put_contents($file,json_encode($base+['direct_costs'=>$safe],JSON_THROW_ON_ERROR));
echo "Verified workshop sources prepared via existing commands in sgf_tempo_test. Cost rates/calculations are empty; no copied operational events.\n";
+7
View File
@@ -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\Workshop\DirectCosts::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{$d=app(\App\Modules\Workshop\DirectCosts::class);$out=$d->{$in['method']}($in['order'],$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;}
+34
View File
@@ -0,0 +1,34 @@
<?php
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('Own SGF development database required.');
$args=array_values(array_filter(array_slice($argv,1),fn($v)=>$v!=='--apply'));
if(count($args)!==4 || array_filter($args,fn($v)=>!ctype_digit($v)))throw new RuntimeException('Usage: invoice-access.php proposer validator company unit [--apply]; exact prior confirmation required.');
[$proposer,$validator,$companyId,$unitId]=array_map('intval',$args);
if($proposer===$validator)throw new RuntimeException('Distinct proposer and validator required.');
$db=\Illuminate\Support\Facades\DB::class;
$preview=function()use($db,$proposer,$validator,$companyId,$unitId){
$company=$db::table('companies')->where('id',$companyId)->first(['id','tenant_id','name']);
if(!$company)throw new RuntimeException('Existing company required.');
$unit=$db::table('units')->where('tenant_id',$company->tenant_id)->where('company_id',$companyId)->where('id',$unitId)->first(['id','name']);
$accounts=$db::table('users')->whereIn('id',[$proposer,$validator])->orderBy('id')->get(['id','name','email']);
if(!$unit || $accounts->count()!==2)throw new RuntimeException('Existing unit and two existing accounts required.');
$grants=[];
foreach($accounts as $a){
if(!$db::table('company_memberships')->where('user_id',$a->id)->where('tenant_id',$company->tenant_id)->where('company_id',$companyId)->exists())throw new RuntimeException('Membership missing; no association inferred.');
app(\App\Modules\Identity\Services\Permissions::class)->require($a->id,$company->tenant_id,$companyId,'company.view');
app(\App\Modules\Identity\Services\Permissions::class)->require($a->id,$company->tenant_id,$companyId,'unit.view',$unitId);
foreach(['invoice_series.view',$a->id===$proposer?'invoice_series.propose':'invoice_series.approve'] as $permission)$grants[]=['user_id'=>$a->id,'tenant_id'=>$company->tenant_id,'company_id'=>$companyId,'unit_id'=>null,'permission'=>$permission];
foreach($a->id===$proposer?['service_invoices.view','service_invoices.issue','service_invoices.correction_request']:['service_invoices.view'] as $permission)$grants[]=['user_id'=>$a->id,'tenant_id'=>$company->tenant_id,'company_id'=>$companyId,'unit_id'=>$unitId,'permission'=>$permission];
}
return ['company'=>$company,'unit'=>$unit,'accounts'=>$accounts,'proposed_grants'=>$grants,'applied'=>false];
};
if(!in_array('--apply',$argv,true)){echo json_encode($preview(),JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL;exit;}
$count=$db::transaction(function()use($db,$companyId,$preview){
$db::table('companies')->where('id',$companyId)->lockForUpdate()->firstOrFail();$p=$preview();$count=0;
foreach($p['proposed_grants'] as $g)if($db::table('permission_grants')->insertOrIgnore($g+['created_at'=>now(),'updated_at'=>now()])){
$count++;\App\Modules\Identity\Services\Audit::record('service_invoice.initial_grant',null,$g['tenant_id'],$g['company_id'],$g['unit_id'],subject:$g['user_id'],details:['permission'=>$g['permission'],'reason'=>'Eight initial grants specifically confirmed; real issuance remains blocked.']);
}
return $count;
});
echo "Applied $count specifically confirmed grants; no series or document created.\n";
+45
View File
@@ -0,0 +1,45 @@
<?php
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('Own SGF development database required.');
$db=\Illuminate\Support\Facades\DB::class;
$before=in_array('--before',$argv,true);$grants=in_array('--after-grants',$argv,true);
$file=storage_path('app/invoice-dev-preservation-before.json');
$baseline=$before?null:json_decode(file_get_contents($file),true,64,JSON_THROW_ON_ERROR);
$tables=$before?array_map(fn($r)=>$r->tablename,$db::select("SELECT tablename FROM pg_tables WHERE schemaname='public' AND (tablename ~ '^(a_|b_|ca_|sf_|doc_|document_|service_calculation_)' OR tablename IN ('users','tenants','companies','units','company_memberships','permission_grants','identity_audit')) ORDER BY tablename")):array_keys($baseline['tables']);
$summary=[];
foreach($tables as $table){
if(!preg_match('/^[a-z_]+$/D',$table))throw new RuntimeException('Unexpected table identifier.');
$where='';if($grants&&in_array($table,['permission_grants','identity_audit'],true))$where=' WHERE id <= '.(int)$baseline['max'][$table];
$r=$db::selectOne('SELECT count(*) AS n, md5(COALESCE(string_agg(row_to_json(t)::text, E\'\\n\' ORDER BY row_to_json(t)::text),\'\')) AS h FROM (SELECT * FROM "'.$table.'"'.$where.') t');
$summary[$table]=['count'=>(int)$r->n,'hash'=>$r->h];
}
if($before){
file_put_contents($file,json_encode(['tables'=>$summary,'max'=>['permission_grants'=>(int)$db::table('permission_grants')->max('id'),'identity_audit'=>(int)$db::table('identity_audit')->max('id')],'guard_file_hash'=>hash_file('sha256',base_path('app/Modules/Services/ServiceInvoicing.php'))],JSON_THROW_ON_ERROR));
echo 'Fingerprinted '.count($summary)." preexisting tables; no record contents disclosed.\n";exit;
}
if($summary!==$baseline['tables'])throw new RuntimeException('Preexisting data or grants changed; review before proceeding.');
if(hash_file('sha256',base_path('app/Modules/Services/ServiceInvoicing.php'))!==$baseline['guard_file_hash'])throw new RuntimeException('Issuance guard code changed.');
$counts=[];foreach(['si_proposals','si_decisions','si_counters','si_invoices','si_events','si_outbox','si_commands','si_correction_requests'] as $table){$counts[$table]=$db::table($table)->count();if($counts[$table]!==0)throw new RuntimeException('New series/document tables not empty.');}
$denials=[];
foreach(['guard','issue','propose'] as $entry){
try{
if($entry==='guard')\App\Modules\Services\ServiceInvoicing::guard();
elseif($entry==='issue')app(\App\Modules\Services\ServiceInvoicing::class)->issue(0,1,[],'blocked-verification');
else app(\App\Modules\Services\ServiceInvoicing::class)->propose(1,['kind'=>'series']);
throw new RuntimeException('Real issuance entry did not refuse.');
}catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){if($e->getStatusCode()!==403||!str_contains($e->getMessage(),'conformidade'))throw $e;$denials[$entry]=403;}
}
if($grants){
$expected=[];foreach([1=>['invoice_series.view','invoice_series.propose','service_invoices.view','service_invoices.issue','service_invoices.correction_request'],2=>['invoice_series.view','invoice_series.approve','service_invoices.view']] as $user=>$rights)foreach($rights as $right)$expected[]=['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'unit_id'=>str_starts_with($right,'service_invoices.')?1:null,'permission'=>$right];
usort($expected,fn($a,$b)=>[$a['user_id'],$a['permission']]<=>[$b['user_id'],$b['permission']]);
$actual=$db::table('permission_grants')->where(fn($q)=>$q->where('permission','like','invoice_series.%')->orWhere('permission','like','service_invoices.%'))->orderBy('user_id')->orderBy('permission')->get(['user_id','tenant_id','company_id','unit_id','permission'])->map(fn($r)=>(array)$r)->all();
if($actual!==$expected)throw new RuntimeException('Grants differ from the eight specifically approved operations.');
if($db::table('identity_audit')->where('id','>',$baseline['max']['identity_audit'])->where('action','service_invoice.initial_grant')->count()!==8)throw new RuntimeException('Eight initial grant audit entries required.');
app(\App\Shared\Context\CompanyContext::class)->selectFor(1,1,1,1);
$listing=app(\App\Modules\Services\ServiceInvoicing::class)->listing(1);
if(!$listing['real_blocked'] || $listing['test_enabled'] || $listing['invoices']!==[] || $listing['proposals']!==[])throw new RuntimeException('Normal application must report blocked.');
}
$proof=['database'=>'sgf_tempo_dev','preexisting_tables_checked'=>count($summary),'preexisting_records_unchanged'=>true,'prior_grants_and_audit_unchanged'=>true,'new_tables'=>$counts,'confirmed_grants_verified'=>$grants?8:0,'guard_code_unchanged'=>true,'real_entry_denials'=>$denials,'real_issuance_blocked'=>true,'real_series_created'=>false,'documents_issued'=>false];
file_put_contents(base_path('docs/proofs/invoice-dev-preservation'.($grants?'-grants':'').'.json'),json_encode($proof,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo $grants?"Verified exactly eight audited grants, prior data preserved, all new tables empty and real issuance blocked.\n":"Verified preserved data and grants, eight empty tables and real issuance blocked.\n";
+99
View File
@@ -0,0 +1,99 @@
<?php
// Only the specifically authorised 070000 and eight unit grants; no real operations.
require __DIR__.'/../vendor/autoload.php';
$app=require __DIR__.'/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Artisan;
use App\Modules\Identity\Services\LocalDatabaseGuard;
use App\Modules\Identity\Services\Permissions;
use App\Modules\Identity\Services\Audit;
use App\Modules\Services\ServiceReceivables;
use App\Modules\Workshop\WorkshopWorkflow;
use App\Shared\Context\CompanyContext;
function ensureReceivables(bool $ok,string $message): void { if(!$ok)throw new RuntimeException($message); }
function receivablesFingerprint(string $table,?int $max=null): array {
ensureReceivables((bool)preg_match('/^[a-z_]+$/D',$table),'Unexpected table identifier.');
$where=$max===null?'':' WHERE id <= '.$max;
$r=DB::selectOne('SELECT count(*) AS n, md5(COALESCE(string_agg(row_to_json(t)::text, E\'\\n\' ORDER BY row_to_json(t)::text),\'\')) AS h FROM (SELECT * FROM "'.$table.'"'.$where.') t');
return [(int)$r->n,$r->h]; // In memory only; no record contents exported.
}
ensureReceivables(LocalDatabaseGuard::allows(),'Own SGF development application connection required.');
ensureReceivables($argv===[$argv[0],'--apply-confirmed-070000-and-eight-grants'],'Exact confirmed operation argument required.');
$migration='2026_10_27_070000_create_simulated_receivables';
ensureReceivables(!DB::table('migrations')->where('migration',$migration)->exists(),'070000 already installed; review changed state.');
ensureReceivables(DB::table('migrations')->where('migration','2026_10_27_080000_create_workshop_resource_records')->exists(),'Expected workshop installation missing.');
$newTables=['sr_records','sr_effects','sr_commands','sr_events','sr_outbox','sr_deliveries'];
$tables=array_map(fn($r)=>$r->tablename,DB::select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"));
ensureReceivables(!array_intersect($tables,$newTables),'Receivables tables already exist; review first.');
$rights=[1=>['view','prepare','allocate','correction_request'],2=>['view','confirm','reconcile','correction_approve']];
$checkScope=function(): void {
foreach([1=>['Osmane Nala','nala@tempo.co.mz'],2=>['Samira Nal','onala@me.com']] as $id=>[$name,$email]){
ensureReceivables(DB::table('users')->where(compact('id','name','email'))->exists(),'Confirmed account identity differs.');
ensureReceivables(DB::table('company_memberships')->where(['user_id'=>$id,'tenant_id'=>1,'company_id'=>1])->exists(),'Existing membership required.');
$p=app(Permissions::class);$p->require($id,1,1,'company.view');$p->require($id,1,1,'unit.view',1);
}
ensureReceivables(DB::table('companies')->where(['id'=>1,'tenant_id'=>1,'name'=>'Tempo Lda'])->exists(),'Confirmed company differs.');
ensureReceivables(DB::table('units')->where(['id'=>1,'tenant_id'=>1,'company_id'=>1,'name'=>'SEGI'])->exists(),'Confirmed unit differs.');
ensureReceivables(!DB::table('permission_grants')->where('permission','like','service_receivables.%')->exists(),'Unexpected existing receivables rights; review first.');
};
$checkScope();
$baseline=[];foreach($tables as $t)$baseline[$t]=receivablesFingerprint($t);
$max=[];foreach(['permission_grants','identity_audit'] as $t)$max[$t]=(int)DB::table($t)->max('id');
$ledger=DB::table('migrations')->orderBy('id')->get()->map(fn($r)=>(array)$r)->all();
$sourceHashes=[];foreach(['app/Modules/Services/ServiceReceivables.php','app/Modules/Services/ServiceInvoicing.php','app/Modules/Workshop/WorkshopWorkflow.php'] as $p)$sourceHashes[$p]=hash_file('sha256',base_path($p));
$secrets=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/database.json'),true,32,JSON_THROW_ON_ERROR);
$owner=config('database.connections.'.config('database.default'));
$owner['username']='sgf_dev_owner';$owner['password']=$secrets['dev_owner'];$owner['url']=null;
config(['database.connections.sgf_receivables_owner'=>$owner]);unset($owner,$secrets);
try {
$connection=DB::connection('sgf_receivables_owner');
$identity=$connection->selectOne('SELECT current_database() AS db,current_user AS role,inet_server_port() AS port,rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user');
ensureReceivables($identity->db==='sgf_tempo_dev'&&$identity->role==='sgf_dev_owner'&&(int)$identity->port===55432&&!$identity->rolsuper&&!$identity->rolcreatedb&&!$identity->rolcreaterole&&!$identity->rolbypassrls,'Unexpected migration owner/destination.');
$connection->transaction(function()use($migration,$baseline,$newTables): void {
$applicationConnection=DB::getDefaultConnection();DB::setDefaultConnection('sgf_receivables_owner');
try {
$exit=Artisan::call('migrate',['--database'=>'sgf_receivables_owner','--path'=>'database/migrations/'.$migration.'.php','--force'=>true,'--no-interaction'=>true]);
ensureReceivables($exit===0,'Targeted migration failed.');
foreach($baseline as $t=>$fp)if($t!=='migrations')ensureReceivables(receivablesFingerprint($t)===$fp,'Existing records changed during migration: '.$t);
foreach($newTables as $t)ensureReceivables(DB::table($t)->count()===0,'New tables must be empty.');
} finally { DB::setDefaultConnection($applicationConnection); }
});
} finally { DB::purge('sgf_receivables_owner');config(['database.connections.sgf_receivables_owner'=>null]); }
ensureReceivables(LocalDatabaseGuard::allows(),'Application must remain sgf_dev_app.');
$afterLedger=DB::table('migrations')->orderBy('id')->get()->map(fn($r)=>(array)$r)->all();
ensureReceivables(count($afterLedger)===count($ledger)+1&&array_slice($afterLedger,0,count($ledger))===$ledger&&end($afterLedger)['migration']===$migration,'Unexpected migration ledger delta.');
$denials=[];
$grants=DB::transaction(function()use($rights,$checkScope,$baseline,$max,&$denials): array {
DB::table('companies')->where('id',1)->lockForUpdate()->firstOrFail();$checkScope();$grants=[];
foreach($rights as $user=>$actions)foreach($actions as $action){
$g=['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>'service_receivables.'.$action];
DB::table('permission_grants')->insert($g+['created_at'=>now(),'updated_at'=>now()]);
Audit::record('service_receivables.initial_grant',null,1,1,1,subject:$user,details:['permission'=>$g['permission'],'reason'=>'Eight unit grants specifically authorised; real operations remain blocked.']);$grants[]=$g;
}
foreach($baseline as $t=>$fp)if($t!=='migrations')ensureReceivables(receivablesFingerprint($t,$max[$t]??null)===$fp,'Prior records changed: '.$t);
ensureReceivables(DB::table('permission_grants')->where('id','>',$max['permission_grants'])->count()===8,'Exactly eight new grants required.');
ensureReceivables(DB::table('identity_audit')->where('id','>',$max['identity_audit'])->count()===8&&DB::table('identity_audit')->where('id','>',$max['identity_audit'])->where('action','service_receivables.initial_grant')->count()===8,'Exactly eight grant audit entries required.');
$actual=DB::table('permission_grants')->where('permission','like','service_receivables.%')->get(['user_id','tenant_id','company_id','unit_id','permission'])->map(fn($r)=>(array)$r)->all();
$sort=fn($a,$b)=>[$a['user_id'],$a['permission']]<=>[$b['user_id'],$b['permission']];usort($actual,$sort);$expected=$grants;usort($expected,$sort);ensureReceivables($actual===$expected,'Granted accounts/rights/scope differ.');
foreach([1,2] as $actor){app(CompanyContext::class)->selectFor($actor,1,1,1);$listing=app(ServiceReceivables::class)->listing($actor);ensureReceivables($listing['blocked']&&$listing['records']===[]&&$listing['obligations']===[]&&$listing['documents']===[],'Normal receivables listing must remain blocked.');}
foreach(['guard','propose','decide','allocate','deliver','workshop','issuance'] as $entry){
try {
$r=app(ServiceReceivables::class);
match($entry){'guard'=>ServiceReceivables::guard(),'propose'=>$r->propose(1,[],'blocked-probe'),'decide'=>$r->decide(0,2,[],'blocked-probe'),'allocate'=>$r->allocate(0,1,[],'blocked-probe'),'deliver'=>$r->deliver(0),'workshop'=>WorkshopWorkflow::guard(),'issuance'=>\App\Modules\Services\ServiceInvoicing::guard()};
throw new RuntimeException('Operational entry did not refuse.');
}catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){ensureReceivables($e->getStatusCode()===403,'Expected operational refusal.');$denials[$entry]=403;}
}
app(CompanyContext::class)->selectFor(1,1,1,1);$w=app(WorkshopWorkflow::class)->listing(1);ensureReceivables($w['blocked']&&$w['orders']===[],'Workshop operational block must remain.');
return $grants;
});
foreach(['sr','wo'] as $prefix){
try {DB::select('SELECT sgf_'.$prefix.'_environment()');throw new RuntimeException('SQL environment guard did not refuse.');}
catch(\Illuminate\Database\QueryException $e){ensureReceivables(str_contains($e->getMessage(),'SGF_'.strtoupper($prefix).'_TEST_DATABASE_ONLY'),'Unexpected SQL guard response.');}
}
foreach($sourceHashes as $p=>$hash)ensureReceivables(hash_file('sha256',base_path($p))===$hash,'Functional source or guard changed.');
foreach($newTables as $t)ensureReceivables(DB::table($t)->count()===0,'New receivables tables must remain empty.');
$proof=['date'=>'2026-10-27','database'=>'sgf_tempo_dev','migration_applied'=>$migration,'only_one_migration_added'=>true,'workshop_080000_preserved'=>true,'preexisting_tables_checked'=>count($baseline),'prior_records_and_accesses_preserved'=>true,'new_empty_tables'=>$newTables,'new_grants'=>$grants,'audited_new_grants'=>8,'application_role'=>'sgf_dev_app','both_normal_account_listings_blocked'=>true,'entry_denials'=>$denials,'receivables_and_workshop_sql_guards_refused'=>true,'functional_and_guard_sources_unchanged'=>true,'real_operations_created'=>false];
file_put_contents(storage_path('app/private/receivables-dev-install-proof.json'),json_encode($proof,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo 'Applied only 070000 and eight audited grants; '.count($baseline)." prior tables preserved, six new tables empty, workshop and operational/fiscal guards verified.\n";