Accept isolated workshop billing draft trial with documented manual evidence

This commit is contained in:
2026-10-28 19:32:10 +02:00
parent 090b7da336
commit 3932b94a16
30 changed files with 633 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
<?php
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
use App\Modules\Workshop\WorkshopBilling;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\Process\Process;
WorkshopBilling::guard();$base=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);$f=$base+$base['workshop_billing'];$scope=['tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit'],'order'=>$f['order']];$key=fn()=>(string)Str::uuid();$checks=[];
$pair=function($one,$two)use($scope){$a=new Process([PHP_BINARY,'scripts/workshop-billing-worker.php','--env=testing'],base_path(),null,json_encode($one+$scope+['pause'=>true]));$b=new Process([PHP_BINARY,'scripts/workshop-billing-worker.php','--env=testing'],base_path(),null,json_encode($two+$scope));$a->start();if(!$a->waitUntil(fn($type,$v)=>str_contains($v,'LOCK_HELD')))throw new RuntimeException('Concurrent lock not observed.');$b->start();$a->wait();$b->wait();$out=[];foreach([$a,$b] as $p){if(!$p->isSuccessful())throw new RuntimeException('Worker failed.');$lines=array_values(array_filter(explode("\n",trim($p->getOutput())),fn($s)=>str_starts_with($s,'{')));$out[]=json_decode(end($lines),true,32,JSON_THROW_ON_ERROR);}return $out;};
$check=function($out,$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 result.');return $out[0]['result']['id'];};
$p=['actor'=>$f['manager_id'],'method'=>'prepare','key'=>$key(),'input'=>\Tests\Support\WorkshopBillingFixture::proposal($f)];$id=$check($pair($p,$p),true);$checks['proposal_replay_one_version']=true;
$p['key']=$key();$p['input']['version']=2;$p['input']['previous_id']=$id;$other=$p;$other['key']=$key();$other['input']['lines'][0]['price']='210';$id=$check($pair($p,$other),false);$checks['competing_proposals_one_successor']=true;
$r=['actor'=>$f['reader_id'],'method'=>'review','key'=>$key(),'input'=>['id'=>$id,'version'=>2,'decision'=>'approved','reason'=>'Revisão comercial concorrente distinta.']];$check($pair($r,$r),true);$checks['review_replay_one_decision']=true;
$g=['actor'=>$f['manager_id'],'method'=>'generate','key'=>$key(),'input'=>['id'=>$id,'version'=>2]];$gid=$check($pair($g,$g),true);$checks['generation_replay_one_draft']=true;
$g['key']=$key();$out=$pair($g,$g);if($out[0]['status']!==409||$out[1]['status']!==409)throw new RuntimeException('Duplicate generation not blocked.');$checks['new_keys_cannot_duplicate_generation']=true;
if(DB::table('wb_generations')->where('order_id',$f['order'])->count()!==1||DB::table('wb_claims')->where('order_id',$f['order'])->count()!==2)throw new RuntimeException('Generation or origin claims duplicated.');
file_put_contents(base_path('docs/proofs/workshop-billing-concurrency.json'),json_encode(['policy'=>WorkshopBilling::POLICY,'database'=>'sgf_tempo_test','overlapping_processes_per_case'=>2,'checks'=>$checks,'one_generation_two_origin_claims'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Five overlapping commercial conversion cases passed; one generation and two origin claims.\n";
+13
View File
@@ -0,0 +1,13 @@
<?php
// Read-only check. Never prints credentials, attempts login, or resets accounts.
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
\App\Modules\Workshop\WorkshopBilling::guard();
$identity=\Illuminate\Support\Facades\DB::selectOne('SELECT current_database() AS db,current_user AS role');
if($identity->db!=='sgf_tempo_test'||$identity->role!=='sgf_test_runner')throw new RuntimeException('Wrong database; no accounts changed.');
$path='C:/laragon/data/sgf-tempo/secrets/workshop-billing-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('/Temporary synthetic password for these two accounts: ([^\r\n]+)/',$content,$match))throw new RuntimeException('Unexpected entry format; no accounts changed.');
$checks=[];foreach(['gestor@example.test','consulta@example.test'] as $email){$u=\App\Models\User::where('email',$email)->first();$checks[]=['account'=>$email,'exists'=>(bool)$u,'entry_matches'=>$u&&\Illuminate\Support\Facades\Hash::check($match[1],$u->password)];}
unset($content,$match);
echo json_encode(['path'=>realpath($path),'database'=>$identity->db,'accounts'=>$checks,'credentials_changed'=>false],JSON_THROW_ON_ERROR).PHP_EOL;
+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\WorkshopBilling::guard();$path='C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';$base=json_decode(file_get_contents($path),true,32,JSON_THROW_ON_ERROR);
if(\Illuminate\Support\Facades\DB::table('wo_orders')->exists())throw new RuntimeException('Empty workshop proof required; nothing deleted.');
$f=\Tests\Support\WorkshopBillingFixture::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','task','auth','time','material','day','service','client']));
file_put_contents($path,json_encode($base+['workshop_billing'=>$safe],JSON_THROW_ON_ERROR));
file_put_contents('C:/laragon/data/sgf-tempo/secrets/workshop-billing-manual-entry.txt',"Synthetic SGF proof only - http://127.0.0.1:8002\r\nPreparer: gestor@example.test\r\nReviewer: consulta@example.test\r\nTemporary synthetic password for these two accounts: ".$base['password']."\r\nDo not paste this password into chat, screenshots or reports.\r\n");
// Existing protected manifest is kept outside Git; no credentials printed.
echo "Synthetic workshop sources verified by a technical identity, commercial roles separate. No proposal, review or generation prefilled.\n";
+18
View File
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict';
import {workshopBillingProposal} from '../resources/js/workshopBillingProposal.js';
const order={client_id:16,entries:[{id:6,version:1},{id:4,version:1}],proposals:[]},services=[{id:9,version:1}];
const form=new FormData();
assert.throws(()=>workshopBillingProposal(order,services,form),/Incluir origem integral/);
form.set('select-6','on');
assert.throws(()=>workshopBillingProposal(order,services,form),/Origem #6: seleccione o serviço/);
form.set('service-6','9');
assert.throws(()=>workshopBillingProposal(order,services,form),/Origem #6: preencha o preço/);
form.set('price-6','0');
assert.throws(()=>workshopBillingProposal(order,services,form),/fonte comercial/);
form.set('source-6','WORKSHOP-SALES-SYNTHETIC-v1');
assert.throws(()=>workshopBillingProposal(order,services,form),/fundamento/);
form.set('evidence','Proposta sintética');
const zero=workshopBillingProposal(order,services,form);assert.equal(zero.lines[0].price,'0');assert.equal(zero.lines.length,1);
form.set('select-4','on');form.set('service-4','9');form.set('price-4','200');form.set('source-4','WORKSHOP-SALES-SYNTHETIC-v1');form.set('price-6','75');
const proposal=workshopBillingProposal(order,services,form);assert.deepEqual(proposal.lines.map(l=>l.price),['75','200']);assert.equal(proposal.previous_id,null);assert.equal(proposal.version,1);
console.log('Workshop form checks passed: missing selection/service/price/source/evidence; explicit zero; exact selected values. No database or browser used.');
@@ -0,0 +1,33 @@
<?php
// Reconcile the reported manual generation without altering records or credentials.
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 App\Modules\Workshop\WorkshopBilling;
use App\Modules\Services\DraftValues;
function requireWorkshopManual(bool $ok,string $reason): void {if(!$ok)throw new RuntimeException($reason);}
WorkshopBilling::guard();
$identity=DB::selectOne('SELECT current_database() AS db,current_user AS role');
requireWorkshopManual($identity->db==='sgf_tempo_test'&&$identity->role==='sgf_test_runner','Unexpected database.');
$generations=DB::table('wb_generations')->get();requireWorkshopManual($generations->count()===1,'Exactly one synthetic generation required.');$g=$generations->first();
$p=DB::table('wb_proposals')->find($g->proposal_id);$review=DB::table('wb_decisions')->where(['proposal_id'=>$p->id,'version'=>$p->version,'decision'=>'approved'])->first();
requireWorkshopManual($review&&$review->actor_id!==$p->actor_id&&$g->actor_id===$p->actor_id,'Separate commercial identities required.');
$snapshot=json_decode($p->snapshot,true,64,JSON_THROW_ON_ERROR);$service=DB::table('a_service_drafts')->find($g->service_draft_id);$billing=DB::table('a_billing_drafts')->find($g->billing_draft_id);
requireWorkshopManual($service&&$billing&&$billing->source_id===$service->id&&$billing->state==='draft'&&$service->state==='draft','Draft destination chain differs.');
$sd=json_decode($service->data,true);$bd=json_decode($billing->data,true);
requireWorkshopManual($sd['currency']==='MZN'&&$bd['currency']==='MZN'&&DraftValues::base($sd)==='500'&&DraftValues::base($bd)==='500'&&$snapshot['base_exact']==='500','Commercial amounts differ.');
requireWorkshopManual(count($snapshot['lines'])===2&&count($bd['lines'])===2,'Two selected lines required.');
$claims=DB::table('wb_claims')->where('generation_id',$g->id)->get();requireWorkshopManual($claims->count()===2,'Two unique source claims required.');
foreach($snapshot['lines'] as $i=>$line){
$entry=DB::table('wo_entries')->where(['id'=>$line['entry_id'],'version'=>$line['entry_version'],'order_id'=>$g->order_id,'origin'=>$line['origin']])->first();
requireWorkshopManual($entry&&!$entry->outside_authority,'Source differs.');
requireWorkshopManual(DB::table('wo_decisions')->where(['entry_id'=>$entry->id,'entry_version'=>$entry->version,'decision'=>'verified'])->exists(),'Technical verification missing.');
requireWorkshopManual($claims->contains(fn($c)=>$c->entry_id===$entry->id&&$c->entry_version===$entry->version&&$c->origin===$entry->origin),'Source claim differs.');
foreach(['service_id','quantity','unit_price','description'] as $field)requireWorkshopManual($bd['lines'][$i][$field]===$line[$field],'Generated line differs: '.$field);
requireWorkshopManual($line['price_source']==='WORKSHOP-SALES-SYNTHETIC-v1','Unexpected price reference.');
requireWorkshopManual($entry->kind==='time'?($line['quantity']==='1.75'&&$line['unit_price']==='200'):($line['quantity']==='2'&&$line['unit_price']==='75'),'Quantity/price differs.');
}
requireWorkshopManual(!DB::table('b_internal_confirmations')->where('billing_draft_id',$billing->id)->exists(),'Unexpected internal confirmation.');
requireWorkshopManual($bd['tax_status']==='pending'&&$bd['accounting_status']==='not_processed','Unexpected fiscal/accounting state.');
$proof=['policy'=>WorkshopBilling::POLICY,'date'=>'2026-10-28','database'=>'sgf_tempo_test','method'=>'user_reported_manual_browser_and_read_only_reconciliation','order_id'=>$g->order_id,'proposal_id'=>$p->id,'proposal_version'=>$p->version,'service_draft_id'=>$service->id,'billing_draft_id'=>$billing->id,'commercial_base_exact'=>'500','currency'=>'MZN','source_claims'=>2,'distinct_commercial_review'=>true,'generated_lines_match_reviewed_proposal'=>true,'internal_confirmation_created'=>false,'fiscal_status'=>'pending','accounting_status'=>'not_processed','screenshots_received'=>false,'automated_browser_recovered'=>false,'records_changed'=>false,'user_acceptance'=>false,'closing_checkpoint_created'=>false];
file_put_contents(base_path('docs/proofs/workshop-billing-manual-results.json'),json_encode($proof,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR).PHP_EOL);
echo 'Manual generation reconciled: proposal #'.$p->id.' v'.$p->version.', service #'.$service->id.', billing draft #'.$billing->id.'; 500 MZN, two source claims, separate review, no internal confirmation. No records changed.'.PHP_EOL;
@@ -0,0 +1,10 @@
<?php
// Read-only; no apply or real activation 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.');
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.');
$proposals=[];foreach([1=>['workshop_billing.view','workshop_billing.prepare','workshop_billing.generate'],2=>['workshop_billing.view','workshop_billing.review','workshop.view']] as $user=>$rights){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($rights as $permission){$g=['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>$permission];$proposals[]=$g+['already_granted'=>DB::table('permission_grants')->where($g)->exists()];}}
$prerequisites=[];foreach([1,2] as $user){foreach(['clients.view','services.view'] as $permission)$prerequisites[]=['user_id'=>$user,'permission'=>$permission,'unit_id'=>null,'present'=>DB::table('permission_grants')->where(['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'permission'=>$permission])->whereNull('unit_id')->exists()];}foreach(['workshop.view','service_drafts.view','service_drafts.create','billing_drafts.view','billing_drafts.create'] as $permission)$prerequisites[]=['user_id'=>1,'permission'=>$permission,'unit_id'=>1,'present'=>DB::table('permission_grants')->where(['user_id'=>1,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>$permission])->exists()];
try{\App\Modules\Workshop\WorkshopBilling::guard();throw new RuntimeException('Real conversion 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_120000_applied'=>DB::table('migrations')->where('migration','2026_10_28_120000_create_workshop_billing_drafts')->exists(),'proposed_grants'=>$proposals,'existing_prerequisites'=>$prerequisites,'real_conversion_blocked'=>true,'applied'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR).PHP_EOL;
+6
View File
@@ -0,0 +1,6 @@
<?php
// Read-only synthetic status. No credentials, form contents or account changes.
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
\App\Modules\Workshop\WorkshopBilling::guard();
$db=\Illuminate\Support\Facades\DB::class;
echo json_encode(['database'=>$db::selectOne('SELECT current_database() AS db')->db,'proposals'=>$db::table('wb_proposals')->orderBy('id')->get(['id','order_id','version','previous_id'])->all(),'reviews'=>$db::table('wb_decisions')->get(['proposal_id','version','decision'])->all(),'generations'=>$db::table('wb_generations')->get(['id','order_id','proposal_id','billing_draft_id'])->all(),'read_only'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR).PHP_EOL;
+6
View File
@@ -0,0 +1,6 @@
<?php
require __DIR__.'/../vendor/autoload.php';$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
\App\Modules\Workshop\WorkshopBilling::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\Workshop\WorkshopBilling::class)->{$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;}