Regista aceitacao funcional do primeiro incremento PGC-NIRF e preserva as dependencias documentais e tecnicas existentes. A validacao do calculo fiscal continua separada e pendente. Sem credenciais, bases operacionais ou publicacao.
102 lines
8.2 KiB
PHP
102 lines
8.2 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 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());
|
|
}
|
|
}
|