Files
SGF/tests/Feature/ServiceCalculationTest.php
2026-11-02 08:06:45 +02:00

104 lines
8.3 KiB
PHP

<?php
namespace Tests\Feature;
use App\Modules\Services\ServiceCalculation;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\Support\DocumentPreviewFixture;
use Tests\TestCase;
final class ServiceCalculationTest extends TestCase
{
use \Tests\Support\UsesModuleFixture;
protected function moduleFixture(): string { return 'commercial'; }
use RefreshDatabase;
private array $f;
private int $draft;
private int $profile;
private string $token;
private int $service;
protected function setUp(): void
{
parent::setUp(); $this->f = DocumentPreviewFixture::create(); $this->login();
$client = $this->write('clients',['name'=>'Cliente sintético'])->json('id');
$this->service = $this->write('services',['name'=>'Serviço sintético'])->json('id');
$source = $this->write('service_drafts',['client_id'=>$client,'currency'=>'MZN','lines'=>[['service_id'=>$this->service,'quantity'=>'1','unit_price'=>'1000.03']]])->json('id');
$this->draft = $this->write('billing_drafts',['source_id'=>$source])->json('id');
$this->withHeader('X-SGF-Context',$this->token)->patchJson('/api/document-preparation/billing_drafts/'.$this->draft,['version'=>0,'draft_version'=>1,'currency'=>'MZN','price_mode'=>'added','units'=>['UN'],'payment_instruction_ids'=>[]])->assertOk();
$this->profile = $this->newProfile(1);
}
private function login(string $kind='manager'): void
{
$this->postJson('/api/login',['email'=>$this->f[$kind]->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 write(string $area,array $data,?int $id=null)
{
$this->withHeaders(['X-SGF-Context'=>$this->token,'Idempotency-Key'=>(string)Str::uuid()]);
return ($id ? $this->patchJson("/api/a/$area/$id",$data) : $this->postJson("/api/a/$area",$data))->assertSuccessful();
}
private function newProfile(int $version): int
{
return DB::table('service_calculation_profiles')->insertGetId(['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['company'],'unit_id'=>$this->f['unit'],'identifier'=>'SYNTHETIC-MZN-16','version'=>$version,'definition'=>json_encode(['kind'=>'synthetic','currency'=>'MZN','rate_percent'=>'16','policy'=>ServiceCalculation::POLICY,'sources'=>ServiceCalculation::SOURCES]),'created_at'=>now()]);
}
private function calculate(array $changes=[])
{
return $this->withHeader('X-SGF-Context',$this->token)->postJson('/api/service-calculations/'.$this->draft,array_replace(['draft_version'=>1,'preparation_version'=>1,'profile_id'=>$this->profile,'profile_version'=>1],$changes));
}
private function preview() { return $this->getJson('/api/document-preview/draft/'.$this->draft)->assertOk(); }
public function test_exact_values_truncation_boundaries_and_missing_rules(): void
{
$profile=['kind'=>'synthetic','currency'=>'MZN','rate_percent'=>'16','policy'=>ServiceCalculation::POLICY];
foreach ([['100','16','16.00','0','116.00'],['1000.03','160.0048','160.00','0.0048','1160.03'],['0.03','0.0048','0.00','0.0048','0.03'],['0.06249999','0.0099999984','0.00','0.0099999984',null],['0.0625','0.01','0.01','0',null],['0.06250001','0.0100000016','0.01','0.0000000016',null],['0','0','0.00','0','0.00']] as [$base,$exact,$doc,$difference,$total]) {
$result=ServiceCalculation::evaluate(['currency'=>'MZN','lines'=>[['quantity'=>'1','unit_price'=>$base]]],['price_mode'=>'added'],$profile);
foreach (['vat_exact'=>$exact,'vat_document_proposed'=>$doc,'discarded_difference'=>$difference,'total_document_proposed'=>$total] as $field=>$expected) { $this->assertSame($expected,$result[$field],$base.' '.$field); }
$this->assertFalse($result['fiscally_validated']);
}
$data=['currency'=>'MZN','lines'=>[['quantity'=>'1','unit_price'=>'0.03']]];
foreach ([null,array_diff_key($profile,['rate_percent'=>true]),array_replace($profile,['currency'=>'USD']),array_replace($profile,['kind'=>'real']),array_replace($profile,['policy'=>'old'])] as $bad) { $this->assertSame('pending',ServiceCalculation::evaluate($data,['price_mode'=>'added'],$bad)['state']); }
foreach ([[],['price_mode'=>'included']] as $prepared) { $this->assertSame('pending',ServiceCalculation::evaluate($data,$prepared,$profile)['state']); }
$data['currency']=null; $result=ServiceCalculation::evaluate($data,['price_mode'=>'added'],$profile); $this->assertSame('pending',$result['state']); $this->assertArrayNotHasKey('vat_exact',$result);
}
public function test_persistence_repetition_versions_history_and_profile_changes(): void
{
$id=$this->calculate()->assertOk()->assertJsonPath('replayed',false)->json('id');
$this->calculate()->assertOk()->assertJsonPath('id',$id)->assertJsonPath('replayed',true);
$this->assertDatabaseCount('service_calculation_results',1);
$this->preview()->assertJsonPath('calculation.history.0.current',true)->assertJsonPath('calculation.history.0.snapshot.result.vat_exact','160.0048');
$this->calculate(['draft_version'=>2])->assertStatus(409);
$this->newProfile(2); $this->calculate()->assertStatus(409);
$this->preview()->assertJsonPath('calculation.history.0.current',false);
$this->write('billing_drafts',['version'=>1,'lines'=>[['service_id'=>$this->service,'quantity'=>'1','unit_price'=>'0.03']]],$this->draft);
$this->preview()->assertJsonPath('calculation.history.0.snapshot.result.vat_exact','160.0048');
}
public function test_reader_revocation_cross_scope_and_normal_environment_cannot_calculate(): void
{
$this->calculate()->assertOk(); $this->login('reader'); $this->preview()->assertJsonPath('calculation.can_calculate',false); $this->calculate()->assertForbidden();
$this->login();
$foreign=DB::table('service_calculation_profiles')->insertGetId(['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['company'],'unit_id'=>$this->f['hidden'],'identifier'=>'FOREIGN','version'=>1,'definition'=>'{}','created_at'=>now()]);
$this->calculate(['profile_id'=>$foreign])->assertForbidden();
$this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['sibling'],'unit_id'=>$this->f['siblingUnit']])->assertOk();
$this->getJson('/api/document-preview/draft/'.$this->draft)->assertForbidden();
$this->login(); DB::table('permission_grants')->where('user_id',$this->f['manager']->id)->where('permission','billing_drafts.edit')->delete(); $this->calculate()->assertForbidden();
$this->app->instance('env','local');
try { app(ServiceCalculation::class)->calculate($this->draft,$this->f['manager']->id,[]); $this->fail('Normal environment accepted a synthetic calculation'); }
catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { $this->assertSame(403,$e->getStatusCode()); }
app(\App\Shared\Context\CompanyContext::class)->selectFor($this->f['manager']->id,$this->f['tenant'],$this->f['company'],$this->f['unit']);
$read=app(ServiceCalculation::class)->read($this->draft,$this->f['manager']->id);
$this->assertSame('normal',$read['environment']); $this->assertSame([],$read['history']);
}
public function test_database_refuses_mass_transfer_and_b1_remains_unchanged(): void
{
$this->calculate()->assertOk();
try { DB::transaction(fn()=>DB::table('service_calculation_results')->update(['unit_id'=>$this->f['hidden']])); $this->fail('Mass transfer accepted'); }
catch (\Illuminate\Database\QueryException $e) { $this->assertStringContainsString('SGF_',$e->getMessage()); }
$this->withHeaders(['X-SGF-Context'=>$this->token,'Idempotency-Key'=>(string)Str::uuid()])->postJson('/api/b1/billing-drafts/'.$this->draft.'/confirm-internal',['version'=>1])->assertOk();
$before=DB::table('b_internal_confirmations')->get()->toJson();
$this->calculate()->assertStatus(409);
$this->getJson('/api/document-preview/history/'.$this->draft)->assertOk()->assertJsonMissingPath('calculation');
$this->assertSame($before,DB::table('b_internal_confirmations')->get()->toJson());
}
}