Accept conditional service calculation with versioned fiscal profile
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
<?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 local 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: service-fiscal-access.php proposer validator company unit [--apply]; specific 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($proposer,$validator,$companyId,$unitId,$db) {
|
||||
$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('id',$unitId)->where('company_id',$companyId)->where('tenant_id',$company->tenant_id)->first(['id','name']);
|
||||
if(!$unit) throw new RuntimeException('Existing unit required.');
|
||||
$accounts=$db::table('users')->whereIn('id',[$proposer,$validator])->orderBy('id')->get(['id','name','email']);
|
||||
if($accounts->count()!==2) throw new RuntimeException('Two existing accounts required.');
|
||||
$operations=[];
|
||||
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('No membership inference allowed.');
|
||||
foreach(['company.view','services.view'] as $right) app(\App\Modules\Identity\Services\Permissions::class)->require($a->id,$company->tenant_id,$companyId,$right);
|
||||
app(\App\Modules\Identity\Services\Permissions::class)->require($a->id,$company->tenant_id,$companyId,'unit.view',$unitId);
|
||||
foreach(['view',$a->id===$proposer?'propose':'approve'] as $action) $operations[]=['user_id'=>$a->id,'tenant_id'=>$company->tenant_id,'company_id'=>$companyId,'unit_id'=>null,'permission'=>'service_fiscal.'.$action];
|
||||
if($a->id===$proposer) $operations[]=['user_id'=>$a->id,'tenant_id'=>$company->tenant_id,'company_id'=>$companyId,'unit_id'=>$unitId,'permission'=>'service_fiscal.calculate'];
|
||||
}
|
||||
return ['accounts'=>$accounts,'company'=>$company,'unit'=>$unit,'proposed_grants'=>$operations,'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 $grant) if($db::table('permission_grants')->insertOrIgnore($grant+['created_at'=>now(),'updated_at'=>now()])) {
|
||||
$count++; \App\Modules\Identity\Services\Audit::record('service_fiscal.initial_grant',null,$grant['tenant_id'],$grant['company_id'],$grant['unit_id'],subject:$grant['user_id'],details:['permission'=>$grant['permission'],'reason'=>'Initial grants require exact specific user confirmation; no activation or emission right.']);
|
||||
}
|
||||
return $count;
|
||||
});
|
||||
echo "Applied $count specifically confirmed fiscal grants; no profile activated.\n";
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
// Input is a reviewed operation manifest, not credentials. Never accepts rates/results or grants.
|
||||
$options=getopt('',['manifest:','apply']);
|
||||
if(!isset($options['manifest'])) throw new RuntimeException('Usage: service-fiscal-activate.php --manifest=path [--apply]; specific prior authorisation required.');
|
||||
$input=json_decode(file_get_contents($options['manifest']),true,32,JSON_THROW_ON_ERROR);
|
||||
$result=app(\App\Modules\Services\LocalFiscalActivation::class)->execute($input,array_key_exists('apply',$options));
|
||||
echo json_encode($result,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL;
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\Process\Process;
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
$f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);
|
||||
$context=app(\App\Shared\Context\CompanyContext::class);
|
||||
$old=DB::table('sf_proposals')->where('draft_id',$f['fiscal_draft'])->orderByDesc('version')->first();
|
||||
if(!$old) throw new RuntimeException('Synthetic temporal fixture required.');
|
||||
$data=json_decode($old->data,true,32,JSON_THROW_ON_ERROR); $data['evidence']='New synthetic temporal version for overlapping process proof.';
|
||||
$context->selectFor($f['manager_id'],$f['tenant'],$f['company'],$f['unit']);
|
||||
$temporal=app(\App\Modules\Services\ServiceFiscal::class)->propose($f['manager_id'],['kind'=>'temporal','identifier'=>$old->identifier,'previous_id'=>$old->id,'data'=>$data]);
|
||||
$context->selectFor($f['reader_id'],$f['tenant'],$f['company'],$f['unit']);
|
||||
app(\App\Modules\Services\ServiceFiscal::class)->decide($temporal['id'],$f['reader_id'],['version'=>$temporal['version'],'decision'=>'approved','reason'=>'Synthetic concurrent proof only.']);
|
||||
$profile=DB::table('sf_proposals')->where('id',$f['fiscal_profile_proposal'])->first();
|
||||
$input=['actor'=>$f['manager_id'],'tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit'],'draft'=>$f['fiscal_draft'],'command'=>['profile_proposal_id'=>$profile->id,'profile_version'=>$profile->version,'temporal_proposal_id'=>$temporal['id'],'temporal_version'=>$temporal['version'],'draft_version'=>1,'preparation_version'=>1]];
|
||||
$before=DB::table('service_calculation_results')->where('billing_draft_id',$input['draft'])->count();
|
||||
$a=new Process([PHP_BINARY,'scripts/service-fiscal-worker.php','--env=testing'],base_path(),null,json_encode($input+['pause'=>true]));
|
||||
$b=new Process([PHP_BINARY,'scripts/service-fiscal-worker.php','--env=testing'],base_path(),null,json_encode($input));
|
||||
$a->start(); if(!$a->waitUntil(fn($type,$buffer)=>str_contains($buffer,'LOCK_HELD'))) throw new RuntimeException('No overlap established.');
|
||||
$b->start(); $a->wait(); $b->wait(); $results=[];
|
||||
foreach([$a,$b] as $worker) {
|
||||
if(!$worker->isSuccessful()) throw new RuntimeException('Fiscal worker failed; no output containing input disclosed.');
|
||||
$lines=array_values(array_filter(explode("\n",trim($worker->getOutput())),fn($s)=>str_starts_with($s,'{'))); $results[]=json_decode(end($lines),true,32,JSON_THROW_ON_ERROR);
|
||||
}
|
||||
if($results[0]['id']!==$results[1]['id'] || $results[0]['replayed'] || !$results[1]['replayed'] || DB::table('service_calculation_results')->where('billing_draft_id',$input['draft'])->count()!==$before+1) throw new RuntimeException('Concurrent duplicate or unexpected result.');
|
||||
file_put_contents(base_path('docs/proofs/service-fiscal-concurrency.json'),json_encode(['database'=>'sgf_tempo_test','overlapping_php_processes'=>2,'lock_overlap_observed'=>true,'one_insert_one_replay'=>true,'prior_history_preserved'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "Fiscal concurrency proof: two overlapping processes, one insertion and one replay; history preserved.\n";
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
// Synthetic, isolated browser proof. Never creates a Tempo account/profile/operation.
|
||||
use App\Modules\Services\ServiceFiscal;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app=require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
abort_unless(\App\Modules\Services\ServiceCalculation::isolated(),403);
|
||||
$file='C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';
|
||||
$f=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);
|
||||
abort_if(DB::table('a_services')->exists() || DB::table('sf_proposals')->exists(),409,'Fixture não vazia; nenhum dado apagado.');
|
||||
$result=DB::transaction(function() use($f) {
|
||||
foreach(['manager_id'=>['view','propose'],'reader_id'=>['view','approve']] as $who=>$actions) foreach($actions as $action) \Tests\Support\IdentityFixture::grant($f[$who],$f['tenant'],$f['company'],'service_fiscal.'.$action);
|
||||
\Tests\Support\IdentityFixture::grant($f['manager_id'],$f['tenant'],$f['company'],'service_fiscal.calculate',$f['unit']);
|
||||
$context=app(\App\Shared\Context\CompanyContext::class);
|
||||
$select=function($actor) use($context,$f) { $context->selectFor($actor,$f['tenant'],$f['company'],$f['unit']); };
|
||||
$select($f['manager_id']); $w=app(\App\Modules\Services\ServiceWorkflow::class); $actor=$f['manager_id'];
|
||||
$client=$w->save('clients',null,$actor,['name'=>'Cliente de prova fiscal'],(string)Str::uuid())['id'];
|
||||
$service=$w->save('services',null,$actor,['name'=>'Alojamento mensal — PROVA SINTÉTICA'],(string)Str::uuid())['id'];
|
||||
$source=$w->save('service_drafts',null,$actor,['name'=>'Período de prova fiscal','client_id'=>$client,'currency'=>'MZN','lines'=>[['service_id'=>$service,'quantity'=>'1','unit_price'=>'7850.00']]],(string)Str::uuid())['id'];
|
||||
$draft=$w->save('billing_drafts',null,$actor,['source_id'=>$source,'name'=>'Prova fiscal condicionada — 7850 sem IVA'],(string)Str::uuid())['id'];
|
||||
app(\App\Modules\Services\DocumentPreparation::class)->save('billing_drafts',$draft,$actor,['version'=>0,'draft_version'=>1,'currency'=>'MZN','price_mode'=>'added','period_start'=>'2026-10-26','units'=>['PERIODO'],'payment_instruction_ids'=>[]]);
|
||||
$fiscal=app(ServiceFiscal::class);
|
||||
$profile=$fiscal->propose($actor,['kind'=>'profile','identifier'=>'PROVA-ALOJAMENTO-16','data'=>['service_id'=>$service,'service_version'=>1,'currency'=>'MZN','price_mode'=>'added','rate_percent'=>'16','valid_from'=>'2026-10-26','treatment'=>'domestic_normal','conditions'=>'EXCLUSIVAMENTE SINTÉTICO — operação interna fictícia.','evidence'=>'Fixture isolada de aceitação; referências da ficha v0.8, sem aplicação à Tempo.']])['id'];
|
||||
$select($f['reader_id']); $fiscal->decide($profile,$f['reader_id'],['version'=>1,'decision'=>'approved','reason'=>'Validação sintética do perfil exacto.']);
|
||||
$select($actor);
|
||||
$temporal=$fiscal->propose($actor,['kind'=>'temporal','identifier'=>'PROVA-PERIODO','data'=>['profile_proposal_id'=>$profile,'draft_id'=>$draft,'draft_version'=>1,'preparation_version'=>1,'reference_date'=>'2026-10-26','reference_basis'=>'Referência contratual explícita para prova; não é exigibilidade.','period_start'=>'2026-10-26','next_billing'=>'2026-11-26','event'=>'pending','evidence'=>'Pendência temporal deliberada; não se presumem fim de cobertura ou emissão.']])['id'];
|
||||
$select($f['reader_id']); $fiscal->decide($temporal,$f['reader_id'],['version'=>1,'decision'=>'approved','reason'=>'Aprovado resultado condicionado, sem exigibilidade determinada.']);
|
||||
$fiscal->proofActivation($profile,$f['reader_id'],1,true);
|
||||
return ['fiscal_draft'=>$draft,'fiscal_profile_proposal'=>$profile,'fiscal_temporal_proposal'=>$temporal];
|
||||
});
|
||||
file_put_contents($file,json_encode($f+$result,JSON_THROW_ON_ERROR));
|
||||
echo "Synthetic fiscal proposals and draft prepared only in sgf_tempo_test; no calculation precomputed.\n";
|
||||
@@ -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 local development database required.');
|
||||
$db=\Illuminate\Support\Facades\DB::class;
|
||||
$file=storage_path('app/service-fiscal-preservation-before.json');
|
||||
$before=in_array('--before',$argv,true); $grants=in_array('--after-grants',$argv,true);
|
||||
$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_|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.');
|
||||
$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 fingerprint FROM (SELECT * FROM "'.$table.'"'.$where.') t');
|
||||
$summary[$table]=['count'=>(int)$r->n,'hash'=>$r->fingerprint];
|
||||
}
|
||||
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')]],JSON_THROW_ON_ERROR));
|
||||
echo "Existing SGF development records fingerprinted in database; no record contents disclosed.\n"; exit;
|
||||
}
|
||||
if($summary!==$baseline['tables']) throw new RuntimeException('Existing SGF record fingerprint changed; review before proceeding.');
|
||||
foreach(['sf_proposals','sf_decisions','sf_activation_events'] as $table) if($db::table($table)->count()!==0) throw new RuntimeException('Unexpected real fiscal lifecycle record.');
|
||||
if($grants) {
|
||||
$actual=$db::table('permission_grants')->where('permission','like','service_fiscal.%')->orderBy('user_id')->orderBy('permission')->get(['user_id','tenant_id','company_id','unit_id','permission'])->map(fn($r)=>(array)$r)->all();
|
||||
$expected=[];
|
||||
foreach([1=>['view','propose','calculate'],2=>['view','approve']] as $user=>$actions) foreach($actions as $a) $expected[]=['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'unit_id'=>$a==='calculate'?1:null,'permission'=>'service_fiscal.'.$a];
|
||||
usort($expected,fn($a,$b)=>[$a['user_id'],$a['permission']]<=>[$b['user_id'],$b['permission']]);
|
||||
if($actual!==$expected) throw new RuntimeException('Fiscal grants differ from specifically confirmed matrix.');
|
||||
$new=$db::table('identity_audit')->where('id','>',$baseline['max']['identity_audit'])->where('action','service_fiscal.initial_grant')->count();
|
||||
if($new!==5) throw new RuntimeException('Expected five audited initial grants.');
|
||||
}
|
||||
file_put_contents(base_path('docs/proofs/service-fiscal-dev-preservation'.($grants?'-grants':'').'.json'),json_encode(['database'=>'sgf_tempo_dev','preexisting_tables_checked'=>count($summary),'preexisting_records_unchanged'=>true,'new_fiscal_tables_empty'=>true,'real_profile_activated'=>false,'confirmed_grants_verified'=>$grants?5:0,'prior_grants_and_audit_unchanged'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo $grants ? "Exactly five confirmed fiscal grants and audit entries verified; prior records preserved; no real profile created or activated.\n" : "Migration preserved existing records and grants; all new fiscal tables empty.\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\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
$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(1500000); } });
|
||||
}
|
||||
echo json_encode(app(\App\Modules\Services\ServiceFiscal::class)->calculate($in['draft'],$in['actor'],$in['command']),JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
Reference in New Issue
Block a user