mapping=['id'=>'mapping-qa','version'=>1,'source_scope'=>['tenant_id'=>91], 'target_scope'=>['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],'order_id'=>3,'lines'=>['SOURCE-LINE'=>4],'authority'=>'sgf_only']; $this->draft=['reference'=>'DRAFT-QA-1','version'=>'source-v1','state'=>'unregistered_draft','document'=>[ 'number'=>'QA-CLIENT-1','issued_on'=>'2026-11-10','subtotal'=>'100.00','tax'=>'0.00','total'=>'100.00', 'items'=>[['source_line_ref'=>'SOURCE-LINE','quantity'=>'1.000','unit'=>'UN','unit_price'=>'100.00','tax_rate'=>'0.00']]]]; $this->client=$this->make(); } private function make(string $context='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'):InvoiceClient { return new InvoiceClient('synthetic',str_repeat('t',64),$context,['tenant_id'=>91],['tenant_id'=>1,'company_id'=>2,'unit_id'=>2], function($method,$url,$headers,$body){$this->calls[]=compact('method','url','headers','body');$next=array_shift($this->queue);if($next instanceof \Throwable)throw $next;if(!is_array($next))throw new \LogicException('No fake response');return $next;}); } private function intent():array{return $this->client->intent($this->mapping,$this->draft,self::KEY,'ConferĂȘncia individual exclusivamente sintĂ©tica.');} private function prepared():array { $this->queue[]=['status'=>200,'json'=>['order'=>['id'=>3],'revision'=>str_repeat('b',64)]]; $this->queue[]=['status'=>200,'json'=>['preview'=>'encrypted-test-cut','impact'=>['operation'=>'Registar pendente','payable_created'=>false]]]; return $this->client->preview($this->intent(),'source-v1',InvoiceClient::mappingHash($this->mapping)); } private function receipt(int $status=201):array{return ['status'=>$status,'json'=>['invoice_id'=>8,'order_id'=>3,'status'=>'pending','replayed'=>$status===200]];} public function test_explicit_mapping_builds_intent_without_network_or_token_persistence():void { $intent=$this->intent();self::assertCount(0,$this->calls);self::assertSame(4,$intent['data']['items'][0]['purchase_order_item_id']); self::assertArrayNotHasKey('source_line_ref',$intent['data']['items'][0]);self::assertStringNotContainsString(str_repeat('t',64),json_encode($intent)); self::assertStringNotContainsString(str_repeat('t',64),print_r($this->client,true)); } public function test_registered_sgc_invoice_cannot_become_an_outgoing_draft():void { $this->draft['state']='pending';$this->expectException(\InvalidArgumentException::class);$this->intent(); } public function test_missing_mapping_does_not_guess_a_line():void { $this->mapping['lines']=[];$this->expectException(\InvalidArgumentException::class);$this->intent(); } public function test_foreign_target_scope_is_refused_before_transport():void { $this->mapping['target_scope']['company_id']=999;$this->expectException(\InvalidArgumentException::class);$this->intent(); } public function test_mapping_version_change_requires_new_conference():void { $intent=$this->intent();$this->mapping['version']=2;$this->expectException(\LogicException::class); $this->client->preview($intent,'source-v1',InvoiceClient::mappingHash($this->mapping)); } public function test_preview_reads_exact_context_then_previews_without_confirmation():void { $p=$this->prepared();self::assertSame('previewed',$p['phase']);self::assertCount(2,$this->calls); self::assertSame('GET',$this->calls[0]['method']);self::assertSame('http://127.0.0.1:8085/api/v1/integration/sgc/invoices/orders/3',$this->calls[0]['url']); self::assertSame('preview',$this->calls[1]['body']['phase']);self::assertSame(self::KEY,$this->calls[1]['headers']['X-Idempotency-Key']); self::assertSame(str_repeat('b',64),$this->calls[1]['body']['revision']); } public function test_individual_approval_confirms_once_and_does_not_claim_validation():void { $p=$this->prepared();$this->queue[]=$this->receipt();$r=$this->client->confirm($p,$p['approval_hash'],'source-v1',InvoiceClient::mappingHash($this->mapping)); self::assertSame('received_pending',$r['phase']);self::assertSame('pending',$r['receipt']['status']);self::assertCount(3,$this->calls); self::assertSame(['phase'=>'confirm','order_id'=>3,'preview'=>'encrypted-test-cut'],$this->calls[2]['body']); } public function test_unapproved_impact_never_reaches_confirmation_transport():void { $p=$this->prepared();try{$this->client->confirm($p,str_repeat('0',64),'source-v1',InvoiceClient::mappingHash($this->mapping));self::fail('Expected refusal');} catch(\LogicException){self::assertCount(2,$this->calls);} } public function test_changed_source_version_blocks_confirmation():void { $p=$this->prepared();$this->expectException(\LogicException::class);$this->client->confirm($p,$p['approval_hash'],'source-v2',InvoiceClient::mappingHash($this->mapping)); } public function test_changed_connection_blocks_stored_envelope():void { $intent=$this->intent();$other=$this->make(str_repeat('c',64));$this->expectException(\LogicException::class); $other->preview($intent,'source-v1',InvoiceClient::mappingHash($this->mapping)); } public function test_uncertain_confirmation_retains_key_cut_and_content_for_explicit_recovery():void { $p=$this->prepared();$this->queue[]=new \RuntimeException('Transport failure containing secret '.str_repeat('t',64)); $unknown=$this->client->confirm($p,$p['approval_hash'],'source-v1',InvoiceClient::mappingHash($this->mapping)); self::assertSame('uncertain',$unknown['phase']);self::assertCount(3,$this->calls);self::assertSame($p['preview'],$unknown['preview']); self::assertStringNotContainsString(str_repeat('t',64),json_encode($unknown)); $this->queue[]=$this->receipt(200);$r=$this->client->confirm($unknown,$p['approval_hash'],'source-v1',InvoiceClient::mappingHash($this->mapping)); self::assertSame('received_pending',$r['phase']);self::assertTrue($r['receipt']['replayed']); self::assertSame($this->calls[2],$this->calls[3]); } public function test_revocation_conflict_and_server_failure_are_not_success():void { foreach([401=>'blocked',403=>'blocked',409=>'review_required',422=>'not_sent',503=>'uncertain',302=>'uncertain']as$status=>$phase){ $p=$this->prepared();$this->queue[]=['status'=>$status,'json'=>null];$r=$this->client->confirm($p,$p['approval_hash'],'source-v1',InvoiceClient::mappingHash($this->mapping)); self::assertSame($phase,$r['phase']);self::assertSame(self::KEY,$r['intent']['operation_key']); } } public function test_unverified_success_does_not_authorise_another_document():void { $p=$this->prepared();$bad=$this->receipt();$bad['json']['order_id']=999;$this->queue[]=$bad; self::assertSame('uncertain',$this->client->confirm($p,$p['approval_hash'],'source-v1',InvoiceClient::mappingHash($this->mapping))['phase']); } public function test_preview_can_recover_existing_durable_receipt():void { $this->queue[]=['status'=>200,'json'=>['order'=>['id'=>3],'revision'=>str_repeat('b',64)]];$this->queue[]=$this->receipt(200); self::assertSame('received_pending',$this->client->preview($this->intent(),'source-v1',InvoiceClient::mappingHash($this->mapping))['phase']); } public function test_another_host_cannot_receive_credentials():void { $this->expectException(\InvalidArgumentException::class); new InvoiceClient('synthetic',str_repeat('t',64),str_repeat('a',64),['tenant_id'=>91],['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],fn()=>[], 'http://example.com'); } public function test_disabled_sgc_gateway_refuses_before_database_or_network():void { $path=dirname(__DIR__,3).'/compras/app/Services/SgfInvoiceGateway.php';if(!is_file($path))self::markTestSkipped('Adjacent SGC workspace required.'); require_once $path; $previous=\Illuminate\Container\Container::getInstance();$container=new \Illuminate\Container\Container; $container->instance('config',new \Illuminate\Config\Repository(['sgf_invoice'=>['enabled'=>false]]));\Illuminate\Container\Container::setInstance($container); try{(new \App\Services\SgfInvoiceGateway)->clientFor(new \App\Models\User,'missing');self::fail('Expected disabled gateway');} catch(\LogicException $e){self::assertSame('SGF invoice gateway disabled.',$e->getMessage());} finally{\Illuminate\Container\Container::setInstance($previous);} } }