46 lines
8.6 KiB
PHP
46 lines
8.6 KiB
PHP
<?php
|
|
namespace Tests\Feature;
|
|
use App\Modules\Workshop\WorkshopBilling;
|
|
use App\Modules\Services\ServiceWorkflow;
|
|
use App\Shared\Context\CompanyContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Tests\Support\{DirectCostFixture,IdentityFixture};
|
|
use Tests\TestCase;
|
|
final class WorkshopBillingTest extends TestCase {
|
|
use RefreshDatabase;
|
|
private array $f;private int $service;private string $token;
|
|
protected function setUp(): void {parent::setUp();$this->f=DirectCostFixture::create();foreach(['manager'=>['view','prepare','generate'],'reader'=>['view','review']] as $who=>$rights){foreach($rights as $r)IdentityFixture::grant($this->f[$who]->id,$this->f['tenant'],$this->f['company'],'workshop_billing.'.$r,$this->f['unit']);IdentityFixture::grant($this->f[$who]->id,$this->f['tenant'],$this->f['company'],'workshop.view',$this->f['unit']);}$this->service=app(ServiceWorkflow::class)->save('services',null,$this->f['manager']->id,['name'=>'Serviço comercial sintético'],(string)Str::uuid())['id'];$this->login();}
|
|
private function login(string $who='manager'): void {$this->postJson('/api/login',['email'=>$this->f[$who]->email,'password'=>$this->f['password']])->assertOk();$this->token=$this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['company'],'unit_id'=>$this->f['unit']])->assertOk()->json('context.token');}
|
|
private function send(string $action,array $in,?string $key=null){return $this->withHeaders(['X-SGF-Context'=>$this->token,'Idempotency-Key'=>$key??(string)Str::uuid()])->postJson('/api/workshop-billing/orders/'.$this->f['order'].'/'.$action,$in);}
|
|
private function input(): array {return ['version'=>1,'previous_id'=>null,'client_id'=>$this->f['client'],'currency'=>'MZN','evidence'=>'Tarifas sintéticas revistas, não custos internos.','lines'=>array_map(fn($id,$price)=>['entry_id'=>$id,'entry_version'=>1,'service_id'=>$this->service,'service_version'=>1,'price'=>$price,'price_source'=>'WORKSHOP-SALES-SYNTHETIC-v1'],[$this->f['time'],$this->f['material']],['200','75'])];}
|
|
private function approve(int $id): void {$this->login('reader');$this->send('reviews',['id'=>$id,'version'=>1,'decision'=>'approved','reason'=>'Versão comercial exacta conferida.'])->assertOk();$this->login();}
|
|
public function test_exact_500_generation_distinct_review_history_and_replay(): void {
|
|
$count=DB::table('wo_entries')->count();$d=$this->input();$key=(string)Str::uuid();$p=$this->send('proposals',$d,$key)->assertOk()->json('id');$this->send('proposals',$d,$key)->assertOk()->assertJsonPath('id',$p)->assertJsonPath('replayed',true);
|
|
$this->send('generations',['id'=>$p,'version'=>1])->assertStatus(409);$this->approve($p);$key=(string)Str::uuid();$g=$this->send('generations',['id'=>$p,'version'=>1],$key)->assertOk()->assertJsonPath('base_exact','500');$this->send('generations',['id'=>$p,'version'=>1],$key)->assertOk()->assertJsonPath('id',$g->json('id'))->assertJsonPath('replayed',true);
|
|
$bill=DB::table('a_billing_drafts')->find($g->json('billing_draft_id'));$data=json_decode($bill->data,true);$this->assertSame('MZN',$data['currency']);$this->assertSame('1.75',$data['lines'][0]['quantity']);$this->assertSame('200',$data['lines'][0]['unit_price']);$this->assertSame('2',$data['lines'][1]['quantity']);$this->assertSame('75',$data['lines'][1]['unit_price']);$this->assertSame(2,DB::table('wb_claims')->count());$this->assertSame($count,DB::table('wo_entries')->count());$this->assertSame(0,DB::table('b_internal_confirmations')->count());$this->assertSame('draft',$bill->state);$this->getJson('/api/workshop-billing')->assertOk()->assertJsonPath('orders.0.proposals.0.current',true);
|
|
$this->send('generations',['id'=>$p,'version'=>1])->assertStatus(409);try{DB::transaction(fn()=>DB::table('a_billing_drafts')->where('id',$bill->id)->update(['name'=>'Bypass','version'=>$bill->version+1]));$this->fail('Linked draft editable');}catch(\Illuminate\Database\QueryException $e){$this->assertStringContainsString('SGF_WB_LINKED_DRAFT',$e->getMessage());}
|
|
app(CompanyContext::class)->selectFor($this->f['manager']->id,$this->f['tenant'],$this->f['company'],$this->f['unit']);
|
|
try{app(ServiceWorkflow::class)->save('billing_drafts',null,$this->f['manager']->id,['source_id'=>$g->json('service_draft_id')],(string)Str::uuid());$this->fail('Second billing through ordinary workflow');}catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$this->assertSame(409,$e->getStatusCode());}
|
|
}
|
|
public function test_identity_permissions_revocation_and_unknown_price(): void {
|
|
$d=$this->input();$d['lines'][0]['price']='';$this->send('proposals',$d)->assertStatus(422);$d=$this->input();$d['lines'][]=$d['lines'][0];$this->send('proposals',$d)->assertStatus(409);
|
|
$p=$this->send('proposals',$this->input())->assertOk()->json('id');IdentityFixture::grant($this->f['manager']->id,$this->f['tenant'],$this->f['company'],'workshop_billing.review',$this->f['unit']);$this->send('reviews',['id'=>$p,'version'=>1,'decision'=>'approved','reason'=>'Auto review'])->assertForbidden();$this->login('reader');$this->send('generations',['id'=>$p,'version'=>1])->assertForbidden();$this->send('proposals',$this->input())->assertForbidden();DB::table('permission_grants')->where(['user_id'=>$this->f['reader']->id,'permission'=>'workshop_billing.review'])->delete();$this->send('reviews',['id'=>$p,'version'=>1,'decision'=>'approved','reason'=>'Revoked review'])->assertForbidden();
|
|
}
|
|
public function test_material_delivery_wrong_client_service_and_version_refused(): void {
|
|
$d=$this->input();$d['client_id']=999;$this->send('proposals',$d)->assertStatus(409);$d=$this->input();$d['lines'][0]['service_version']=2;$this->send('proposals',$d)->assertStatus(409);$d=$this->input();$delivered=DB::table('wo_entries')->where(['order_id'=>$this->f['order'],'kind'=>'material'])->whereRaw("data->>'event'='delivered'")->first();$d['lines'][1]['entry_id']=$delivered->id;$this->send('proposals',$d)->assertStatus(422);
|
|
}
|
|
public function test_rollback_outbox_preserves_no_orphan_drafts_and_recovery(): void {
|
|
$p=$this->send('proposals',$this->input())->assertOk()->json('id');$this->approve($p);$n=DB::table('a_billing_drafts')->count();$s=DB::table('a_service_drafts')->count();DB::unprepared("CREATE OR REPLACE FUNCTION sgf_wb_fail() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'WB_OUTBOX_FAIL'; END; \$\$; CREATE TRIGGER wb_fail BEFORE INSERT ON wb_outbox FOR EACH ROW EXECUTE FUNCTION sgf_wb_fail()");$key=(string)Str::uuid();$this->send('generations',['id'=>$p,'version'=>1],$key)->assertStatus(500);$this->assertSame($n,DB::table('a_billing_drafts')->count());$this->assertSame($s,DB::table('a_service_drafts')->count());$this->assertSame(0,DB::table('wb_claims')->count());DB::unprepared('DROP TRIGGER wb_fail ON wb_outbox');$this->send('generations',['id'=>$p,'version'=>1],$key)->assertOk();
|
|
}
|
|
public function test_successor_requires_fresh_review_and_changed_catalogue_invalidates(): void {
|
|
$p=$this->send('proposals',$this->input())->assertOk()->json('id');$this->approve($p);$d=$this->input();$d['version']=2;$d['previous_id']=$p;$d['lines'][0]['price']='210';$next=$this->send('proposals',$d)->assertOk()->json('id');$this->send('generations',['id'=>$p,'version'=>1])->assertStatus(409);$this->send('generations',['id'=>$next,'version'=>2])->assertStatus(409);
|
|
$this->login('reader');$this->send('reviews',['id'=>$next,'version'=>2,'decision'=>'approved','reason'=>'Nova revisão comercial.'])->assertOk();$this->login();app(CompanyContext::class)->selectFor($this->f['manager']->id,$this->f['tenant'],$this->f['company'],$this->f['unit']);app(ServiceWorkflow::class)->save('services',$this->service,$this->f['manager']->id,['version'=>1,'name'=>'Serviço corrigido'],(string)Str::uuid());$this->send('generations',['id'=>$next,'version'=>2])->assertStatus(409);$this->getJson('/api/workshop-billing')->assertOk()->assertJsonPath('orders.0.proposals.1.current',false);$this->assertSame(0,DB::table('wb_generations')->count());
|
|
}
|
|
public function test_generation_replay_rechecks_destination_creation_right_and_real_guard(): void {
|
|
$p=$this->send('proposals',$this->input())->assertOk()->json('id');$this->approve($p);$key=(string)Str::uuid();$this->send('generations',['id'=>$p,'version'=>1],$key)->assertOk();DB::table('permission_grants')->where(['user_id'=>$this->f['manager']->id,'permission'=>'billing_drafts.create'])->delete();$this->send('generations',['id'=>$p,'version'=>1],$key)->assertForbidden();
|
|
$this->app->detectEnvironment(fn()=>'local');try{WorkshopBilling::guard();$this->fail('Real conversion allowed');}catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$this->assertSame(403,$e->getStatusCode());}
|
|
}
|
|
}
|