21 lines
8.3 KiB
PHP
21 lines
8.3 KiB
PHP
<?php
|
|
use Illuminate\Support\Facades\Auth;use Illuminate\Support\Str;use App\Shared\Context\CompanyContext;use App\Modules\Services\{CommercialQuotes,ServiceWorkflow};
|
|
require __DIR__.'/sgf-ledger-proof-bootstrap.php';config(['commercial_quotes.enabled'=>true]);$commercial=json_decode(file_get_contents($s['root'].'/single-commercial.json'),true);$workshop=json_decode(file_get_contents($s['root'].'/single-workshop-stock-input.json'),true);
|
|
$select=function($role)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/commercial-quotes','GET');$r->attributes->set('sgf_module','commercial');$r->setUserResolver(fn()=>$u);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>'commercial','token'=>'quote-'.$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 $label)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,$label);};$key=fn()=>(string)Str::uuid();$w=app(CommercialQuotes::class);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);});
|
|
$host->beginTransaction();$beforeClients=$host->table('a_clients')->orderBy('id')->get()->toJson();$beforeInvoices=$host->table('si_invoices')->orderBy('id')->get()->toJson();
|
|
try{
|
|
$actor=$select('commercial_prepare');$data=['action'=>'prepare','quote_key'=>$key(),'version'=>0,'reason'=>'Proposta comercial sintética com preço explicitamente seleccionado.','data'=>['client_id'=>$commercial['client'],'currency'=>'MZN','valid_until'=>'2026-12-31','description'=>'Proposta QA de base1000, impostos conferidos separadamente.','lines'=>[['service_id'=>$commercial['service'],'quantity'=>'1','unit_price'=>'1000']]]];$command=$key();$p=$w->preview($actor,$data,$command);$ok($p['impact']['base']==='1000.00'&&!$p['impact']['invoice_issued'],'quote preview shows exact proposed base without pretending tax or invoice');$ok($host->table('cq_versions')->count()===0,'quote preview writes nothing persistently');$created=$w->confirm($actor,$p['preview']);$replay=$w->confirm($actor,$p['preview']);$ok($created['id']===$replay['id']&&$replay['replayed'],'prepare replay retains one quote version');
|
|
$review=['action'=>'review','quote_key'=>$data['quote_key'],'version'=>1,'reason'=>'Conferência comercial independente das linhas e validade.','data'=>['decision'=>'approved']];$reject(fn()=>$w->preview($actor,$review,$key()),403,'preparer cannot use reviewer authority');$reviewer=$select('commercial_review');$p=$w->preview($reviewer,$review,$key());$approved=$w->confirm($reviewer,$p['preview']);$ok($approved['state']==='approved','distinct reviewer approves exact quote version');
|
|
$actor=$select('commercial_prepare');$convert=['action'=>'convert','quote_key'=>$data['quote_key'],'version'=>2,'reason'=>'Não converter antes de decisão documentada do cliente.','data'=>[]];$reject(fn()=>$w->preview($actor,$convert,$key()),409,'review alone does not authorise conversion');
|
|
$customer=['action'=>'customer','quote_key'=>$data['quote_key'],'version'=>2,'reason'=>'Decisão fictícia do cliente registada com prova identificada.','data'=>['decision'=>'accepted','representative'=>'Representante fictício QA','capacity'=>'Cliente de teste','evidence'=>'QA-ACEITACAO-ORCAMENTO-1000','decided_on'=>'2026-11-10']];$p=$w->preview($actor,$customer,$key());$accepted=$w->confirm($actor,$p['preview']);$ok($accepted['state']==='customer_accepted','client acceptance requires representative and evidence');
|
|
$convert['version']=3;$priorNow=Carbon\Carbon::getTestNow();Carbon\Carbon::setTestNow('2027-01-01 10:00:00');try{$reject(fn()=>$w->preview($actor,$convert,$key()),409,'expired customer acceptance cannot be converted');}finally{Carbon\Carbon::setTestNow($priorNow);}$p=$w->preview($actor,$convert,$key());$n=$host->table('a_billing_drafts')->count();$host->beginTransaction();try{$client=$host->table('a_clients')->where('id',$commercial['client'])->first();app(ServiceWorkflow::class)->save('clients',$client->id,$actor,['version'=>$client->version,'name'=>'Cliente QA alterado apenas no ensaio de versão'],$key());$reject(fn()=>$w->confirm($actor,$p['preview']),409,'new client version invalidates old quote conversion conference');}finally{$host->rollBack();}$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>$actor,'permission'=>'commercial_quotes.convert'])->delete();$reject(fn()=>$w->confirm($actor,$p['preview']),403,'revoked conversion right refuses prepared decision');}finally{$host->rollBack();}
|
|
$host->unprepared("CREATE FUNCTION cq_test_fail() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''CQ_TEST_FAILURE''; END'");$host->unprepared('REVOKE ALL ON FUNCTION cq_test_fail() FROM PUBLIC');$host->unprepared('CREATE TRIGGER cq_test_fail BEFORE INSERT ON cq_commands FOR EACH ROW EXECUTE FUNCTION cq_test_fail()');$failed=false;try{$w->confirm($actor,$p['preview']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$host->table('a_billing_drafts')->count()===$n,'failure after draft writer rolls back drafts, conversion and quote version');$host->unprepared('DROP TRIGGER cq_test_fail ON cq_commands');$host->unprepared('DROP FUNCTION cq_test_fail()');
|
|
$converted=$w->confirm($actor,$p['preview']);$again=$w->confirm($actor,$p['preview']);$ok($converted['state']==='converted'&&$converted['drafts']['billing_draft_id']===$again['drafts']['billing_draft_id']&&$converted['drafts']['service_draft_id']===$again['drafts']['service_draft_id']&&$again['replayed'],'one explicit conversion recovers same two original-writer drafts');$billing=$host->table('a_billing_drafts')->where('id',$converted['drafts']['billing_draft_id'])->first();$ok($billing->client_id===$commercial['client']&&App\Modules\Services\DraftValues::base(json_decode($billing->data,true))==='1000','conversion preserves client, quantity and proposed price');
|
|
$reject(fn()=>app(ServiceWorkflow::class)->save('billing_drafts',$billing->id,$actor,['version'=>1,'description'=>'Não editar a proposta aceite por arrasto.'],$key()),409,'converted quote draft cannot bypass its accepted version through generic edit');$reject(fn()=>$w->preview($actor,array_replace($data,['version'=>4]),$key()),409,'converted quote cannot be silently revised or converted twice');
|
|
$finance=$select('finance_prepare');$q=array_replace($data,['quote_key'=>$key(),'data'=>array_replace($data['data'],['source_order_id'=>$workshop['order'],'description'=>'Orçamento previsto da reparação, sem autorizar trabalho.','lines'=>[['service_id'=>$commercial['service'],'quantity'=>'1','unit_price'=>'500']]])]);$p=$w->preview($finance,$q,$key());$createdW=$w->confirm($finance,$p['preview']);$ok($p['impact']['base']==='500.00'&&$p['impact']['source_reference']!==null,'workshop quote preserves explicit work-order reference and base500');
|
|
$read=$select('read_only');$list=$w->listing($read,['per_page'=>100]);$ok($list['quotes']->total()===2,'read-only quote listing is server paginated25/100');$reject(fn()=>$w->preview($read,$data,$key()),403,'quote reader cannot prepare');$history=$w->history($read,$data['quote_key']);$ok($history['events']->total()===4,'quote history preserves preparation, distinct review, client decision and conversion');
|
|
$ok($beforeClients===$host->table('a_clients')->orderBy('id')->get()->toJson()&&$beforeInvoices===$host->table('si_invoices')->orderBy('id')->get()->toJson(),'quotes do not change clients, old associations or issued invoices');
|
|
}finally{while($host->transactionLevel()>0)$host->rollBack();}
|
|
file_put_contents($work.'/commercial-quotes-result.json',json_encode(['passed'=>true,'checks'=>$checks,'proof_changes_rolled_back'=>true,'invoices_issued'=>0,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/commercial-quotes-result.json']).PHP_EOL;
|