Preserva classificacao contabilistica aceite e bases locais
Regista aceitacao funcional do primeiro incremento PGC-NIRF e preserva as dependencias documentais e tecnicas existentes. A validacao do calculo fiscal continua separada e pendente. Sem credenciais, bases operacionais ou publicacao.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
<?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();
|
||||
$input=json_decode(stream_get_contents(STDIN),true,32,JSON_THROW_ON_ERROR);
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($input['actor'],$input['tenant'],$input['company'],$input['unit']);
|
||||
if($input['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);}});}
|
||||
try{$result=app(\App\Modules\Services\CatalogueAccounting::class)->decide($input['proposal'],$input['actor'],['decision'=>$input['decision'],'reason'=>'Decisão sintética concorrente'],$input['key']);echo json_encode(['status'=>200,'result'=>$result],JSON_THROW_ON_ERROR).PHP_EOL;}
|
||||
catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){echo json_encode(['status'=>$e->getStatusCode()],JSON_THROW_ON_ERROR).PHP_EOL;}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
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();
|
||||
\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);
|
||||
$c=app(\App\Modules\Services\CatalogueAccounting::class);
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($f['manager_id'],$f['tenant'],$f['company'],$f['unit']);
|
||||
$proposal=$c->propose($f['manager_id'],['kind'=>'category','data'=>['name'=>'Categoria de concorrência sintética','account_id'=>$f['accounting_account'],'conditions'=>'Somente ensaio de decisões concorrentes.'],'valid_from'=>'2026-01-01','reason'=>'Ensaiar concorrência sem operações reais.'],(string)Str::uuid())['id'];$c->submit($proposal,$f['manager_id'],(string)Str::uuid());
|
||||
$base=['actor'=>$f['reader_id'],'tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit'],'proposal'=>$proposal];
|
||||
$a=new Process([PHP_BINARY,'scripts/accounting-concurrency-worker.php','--env=testing'],base_path(),null,json_encode($base+['pause'=>true,'decision'=>'approved','key'=>(string)Str::uuid()]));
|
||||
$b=new Process([PHP_BINARY,'scripts/accounting-concurrency-worker.php','--env=testing'],base_path(),null,json_encode($base+['decision'=>'refused','key'=>(string)Str::uuid()]));
|
||||
$a->start();if(!$a->waitUntil(fn($type,$buffer)=>str_contains($buffer,'LOCK_HELD')))throw new RuntimeException('Concurrent lock not observed.');$b->start();$a->wait();$b->wait();
|
||||
$statuses=[];foreach([$a,$b] as $p){if(!$p->isSuccessful())throw new RuntimeException('Decision worker failed.');$lines=array_values(array_filter(explode("\n",trim($p->getOutput())),fn($s)=>str_starts_with($s,'{')));$statuses[]=json_decode(end($lines),true,32,JSON_THROW_ON_ERROR)['status'];}
|
||||
if($statuses!==[200,409]||DB::table('ca_decisions')->where('proposal_id',$proposal)->count()!==1)throw new RuntimeException('Contradictory decision accepted.');
|
||||
file_put_contents(base_path('docs/proofs/accounting-concurrency.json'),json_encode(['database'=>'sgf_tempo_test','parallel_processes'=>true,'lock_overlap_observed'=>true,'statuses'=>$statuses,'decisions'=>1],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Concurrent approval/refusal verified: one decision accepted, conflicting decision refused (409).\n";
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
use App\Modules\Services\CatalogueAccounting;
|
||||
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();
|
||||
$file='C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';$f=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);
|
||||
if(DB::table('ca_proposals')->exists())throw new RuntimeException('Accounting fixture already present; no data deleted.');
|
||||
foreach(['manager_id','reader_id'] as $field)if(!str_ends_with(DB::table('users')->where('id',$f[$field])->value('email'),'@example.test'))throw new RuntimeException('Synthetic identities required.');
|
||||
$result=DB::transaction(function()use($f){
|
||||
foreach(['manager_id'=>'propose','reader_id'=>'approve'] as $field=>$action)foreach(['view',$action] as $right)\Tests\Support\IdentityFixture::grant($f[$field],$f['tenant'],$f['company'],"catalogue_accounting.$right");
|
||||
$select=function(int $actor)use($f){app(\App\Shared\Context\CompanyContext::class)->selectFor($actor,$f['tenant'],$f['company'],$f['unit']);};
|
||||
$select($f['manager_id']);$workflow=app(\App\Modules\Services\ServiceWorkflow::class);
|
||||
$client=$workflow->save('clients',null,$f['manager_id'],['name'=>'Cliente de classificação sintética'],(string)Str::uuid())['id'];
|
||||
$service=$workflow->save('services',null,$f['manager_id'],['name'=>'Apoio administrativo pontual — Ensaio'],(string)Str::uuid())['id'];
|
||||
$draft=$workflow->save('service_drafts',null,$f['manager_id'],['client_id'=>$client,'lines'=>[['service_id'=>$service,'quantity'=>'1','unit_price'=>'100']]],(string)Str::uuid())['id'];
|
||||
$create=function(string $kind,array $data,bool $approve=true)use($f,$select){
|
||||
$select($f['manager_id']);$c=app(CatalogueAccounting::class);$id=$c->propose($f['manager_id'],['kind'=>$kind,'data'=>$data,'valid_from'=>'2026-01-01','reason'=>'Proposta exclusivamente sintética para demonstração.'],(string)Str::uuid())['id'];
|
||||
if($approve){$c->submit($id,$f['manager_id'],(string)Str::uuid());$select($f['reader_id']);$c->decide($id,$f['reader_id'],['decision'=>'approved','reason'=>'Validação sintética; não é enquadramento da Tempo Lda.'],(string)Str::uuid());}
|
||||
return $id;
|
||||
};
|
||||
$plan=$create('plan',['plan'=>'PGC-NIRF','source_version'=>CatalogueAccounting::SOURCE_VERSION]);
|
||||
$account=$create('account',['plan_id'=>$plan,'code'=>'7.2.900','name'=>'Serviços normais — conta empresarial sintética','origin'=>'enterprise','movable'=>true,'evidence'=>'Desdobramento empresarial de teste, não código oficial proposto à Tempo Lda.']);
|
||||
$category=$create('category',['name'=>'Serviços normais — Categoria sintética','account_id'=>$account,'conditions'=>'Apenas apoio administrativo da actividade normal sintética.']);
|
||||
$link=$create('service_link',['service_id'=>$service,'category_id'=>$category]);
|
||||
$select($f['manager_id']);$c=app(CatalogueAccounting::class);$consulted=$c->resolve($service,'2026-10-26',$f['manager_id']);$resolution=$c->capture($f['manager_id'],['area'=>'service_drafts','record_id'=>$draft,'record_version'=>1,'service_id'=>$service,'reference_date'=>'2026-10-26','resolution_revision'=>$consulted['revision']],(string)Str::uuid())['id'];
|
||||
$refuse=$create('category',['name'=>'Proposta sintética a recusar','account_id'=>$account,'conditions'=>'Evidência incompleta; não activar.'],false);$select($f['manager_id']);$c->submit($refuse,$f['manager_id'],(string)Str::uuid());
|
||||
return ['accounting_service'=>$service,'accounting_draft'=>$draft,'accounting_plan'=>$plan,'accounting_account'=>$account,'accounting_category'=>$category,'accounting_link'=>$link,'accounting_resolution'=>$resolution,'accounting_refuse'=>$refuse];
|
||||
});
|
||||
file_put_contents($file,json_encode($f+$result,JSON_THROW_ON_ERROR));
|
||||
echo "Synthetic classification graph, captured baseline and pending refusal prepared only in sgf_tempo_test. Credentials omitted.\n";
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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;$summary=[];
|
||||
$afterGrants=in_array('--after-grants',$argv,true);
|
||||
foreach(['a_clients','a_services','a_service_drafts','a_billing_drafts','a_history','a_events','a_pending','a_outbox','a_commands','b_internal_confirmations','b_internal_confirmation_commands','doc_records','doc_history','document_preview_models','document_preview_model_history','service_calculation_profiles','service_calculation_results','permission_grants'] as $table){
|
||||
if(!\Illuminate\Support\Facades\Schema::hasTable($table))continue;
|
||||
$query=$db::table($table);
|
||||
if($table==='permission_grants'&&$afterGrants)$query->whereNot(function($q){$q->where('tenant_id',1)->where('company_id',1)->whereNull('unit_id')->where(function($q){$q->where(fn($q)=>$q->where('user_id',1)->whereIn('permission',['catalogue_accounting.view','catalogue_accounting.propose']))->orWhere(fn($q)=>$q->where('user_id',2)->whereIn('permission',['catalogue_accounting.view','catalogue_accounting.approve']));});});
|
||||
$rows=$query->orderBy($table==='a_outbox'?'event_id':'id')->get();$summary[$table]=['count'=>$rows->count(),'hash'=>hash('sha256',$rows->toJson())];
|
||||
}
|
||||
foreach(['app/Modules/Services/ServiceCalculation.php','database/migrations/2026_10_26_030000_create_service_calculations.php','resources/js/DocumentPreview.jsx'] as $path)$summary['file:'.$path]=hash_file('sha256',base_path($path));
|
||||
$file=storage_path('app/accounting-preservation-before.json');
|
||||
if(in_array('--before',$argv,true)){file_put_contents($file,json_encode($summary,JSON_THROW_ON_ERROR));echo "Existing development records and calculation files fingerprinted; no contents disclosed.\n";exit;}
|
||||
if($summary!==json_decode(file_get_contents($file),true,64,JSON_THROW_ON_ERROR))throw new RuntimeException('Existing data, prior grants or calculation files changed.');
|
||||
$counts=[];foreach(['ca_proposals','ca_submissions','ca_decisions','ca_resolutions','ca_commands'] as $table){$counts[$table]=$db::table($table)->count();if($counts[$table]!==0)throw new RuntimeException('Unexpected real classification rows.');}
|
||||
if($afterGrants){
|
||||
$expected=[1=>['catalogue_accounting.view','catalogue_accounting.propose'],2=>['catalogue_accounting.view','catalogue_accounting.approve']];
|
||||
foreach($expected as $user=>$rights){$actual=$db::table('permission_grants')->where('user_id',$user)->where('permission','like','catalogue_accounting.%')->get(['tenant_id','company_id','unit_id','permission']);if($actual->count()!==2)throw new RuntimeException('Unexpected classification rights.');foreach($actual as $g)if($g->tenant_id!==1||$g->company_id!==1||$g->unit_id!==null||!in_array($g->permission,$rights,true))throw new RuntimeException('Unexpected classification scope.');}
|
||||
file_put_contents(base_path('docs/proofs/accounting-access-results.json'),json_encode(['database'=>'sgf_tempo_dev','confirmed_grants_applied'=>4,'company_id'=>1,'proposer_id'=>1,'validator_id'=>2,'no_unit_grants'=>true,'prior_grants_unchanged'=>true,'business_records_and_calculation_files_unchanged'=>true,'real_classification_rows'=>0],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Exactly four confirmed company grants verified; prior grants, data and calculation unchanged; no unit grants or real classification rows.\n";
|
||||
}else{
|
||||
file_put_contents(base_path('docs/proofs/accounting-dev-preservation.json'),json_encode(['database'=>'sgf_tempo_dev','existing_data_hashes_unchanged'=>true,'existing_grants_unchanged'=>true,'calculation_files_unchanged'=>true,'new_table_counts'=>$counts],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Development migration preserved existing records, grants and calculation files; all five new tables empty.\n";
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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);
|
||||
$input=['actor'=>$f['manager_id'],'tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit'],'draft'=>$f['calculation_drafts'][1],
|
||||
'command'=>['draft_version'=>1,'preparation_version'=>1,'profile_id'=>$f['calculation_profile'],'profile_version'=>1]];
|
||||
$a=new Process([PHP_BINARY,'scripts/calculation-worker.php','--env=testing'],base_path(),null,json_encode($input+['pause'=>true]));
|
||||
$b=new Process([PHP_BINARY,'scripts/calculation-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 concurrent lock established.'); }
|
||||
$b->start(); $a->wait(); $b->wait();
|
||||
$results=[];
|
||||
foreach ([$a,$b] as $worker) {
|
||||
if (!$worker->isSuccessful()) { throw new RuntimeException('Calculation worker failed.'); }
|
||||
$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()!==1) { throw new RuntimeException('Concurrent duplicate or unexpected replay.'); }
|
||||
file_put_contents(base_path('docs/proofs/calculation-concurrency.json'),json_encode(['database'=>'sgf_tempo_test','two_php_processes'=>true,'lock_overlap_observed'=>true,'same_result_id'=>true,'one_insert_one_replay'=>true,'result_count'=>1],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "Concurrent calculation: one immutable result, one replay, two overlapping PHP processes.\n";
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Modules\Services\ServiceCalculation;
|
||||
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();
|
||||
$file='C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';
|
||||
$f=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($f['manager_id'],$f['tenant'],$f['company'],$f['unit']);
|
||||
$ids=DB::transaction(function () use ($f) {
|
||||
$workflow=app(\App\Modules\Services\ServiceWorkflow::class); $actor=$f['manager_id'];
|
||||
$client=$workflow->save('clients',null,$actor,['name'=>'Cliente sintético de cálculo'],(string)Str::uuid())['id'];
|
||||
$service=$workflow->save('services',null,$actor,['name'=>'Serviço sintético de cálculo'],(string)Str::uuid())['id'];
|
||||
$profile=DB::table('service_calculation_profiles')->insertGetId(['tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>$f['unit'],'identifier'=>'ENSAIO-MZN-16','version'=>1,'definition'=>json_encode(['kind'=>'synthetic','currency'=>'MZN','rate_percent'=>'16','policy'=>ServiceCalculation::POLICY,'sources'=>ServiceCalculation::SOURCES]),'created_at'=>now()]);
|
||||
$drafts=[];
|
||||
foreach (['100','1000.03','0.03','0.0001'] as $price) {
|
||||
$source=$workflow->save('service_drafts',null,$actor,['name'=>'Ensaio base '.$price,'client_id'=>$client,'currency'=>'MZN','lines'=>[['service_id'=>$service,'quantity'=>'1','unit_price'=>$price]]],(string)Str::uuid())['id'];
|
||||
$draft=$workflow->save('billing_drafts',null,$actor,['source_id'=>$source,'name'=>'Ensaio base '.$price],(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','units'=>['UN'],'payment_instruction_ids'=>[]]);
|
||||
$drafts[]=$draft;
|
||||
}
|
||||
return ['calculation_profile'=>$profile,'calculation_drafts'=>$drafts];
|
||||
});
|
||||
file_put_contents($file,json_encode($f+$ids,JSON_THROW_ON_ERROR));
|
||||
echo "Four synthetic calculation drafts and profile created only in sgf_tempo_test. No results precomputed.\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();
|
||||
$db=\Illuminate\Support\Facades\DB::class;
|
||||
if (app()->environment()!=='local' || $db::selectOne('select current_database() as db')->db!=='sgf_tempo_dev') { throw new RuntimeException('Development preservation destination refused.'); }
|
||||
$summary=[];
|
||||
foreach (['a_clients','a_services','a_service_drafts','a_billing_drafts','a_history','a_events','a_outbox','a_pending','b_internal_confirmations','doc_records','doc_history','document_preview_models','document_preview_model_history','permission_grants'] as $table) {
|
||||
$rows=$db::table($table)->orderBy($table==='a_outbox' ? 'event_id' : 'id')->get(); $summary[$table]=['count'=>$rows->count(),'sha256'=>hash('sha256',$rows->toJson())];
|
||||
}
|
||||
$file=storage_path('app/calculation-preservation-before.json');
|
||||
if (in_array('--before',$argv,true)) { file_put_contents($file,json_encode($summary,JSON_THROW_ON_ERROR)); echo "Development preservation fingerprints captured; no contents displayed.\n"; }
|
||||
else {
|
||||
if ($summary!==json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR)) { throw new RuntimeException('Existing records changed during migration.'); }
|
||||
foreach (['service_calculation_profiles','service_calculation_results'] as $table) { if ($db::table($table)->count()!==0) { throw new RuntimeException('Unexpected development calculation records.'); } }
|
||||
file_put_contents(base_path('docs/proofs/calculation-dev-preservation.json'),json_encode(['database'=>'sgf_tempo_dev','existing_tables_compared'=>array_keys($summary),'identical_counts_and_content_hashes'=>true,'new_profiles'=>0,'new_results'=>0],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "Development migration verified: existing records identical; zero profiles and zero results.\n";
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?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();
|
||||
$input=json_decode(stream_get_contents(STDIN),true,32,JSON_THROW_ON_ERROR);
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($input['actor'],$input['tenant'],$input['company'],$input['unit']);
|
||||
if ($input['pause'] ?? false) {
|
||||
$paused=false;
|
||||
\Illuminate\Support\Facades\DB::listen(function ($query) use (&$paused): void {
|
||||
if (!$paused && str_contains($query->sql,'"companies"') && str_contains($query->sql,'for update')) {
|
||||
$paused=true; echo "LOCK_HELD\n"; flush(); usleep(1500000);
|
||||
}
|
||||
});
|
||||
}
|
||||
$result=app(\App\Modules\Services\ServiceCalculation::class)->calculate($input['draft'],$input['actor'],$input['command']);
|
||||
echo json_encode($result,JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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)!==3 || array_filter($args,fn($v)=>!ctype_digit($v)))throw new RuntimeException('Usage: catalogue-accounting-access.php proposer_id validator_id company_id [--apply]; exact prior confirmation required.');
|
||||
[$proposer,$validator,$companyId]=array_map('intval',$args);if($proposer===$validator)throw new RuntimeException('The agreed workflow uses distinct preparer and validator accounts.');
|
||||
$db=\Illuminate\Support\Facades\DB::class;
|
||||
$company=$db::table('companies')->where('id',$companyId)->first(['id','tenant_id','name']);
|
||||
$accounts=$db::table('users')->whereIn('id',[$proposer,$validator])->orderBy('id')->get(['id','name','email']);
|
||||
$operations=[];
|
||||
foreach ($accounts as $account) {
|
||||
if (!$company || !$db::table('company_memberships')->where('user_id',$account->id)->where('company_id',$company->id)->where('tenant_id',$company->tenant_id)->exists()) { throw new RuntimeException('Existing membership missing. No association inferred.'); }
|
||||
foreach (['catalogue_accounting.view',$account->id===$proposer ? 'catalogue_accounting.propose' : 'catalogue_accounting.approve'] as $permission) { $operations[]=['user_id'=>$account->id,'tenant_id'=>$company->tenant_id,'company_id'=>$company->id,'unit_id'=>null,'permission'=>$permission]; }
|
||||
}
|
||||
if ($accounts->count()!==2) { throw new RuntimeException('Two existing accounts required.'); }
|
||||
if (!in_array('--apply',$argv,true)) { echo json_encode(['accounts'=>$accounts,'company'=>$company,'proposed_grants'=>$operations,'applied'=>false],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL; exit; }
|
||||
$inserted=$db::transaction(function()use($db,$company,$operations){
|
||||
$db::table('companies')->where('id',$company->id)->where('tenant_id',$company->tenant_id)->lockForUpdate()->firstOrFail();$count=0;
|
||||
foreach($operations as $operation){
|
||||
foreach(['company.view','services.view'] as $right)app(\App\Modules\Identity\Services\Permissions::class)->require($operation['user_id'],$company->tenant_id,$company->id,$right);
|
||||
if(!$db::table('company_memberships')->where('user_id',$operation['user_id'])->where('tenant_id',$company->tenant_id)->where('company_id',$company->id)->exists())throw new RuntimeException('Membership changed. No grant applied.');
|
||||
if($db::table('permission_grants')->insertOrIgnore($operation+['created_at'=>now(),'updated_at'=>now()])){
|
||||
$count++;\App\Modules\Identity\Services\Audit::record('catalogue_accounting.access.initial_grant',null,$company->tenant_id,$company->id,subject:$operation['user_id'],details:['permission'=>$operation['permission'],'reason'=>'Exact initial classification grants specifically confirmed by the user; no fiscal or posting powers.']);
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
});
|
||||
echo "Applied $inserted specifically confirmed classification grants. Existing rights preserved.\n";
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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 ($arg) => $arg !== '--apply'));
|
||||
if (count($args) !== 4 || array_filter($args, fn ($id) => ! ctype_digit($id))) { throw new RuntimeException('Usage: document-access.php editor_id reader_id company_id unit_id [--apply]; specific confirmation required.'); }
|
||||
[$editor, $reader, $company, $unit] = array_map('intval', $args);
|
||||
if ($editor === $reader) { throw new RuntimeException('The two specifically confirmed accounts must be distinct.'); }
|
||||
$db = \Illuminate\Support\Facades\DB::class;
|
||||
$scope = $db::table('units as n')->join('companies as c', 'n.company_id', '=', 'c.id')->where('n.id', $unit)->where('c.id', $company)->first(['n.tenant_id','n.company_id','n.id as unit_id','n.name as unit_name','c.name as company_name']);
|
||||
if (! $scope) { throw new RuntimeException('Existing scope not found.'); }
|
||||
$plan = [];
|
||||
foreach ([$editor => ['view','edit'], $reader => ['view']] as $user => $actions) {
|
||||
$account = $db::table('users')->where('id', $user)->first(['id','name','email']);
|
||||
if (! $account) { throw new RuntimeException('Existing account not found; no account created.'); }
|
||||
foreach (['issuer_documents','client_documents','draft_documents'] as $area) { foreach ($actions as $action) { $plan[] = ['user' => $account, 'tenant_id' => $scope->tenant_id, 'company_id' => $company, 'unit_id' => $area === 'draft_documents' ? $unit : null, 'permission' => "$area.$action"]; } }
|
||||
}
|
||||
if (! in_array('--apply', $argv, true)) { echo json_encode(['scope' => $scope, 'grants' => $plan], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL; exit(0); }
|
||||
$db::transaction(function () use ($db, $scope, $plan): void {
|
||||
$db::table('companies')->where('id', $scope->company_id)->lockForUpdate()->firstOrFail();
|
||||
foreach ($plan as $grant) {
|
||||
$user = $grant['user']->id;
|
||||
foreach (['company.view','unit.view'] as $permission) { app(\App\Modules\Identity\Services\Permissions::class)->require($user, $scope->tenant_id, $scope->company_id, $permission, $permission === 'unit.view' ? $scope->unit_id : null); }
|
||||
$row = array_diff_key($grant, ['user' => true]) + ['user_id' => $user];
|
||||
if ($db::table('permission_grants')->insertOrIgnore($row + ['created_at' => now(), 'updated_at' => now()])) { \App\Modules\Identity\Services\Audit::record('documents.access.initial_grant', null, $scope->tenant_id, $scope->company_id, $grant['unit_id'], $user, ['permission' => $grant['permission'], 'reason' => 'Preparação documental: concessão especificamente confirmada pelo utilizador.']); }
|
||||
}
|
||||
});
|
||||
echo "Specifically confirmed document preparation grants applied; no accounts or financial powers created.\n";
|
||||
@@ -0,0 +1,60 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const fixturePath = 'C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';
|
||||
const fixture = JSON.parse(await fs.readFile(fixturePath, 'utf8'));
|
||||
const base = 'http://127.0.0.1:8001';
|
||||
function client() {
|
||||
const cookies = new Map(); let csrf = ''; let token = '';
|
||||
return {
|
||||
async request(path, method = 'GET', body, context = token) {
|
||||
const response = await fetch(base + path, { method, headers: { Accept: 'application/json', 'Content-Type': 'application/json', Cookie: [...cookies].map(([k,v]) => `${k}=${v}`).join('; '), 'X-CSRF-TOKEN': csrf, 'X-SGF-Context': context, 'Idempotency-Key': randomUUID() }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
||||
for (const cookie of response.headers.getSetCookie()) { const pair = cookie.split(';', 1)[0], split = pair.indexOf('='); cookies.set(pair.slice(0, split), pair.slice(split + 1)); }
|
||||
const data = await response.json().catch(() => ({})); if (data.csrf) csrf = data.csrf; if (data.context?.token) token = data.context.token;
|
||||
return { status: response.status, data };
|
||||
},
|
||||
async login(email) { await this.request('/api/session'); assert.equal((await this.request('/api/login', 'POST', { email, password: fixture.password })).status, 200); return this.select(); },
|
||||
async select(unit = fixture.unit, company = fixture.company) { const response = await this.request('/api/context', 'POST', { tenant_id: fixture.tenant, company_id: company, unit_id: unit }); assert.equal(response.status, 200); return response.data.context.token; }
|
||||
};
|
||||
}
|
||||
const manager = client(), reader = client(); await manager.login('gestor@example.test'); await reader.login('consulta@example.test');
|
||||
async function create(area, data) { const result = await manager.request('/api/a/' + area, 'POST', data); assert.equal(result.status, 201); return result.data.id; }
|
||||
async function save(kind, id, data) { const result = await manager.request(`/api/document-preparation/${kind}/${id}`, 'PATCH', data); assert.equal(result.status, 200); return result.data; }
|
||||
const customer = await create('clients', { name: 'Cliente documental de demonstração' });
|
||||
const service = await create('services', { name: 'Serviço documental de demonstração' });
|
||||
const source = await create('service_drafts', { client_id: customer, lines: [{ service_id: service, quantity: '1.25', unit_price: '0.00000001' }] });
|
||||
const oldDraft = await create('billing_drafts', { source_id: source, name: 'B1 anterior à preparação documental' });
|
||||
assert.equal((await manager.request(`/api/b1/billing-drafts/${oldDraft}/confirm-internal`, 'POST', { version: 1 })).status, 200);
|
||||
const oldBefore = (await manager.request(`/api/a/billing_drafts/${oldDraft}`)).data.confirmation;
|
||||
assert.equal(oldBefore.snapshot.schema_version, undefined);
|
||||
const profile = { version: 0, nuit: 'IDENTIFICAÇÃO SINTÉTICA', address: 'Morada sintética', locality: 'Localidade de teste', country: 'País de teste' };
|
||||
await save('issuer', fixture.company, profile); await save('client', customer, profile);
|
||||
const privateMarker = 'SYNTHETIC-' + randomUUID();
|
||||
const paymentData = { version: 0, label: 'Instruções de demonstração — sem operação bancária', bank: 'Banco fictício de teste', holder: 'Titular sintético', account_number: privateMarker, instructions: 'Apenas instruções sintéticas; não efectuar pagamentos.', currency: null };
|
||||
const instruction = (await save('payment', 0, paymentData)).id;
|
||||
const preparation = { version: 0, draft_version: 1, currency: null, service_date: '2026-11-01', period_start: '2026-11-01', period_end: '2026-11-30', payment_terms: 'Prazo a conferir com vencimento; sem regra aprovada', proposed_due_date: '2026-11-15', price_mode: 'undefined', units: ['hora'], payment_instruction_ids: [instruction] };
|
||||
await save('service_drafts', source, preparation);
|
||||
const newDraft = await create('billing_drafts', { source_id: source, name: 'Documento interno com preparação versão 2' });
|
||||
const reused = (await manager.request(`/api/document-preparation/billing_drafts/${newDraft}`)).data;
|
||||
assert.equal(reused.data.units[0], 'hora'); assert.equal(reused.data.payment_instruction_ids[0], instruction); assert.equal(reused.currency, null);
|
||||
assert.equal((await manager.request(`/api/b1/billing-drafts/${newDraft}/confirm-internal`, 'POST', { version: 1 })).status, 200);
|
||||
const snapshot = (await manager.request(`/api/a/billing_drafts/${newDraft}`)).data.confirmation;
|
||||
assert.equal(snapshot.snapshot.schema_version, 2); assert.equal(snapshot.snapshot.document_preparation.payment_instructions[0].data.account_number, privateMarker);
|
||||
await save('payment', instruction, { ...paymentData, version: 1, instructions: 'Instruções sintéticas revistas após confirmação.' });
|
||||
assert.deepEqual((await manager.request(`/api/a/billing_drafts/${oldDraft}`)).data.confirmation, oldBefore);
|
||||
assert.deepEqual((await manager.request(`/api/a/billing_drafts/${newDraft}`)).data.confirmation, snapshot);
|
||||
assert.equal((await reader.request(`/api/document-preparation/payment/${instruction}`)).status, 200);
|
||||
assert.equal((await reader.request(`/api/document-preparation/payment/${instruction}`, 'PATCH', { ...paymentData, version: 2 })).status, 403);
|
||||
assert.equal((await manager.request(`/api/document-preparation/payment/${instruction}`, 'PATCH', { ...paymentData, version: 1 })).status, 409);
|
||||
const oldContext = await manager.select(); await manager.select(null);
|
||||
assert.equal((await manager.request(`/api/document-preparation/issuer/${fixture.company}`, 'PATCH', { ...profile, version: 1 }, oldContext)).status, 409);
|
||||
await manager.select(null, fixture.sibling);
|
||||
assert.equal((await manager.request(`/api/document-preparation/payment/${instruction}`)).status, 403);
|
||||
await manager.select();
|
||||
const editableDraft = await create('billing_drafts', { source_id: source, name: 'Percurso visual — preparação documental' });
|
||||
fixture.document_client_id = customer; fixture.document_source_id = source; fixture.document_draft_id = editableDraft;
|
||||
await fs.writeFile(fixturePath, JSON.stringify(fixture));
|
||||
await manager.request('/api/logout', 'POST'); await reader.request('/api/logout', 'POST');
|
||||
await fs.writeFile('docs/proofs/document-http-results.json', JSON.stringify({ database: 'sgf_tempo_test', real_synthetic_login: true, results: ['Client created with name only; company/client profiles reused.', 'Source preparation and selected issuer instruction reused in billing draft.', 'Existing B1 v1 and future v2 snapshots unchanged after configuration updates.', 'Reader can consult instructions; editing denied 403.', 'Old version/context denied 409; other company denied 403.', 'Currency unknown, exact prices unchanged; no receipt, issue or accounting operation.'], instruction_values: 'omitted' }, null, 2));
|
||||
console.log('Isolated document preparation HTTP proof passed. Instruction values omitted.');
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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.'); }
|
||||
$path = 'C:/laragon/data/sgf-tempo/run/document-preservation.json';
|
||||
$tables = ['users','tenants','companies','units','company_memberships','a_clients','a_services','a_service_drafts','a_billing_drafts','a_history','a_events','a_outbox','a_pending','a_commands','b_internal_confirmations','doc_records','doc_history'];
|
||||
$state = [];
|
||||
foreach ($tables as $table) {
|
||||
$rows = \Illuminate\Support\Facades\DB::table($table)->get()->map(fn ($row) => json_encode((array) $row, JSON_THROW_ON_ERROR))->all(); sort($rows);
|
||||
$state[$table] = ['count' => count($rows), 'digest' => hash('sha256', implode("\n", $rows))];
|
||||
}
|
||||
if (($argv[1] ?? '') === 'before') { file_put_contents($path, json_encode($state, JSON_THROW_ON_ERROR)); echo "Local preservation baseline recorded; no record values displayed.\n"; }
|
||||
elseif (($argv[1] ?? '') === 'after') {
|
||||
$before = json_decode(file_get_contents($path), true, 32, JSON_THROW_ON_ERROR);
|
||||
if ($before !== $state) { throw new RuntimeException('Existing record preservation comparison failed; inspect locally without printing values.'); }
|
||||
echo "Existing identity, A records and B1 snapshots match the pre-migration baseline exactly.\n";
|
||||
} else { throw new RuntimeException('Usage: document-preservation.php before|after'); }
|
||||
@@ -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\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
$db = \Illuminate\Support\Facades\DB::class;
|
||||
$kinds = $db::table('doc_records')->selectRaw('kind,count(*) AS count')->groupBy('kind')->orderBy('kind')->get()->all();
|
||||
$schemas = ['1' => 0, '2' => 0];
|
||||
foreach ($db::table('b_internal_confirmations')->pluck('snapshot') as $encoded) { $snapshot = json_decode($encoded, true, 64, JSON_THROW_ON_ERROR); $schemas[(string) ($snapshot['schema_version'] ?? 1)]++; }
|
||||
$privateValues = $db::table('doc_records')->where('kind', 'payment')->pluck('data')->map(fn ($value) => json_decode($value, true, 32, JSON_THROW_ON_ERROR)['account_number'] ?? null)->filter()->all();
|
||||
$audit = $db::table('identity_audit')->pluck('details')->implode(''); $log = is_file(storage_path('logs/laravel.log')) ? file_get_contents(storage_path('logs/laravel.log')) : '';
|
||||
foreach ($privateValues as $value) { if (str_contains($audit, $value) || str_contains($log, $value)) { throw new RuntimeException('Instruction value detected in audit or technical log; no value displayed.'); } }
|
||||
$report = ['database' => 'sgf_tempo_test', 'preparation_records_by_kind' => $kinds, 'preparation_history_versions' => $db::table('doc_history')->count(), 'confirmation_snapshot_schemas' => $schemas, 'instruction_values_in_audit_or_technical_log' => false, 'instruction_values' => 'omitted', 'receipt_table_present' => \Illuminate\Support\Facades\Schema::hasTable('receipts'), 'journal_table_present' => \Illuminate\Support\Facades\Schema::hasTable('journal_entries')];
|
||||
file_put_contents(base_path('docs/proofs/document-capture.json'), json_encode($report, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "Document capture metrics recorded with no instruction values; v1/v2 schemas and audit/log confidentiality checked.\n";
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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; $counts = [];
|
||||
foreach (['users','companies','units','a_clients','a_services','a_service_drafts','a_billing_drafts','b_internal_confirmations','doc_records','doc_history'] as $table) { $counts[$table] = $db::table($table)->count(); }
|
||||
$grants = $db::table('permission_grants')->whereIn('permission', ['issuer_documents.view','issuer_documents.edit','client_documents.view','client_documents.edit','draft_documents.view','draft_documents.edit'])->orderBy('user_id')->orderBy('permission')->get(['user_id','company_id','unit_id','permission']);
|
||||
echo json_encode(['database' => 'sgf_tempo_dev', 'counts' => $counts, 'document_grants' => $grants], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL;
|
||||
@@ -9,7 +9,7 @@ foreach (['users', 'tenants', 'companies', 'units'] as $table) {
|
||||
fwrite(STDERR, "Test database is not empty. No data was deleted. Stop other test workflows and prepare the test database explicitly.\n"); exit(1);
|
||||
}
|
||||
}
|
||||
$fixture = \Illuminate\Support\Facades\DB::transaction(fn () => \Tests\Support\InternalConfirmationFixture::create());
|
||||
$fixture = \Illuminate\Support\Facades\DB::transaction(fn () => \Tests\Support\DocumentPreviewFixture::create());
|
||||
$safe = array_intersect_key($fixture, array_flip(['tenant', 'otherTenant', 'company', 'sibling', 'foreign', 'unit', 'hidden', 'siblingUnit', 'password']));
|
||||
$safe['manager_id'] = $fixture['manager']->id;
|
||||
$safe['reader_id'] = $fixture['reader']->id;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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 ($arg) => $arg !== '--apply'));
|
||||
if (count($args) !== 2 || array_filter($args, fn ($id) => ! ctype_digit($id))) { throw new RuntimeException('Usage: preview-access.php user_id company_id [--apply]; specific confirmation required.'); }
|
||||
[$user, $company] = array_map('intval', $args); $db = \Illuminate\Support\Facades\DB::class;
|
||||
$account = $db::table('users')->where('id', $user)->first(['id','name','email']);
|
||||
$scope = $db::table('companies')->where('id', $company)->first(['id','tenant_id','name']);
|
||||
if (! $account || ! $scope) { throw new RuntimeException('Existing account/company not found; nothing created.'); }
|
||||
$permission = \App\Modules\Services\DocumentPreview::CONFIGURE;
|
||||
if (! in_array('--apply', $argv, true)) { echo json_encode(['account' => $account, 'company' => $scope, 'permission' => $permission, 'unit_id' => null], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL; exit(0); }
|
||||
$db::transaction(function () use ($db, $scope, $user, $permission): void {
|
||||
$db::table('companies')->where('id', $scope->id)->lockForUpdate()->firstOrFail();
|
||||
foreach (['company.view','issuer_documents.view'] as $right) { app(\App\Modules\Identity\Services\Permissions::class)->require($user, $scope->tenant_id, $scope->id, $right); }
|
||||
if ($db::table('permission_grants')->insertOrIgnore(['user_id' => $user, 'tenant_id' => $scope->tenant_id, 'company_id' => $scope->id, 'unit_id' => null, 'permission' => $permission, 'created_at' => now(), 'updated_at' => now()])) {
|
||||
\App\Modules\Identity\Services\Audit::record('document_preview.access.initial_grant', null, $scope->tenant_id, $scope->id, subject: $user, details: ['permission' => $permission, 'reason' => 'Configuração da apresentação especificamente confirmada pelo utilizador.']);
|
||||
}
|
||||
});
|
||||
echo "Specifically confirmed company preview-configuration grant applied; no financial powers.\n";
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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();
|
||||
$fixture = 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($fixture['manager_id'], $fixture['tenant'], $fixture['company'], $fixture['unit']);
|
||||
$preparation = app(\App\Modules\Services\DocumentPreparation::class);
|
||||
$issuer = $preparation->show('issuer', $fixture['company'], $fixture['manager_id']);
|
||||
$client = $preparation->show('client', $fixture['document_client_id'], $fixture['manager_id']);
|
||||
$input = ['actor' => $fixture['manager_id'], 'tenant' => $fixture['tenant'], 'company' => $fixture['company'], 'unit' => $fixture['unit'], 'draft' => $fixture['document_draft_id'], 'issuer' => $issuer['data']['nuit'], 'client' => $client['data']['nuit']];
|
||||
$worker = new Process([PHP_BINARY, 'scripts/preview-coherence-worker.php', '--env=testing'], base_path(), null, json_encode($input));
|
||||
$worker->start();
|
||||
if (! $worker->waitUntil(fn ($type, $buffer) => str_contains($buffer, 'SNAPSHOT_STARTED'))) { throw new RuntimeException('Could not establish concurrent snapshot.'); }
|
||||
DB::transaction(function () use ($preparation, $fixture, $issuer, $client): void {
|
||||
$preparation->save('issuer', $fixture['company'], $fixture['manager_id'], ['version' => (int) $issuer['version']] + array_replace($issuer['data'], ['nuit' => 'EMITENTE-SINTETICO-REVISTO']));
|
||||
$preparation->save('client', $fixture['document_client_id'], $fixture['manager_id'], ['version' => (int) $client['version']] + array_replace($client['data'], ['nuit' => 'CLIENTE-SINTETICO-REVISTO']));
|
||||
});
|
||||
$worker->wait(); if (! $worker->isSuccessful()) { throw new RuntimeException('Synthetic snapshot process failed.'); }
|
||||
$lines = array_values(array_filter(explode("\n", trim($worker->getOutput())), fn ($line) => str_starts_with($line, '{')));
|
||||
$result = json_decode(end($lines), true, 32, JSON_THROW_ON_ERROR);
|
||||
if (in_array(false, $result, true)) { throw new RuntimeException('Mixed snapshot detected.'); }
|
||||
$next = app(\App\Modules\Services\DocumentPreview::class)->read($fixture['document_draft_id'], $fixture['manager_id']);
|
||||
if ($next['issuer']['data']['nuit'] !== 'EMITENTE-SINTETICO-REVISTO' || $next['client']['data']['nuit'] !== 'CLIENTE-SINTETICO-REVISTO') { throw new RuntimeException('Explicit refresh did not observe committed changes.'); }
|
||||
file_put_contents(base_path('docs/proofs/preview-coherence.json'), json_encode(['database' => 'sgf_tempo_test', 'parallel_php_processes' => true, 'repeatable_read_verified' => true, 'old_consultation' => 'both original versions', 'explicit_new_consultation' => 'both committed new versions', 'mixed_versions' => false, 'instruction_values' => 'omitted'], JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "Concurrent preview coherence passed: old snapshot remained coherent; new consultation saw both committed versions.\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\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
$input = json_decode(stream_get_contents(STDIN), true, 32, JSON_THROW_ON_ERROR);
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($input['actor'], $input['tenant'], $input['company'], $input['unit']);
|
||||
$paused = false; $isolation = null;
|
||||
\Illuminate\Support\Facades\DB::listen(function ($query) use (&$paused, &$isolation): void {
|
||||
if (! $paused && str_contains($query->sql, 'from "a_billing_drafts"')) {
|
||||
$paused = true;
|
||||
$isolation = \Illuminate\Support\Facades\DB::selectOne('SHOW transaction_isolation')->transaction_isolation;
|
||||
echo "SNAPSHOT_STARTED\n"; flush(); usleep(2000000);
|
||||
}
|
||||
});
|
||||
$result = app(\App\Modules\Services\DocumentPreview::class)->read($input['draft'], $input['actor']);
|
||||
echo json_encode(['repeatable_read' => $isolation === 'repeatable read', 'original_issuer' => $result['issuer']['data']['nuit'] === $input['issuer'], 'original_client' => $result['client']['data']['nuit'] === $input['client']], JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
@@ -0,0 +1,38 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const fixture = JSON.parse(await fs.readFile('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json','utf8'));
|
||||
const base = 'http://127.0.0.1:8001';
|
||||
function client() {
|
||||
const cookies = new Map(); let csrf = '', context = '';
|
||||
return {
|
||||
async request(path, method = 'GET', body, token = context) {
|
||||
const response = await fetch(base + path, { method, headers: { Accept:'application/json','Content-Type':'application/json',Cookie:[...cookies].map(([k,v]) => `${k}=${v}`).join('; '),'X-CSRF-TOKEN':csrf,'X-SGF-Context':token,'Idempotency-Key':randomUUID() },...(body === undefined ? {} : {body:JSON.stringify(body)}) });
|
||||
for (const cookie of response.headers.getSetCookie()) { const pair=cookie.split(';',1)[0], split=pair.indexOf('='); cookies.set(pair.slice(0,split),pair.slice(split+1)); }
|
||||
const data=await response.json().catch(() => ({})); if(data.csrf)csrf=data.csrf;if(data.context?.token)context=data.context.token;
|
||||
return {status:response.status,data,cache:response.headers.get('cache-control')};
|
||||
},
|
||||
async login(email) {await this.request('/api/session');assert.equal((await this.request('/api/login','POST',{email,password:fixture.password})).status,200);return this.select();},
|
||||
async select(unit=fixture.unit) {const response=await this.request('/api/context','POST',{tenant_id:fixture.tenant,company_id:fixture.company,unit_id:unit});assert.equal(response.status,200);return response.data.context.token;}
|
||||
};
|
||||
}
|
||||
const manager=client(),reader=client();await manager.login('gestor@example.test');await reader.login('consulta@example.test');
|
||||
const path=`/api/document-preview/draft/${fixture.document_draft_id}`;
|
||||
const first=await manager.request(path);assert.equal(first.status,200);assert.ok(first.cache.includes('no-store'));assert.equal(first.data.notice,'RASCUNHO — SEM VALIDADE FISCAL');assert.equal(first.data.base_amount,'0.0000000125');assert.equal(first.data.data.currency,null);assert.ok(first.data.pending.tax_rule_pending);
|
||||
const model={version:0,show_logo:false,show_contacts:true,show_observations:true,show_payment_instructions:false,contact_text:'Contactos sintéticos da empresa',observations:'Observações iniciais da demonstração'};
|
||||
assert.equal((await manager.request('/api/document-preview/model','PATCH',model)).status,200);
|
||||
const revision=await manager.request(path+'/revision');assert.equal(revision.status,200);assert.notEqual(revision.data.revision,first.data.revision);assert.equal(Object.keys(revision.data).join(','),'revision');
|
||||
const refreshed=await manager.request(path);assert.equal(refreshed.data.observations,model.observations);assert.equal(refreshed.data.payments.length,0);assert.equal(refreshed.data.notice,first.data.notice);assert.ok(refreshed.data.pending.tax_rule_pending);
|
||||
assert.equal((await reader.request(path)).status,200);assert.equal((await reader.request('/api/document-preview/model','PATCH',{...model,version:1})).status,403);
|
||||
assert.equal((await manager.request('/api/document-preview/model','PATCH',{...model,version:1,show_notice:false})).status,422);
|
||||
assert.equal((await manager.request('/api/document-preview/model','PATCH',model)).status,409);
|
||||
const oldContext=await manager.select();await manager.select(null);assert.equal((await manager.request('/api/document-preview/model','PATCH',{...model,version:1},oldContext)).status,409);await manager.select();
|
||||
const list=await manager.request('/api/a/billing_drafts');const confirmed=list.data.records.find(record=>record.internal_confirmation);
|
||||
assert.ok(confirmed);const historyPath=`/api/document-preview/history/${confirmed.id}`;
|
||||
const history=await manager.request(historyPath);assert.equal(history.status,200);assert.equal(history.data.contacts,null);assert.equal(history.data.issuer.name,null);
|
||||
assert.equal((await manager.request('/api/document-preview/model','PATCH',{...model,version:1,observations:'Observações para a prova no navegador'})).status,200);
|
||||
const historyAfter=await manager.request(historyPath);assert.equal(historyAfter.data.revision,history.data.revision);assert.equal(historyAfter.data.contacts,null);
|
||||
await manager.request('/api/logout','POST');await reader.request('/api/logout','POST');
|
||||
await fs.writeFile('docs/proofs/preview-http-results.json',JSON.stringify({database:'sgf_tempo_test',real_synthetic_login:true,results:['Read-only composition reuses exact values and unknown currency; no-store headers.', 'Revision changes after configuration; explicit new read loads new values.', 'Reader consults but cannot configure (403); hidden mandatory content rejected (422).', 'Old configuration version and context rejected (409).', 'Historical consultation is independent of current configuration and does not reconstruct missing issuer name.'],instruction_values:'omitted'},null,2));
|
||||
console.log('Isolated preview HTTP proof passed; instruction values omitted.');
|
||||
@@ -0,0 +1,9 @@
|
||||
<?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; $counts = [];
|
||||
foreach (['document_preview_models','document_preview_model_history','doc_records','doc_history','b_internal_confirmations'] as $table) { $counts[$table] = $db::table($table)->count(); }
|
||||
echo json_encode(['database' => 'sgf_tempo_dev', 'counts' => $counts, 'configuration_grants' => $db::table('permission_grants')->where('permission', \App\Modules\Services\DocumentPreview::CONFIGURE)->get(['user_id','company_id','unit_id','permission'])], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL;
|
||||
Reference in New Issue
Block a user