Files
SGF/scripts/sgf-workshop-schedule-proof.php
2026-11-11 09:00:17 +02:00

28 lines
7.1 KiB
PHP

<?php
use Illuminate\Support\Facades\Auth;use Illuminate\Support\Str;use App\Shared\Context\CompanyContext;use App\Modules\Workshop\WorkshopSchedule;
require __DIR__.'/sgf-ledger-proof-bootstrap.php';config(['workshop_schedule.enabled'=>true]);$direct=json_decode(file_get_contents($s['root'].'/single-direct-cost.json'),true);
$select=function($role,$module='workshop')use($profiles,$f,$app){Auth::forgetGuards();Auth::shouldUse('web');$u=App\Models\User::findOrFail($profiles[$role]['user_id']);Auth::guard('web')->setUser($u);$r=Illuminate\Http\Request::create('/api/workshop-schedule','GET');$r->attributes->set('sgf_module',$module);$r->setUserResolver(fn()=>$u);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>$module,'token'=>'schedule-'.$role]);$r->setLaravelSession($session);$app->instance('request',$r);app(CompanyContext::class)->selectFor($u->id,1,2,2);return $u->id;};
$reject=function(Closure $fn,int $status,string $name)use($ok){$bad=false;try{$fn();}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$bad=$e->getStatusCode()===$status;}catch(Illuminate\Validation\ValidationException){$bad=$status===422;}$ok($bad,$name);};
set_exception_handler(function($e){echo json_encode(['passed'=>false,'type'=>get_class($e),'message'=>$e->getMessage(),'frames'=>array_map(fn($f)=>[basename($f['file']??''),$f['line']??0],array_slice($e->getTrace(),0,10))]).PHP_EOL;exit(1);});
$w=app(WorkshopSchedule::class);$host->beginTransaction();$nativeBefore=$native->table('stock_movements')->orderBy('id')->get()->toJson();$woBefore=$host->table('wo_entries')->orderBy('id')->get()->toJson();
try{
$manager=$select('workshop_manage');$tech=$profiles['workshop_record']['user_id'];$input=['action'=>'book','booking_key'=>(string)Str::uuid(),'version'=>0,'order_id'=>$direct['order'],'technician_id'=>$tech,'starts_at'=>'2026-11-12T09:00:00+02:00','ends_at'=>'2026-11-12T10:00:00+02:00','reason'=>'Agendamento sintético fundamentado da ordem já atribuída.'];
$key=(string)Str::uuid();$p=$w->preview($manager,$input,$key);$ok($p['impact']['financial_effect']===false&&$p['impact']['stock_effect']===false,'preview declares no physical or financial effect');$ok($host->table('wo_schedule_events')->count()===0,'preview does not reserve time');
$id=$select('workshop_record');$reject(fn()=>$w->preview($id,$input,(string)Str::uuid()),403,'technician cannot borrow scheduling assignment authority');$manager=$select('workshop_manage');
$booked=$w->confirm($manager,$p['preview']);$again=$w->confirm($manager,$p['preview']);$ok($again['replayed']&&$again['id']===$booked['id'],'uncertain confirmation recovers one booking event');
$reject(fn()=>$w->preview($manager,array_replace($input,['booking_key'=>(string)Str::uuid()]),$key),409,'used operation key refuses changed booking');
$reject(fn()=>$w->preview($manager,array_replace($input,['booking_key'=>(string)Str::uuid(),'starts_at'=>'2026-11-12T09:30:00+02:00']), (string)Str::uuid()),409,'overlapping technician/equipment slot refused');
$adjacent=array_replace($input,['booking_key'=>(string)Str::uuid(),'starts_at'=>'2026-11-12T10:00:00+02:00','ends_at'=>'2026-11-12T11:00:00+02:00']);$adj=$w->preview($manager,$adjacent,(string)Str::uuid());$ok(isset($adj['preview']),'adjacent non-overlapping slot remains available');
$stale=$w->preview($manager,array_replace($adjacent,['booking_key'=>(string)Str::uuid()]),(string)Str::uuid());$w->confirm($manager,$adj['preview']);$reject(fn()=>$w->confirm($manager,$stale['preview']),409,'competing slot commit invalidates earlier availability conference');
$reschedule=array_replace($input,['action'=>'reschedule','version'=>1,'starts_at'=>'2026-11-12T12:00:00+02:00','ends_at'=>'2026-11-12T13:00:00+02:00']);$move=$w->preview($manager,$reschedule,(string)Str::uuid());$moved=$w->confirm($manager,$move['preview']);$ok($moved['version']===2,'reschedule appends version2 rather than replacing history');$reject(fn()=>$w->preview($manager,$reschedule,(string)Str::uuid()),409,'old booking version refuses later edit');
$cancel=array_replace($reschedule,['action'=>'cancel','version'=>2]);$cancelled=$w->preview($manager,$cancel,(string)Str::uuid());$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>$manager,'permission'=>'workshop.assign'])->delete();$reject(fn()=>$w->confirm($manager,$cancelled['preview']),403,'revoked assignment authority refuses prepared cancellation');}finally{$host->rollBack();}
$host->unprepared("CREATE FUNCTION sgf_schedule_test_failure() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''SCHEDULE_TEST_FAILURE''; END'");$host->unprepared('REVOKE ALL ON FUNCTION sgf_schedule_test_failure() FROM PUBLIC');$host->unprepared("CREATE TRIGGER sgf_schedule_test_failure BEFORE INSERT ON identity_audit FOR EACH ROW EXECUTE FUNCTION sgf_schedule_test_failure()");$count=$host->table('wo_schedule_events')->count();$failed=false;try{$w->confirm($manager,$cancelled['preview']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$host->table('wo_schedule_events')->count()===$count,'audit failure rolls back the booking version atomically');$host->unprepared('DROP TRIGGER sgf_schedule_test_failure ON identity_audit');$host->unprepared('DROP FUNCTION sgf_schedule_test_failure()');
$done=$w->confirm($manager,$cancelled['preview']);$ok($done['version']===3&&$done['state']==='cancelled','same cancellation recovers after injected failure');
$history=$w->history($manager,$input['booking_key']);$ok($history['events']->total()===3,'history retains original, reschedule and cancellation with reasons and authors');
$list=$w->listing($manager,['from'=>'2026-11-12','to'=>'2026-11-12','per_page'=>25]);$ok($list['bookings']->total()===2,'calendar projects latest versions only');$list=$w->listing($manager,['from'=>'2026-11-12','to'=>'2026-11-12','per_page'=>100,'state'=>'cancelled']);$ok($list['bookings']->total()===1,'server state filter and page100 preserve cancelled booking');
$reject(fn()=>$w->preview($manager,array_replace($input,['booking_key'=>(string)Str::uuid(),'order_id'=>999999]),(string)Str::uuid()),403,'foreign or unknown order cannot be scheduled');$reject(fn()=>$w->preview($manager,array_replace($input,['booking_key'=>(string)Str::uuid(),'starts_at'=>'2026-02-31T09:00:00+02:00']), (string)Str::uuid()),422,'invalid normalised calendar date refused');
$id=$select('workshop_record','finance');$reject(fn()=>$w->options($id,[]),403,'foreign module context cannot expose schedule options');
$ok($woBefore===$host->table('wo_entries')->orderBy('id')->get()->toJson()&&$nativeBefore===$native->table('stock_movements')->orderBy('id')->get()->toJson(),'work orders, assignments and native physical book remain unchanged');
}finally{while($host->transactionLevel()>0)$host->rollBack();}
file_put_contents($work.'/workshop-schedule-result.json',json_encode(['passed'=>true,'checks'=>$checks,'test_changes_rolled_back'=>true,'new_rights'=>0,'external_messages'=>0,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/workshop-schedule-result.json']).PHP_EOL;