From 6d39a19dcc127395e6636f0be02e879bb811e0b0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:22:51 +0000 Subject: [PATCH 1/2] feat: submit decision-flow NFS-e through dedicated endpoint Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/Contracts/NfseClientInterface.php | 6 ++ src/Http/NfseClient.php | 37 ++++++++- tests/Unit/Http/InjectableTransportTest.php | 92 +++++++++++++++++++++ 3 files changed, 133 insertions(+), 2 deletions(-) diff --git a/src/Contracts/NfseClientInterface.php b/src/Contracts/NfseClientInterface.php index 341c33d..1e6bb74 100644 --- a/src/Contracts/NfseClientInterface.php +++ b/src/Contracts/NfseClientInterface.php @@ -7,6 +7,7 @@ namespace LibreCodeCoop\NfsePHP\Contracts; +use LibreCodeCoop\NfsePHP\Dto\DecisionNfseData; use LibreCodeCoop\NfsePHP\Dto\DpsData; use LibreCodeCoop\NfsePHP\Dto\ReceiptData; @@ -17,6 +18,11 @@ interface NfseClientInterface */ public function emit(DpsData $dps): ReceiptData; + /** + * Emit a complete NFS-e through the administrative/judicial decision bypass. + */ + public function emitDecision(DecisionNfseData $nfse): ReceiptData; + /** * Query an existing NFS-e by its access key. */ diff --git a/src/Http/NfseClient.php b/src/Http/NfseClient.php index 1b82eb6..51d63d2 100644 --- a/src/Http/NfseClient.php +++ b/src/Http/NfseClient.php @@ -17,6 +17,7 @@ use LibreCodeCoop\NfsePHP\Contracts\SecretStoreInterface; use LibreCodeCoop\NfsePHP\Contracts\XmlSignerInterface; use LibreCodeCoop\NfsePHP\Danfse\DanfseGenerator; +use LibreCodeCoop\NfsePHP\Dto\DecisionNfseData; use LibreCodeCoop\NfsePHP\Dto\DpsData; use LibreCodeCoop\NfsePHP\Dto\EventReceiptData; use LibreCodeCoop\NfsePHP\Dto\EventRegistrationData; @@ -30,6 +31,7 @@ use LibreCodeCoop\NfsePHP\Exception\QueryException; use LibreCodeCoop\NfsePHP\Support\DpsIdentifier; use LibreCodeCoop\NfsePHP\Support\GzipBase64; +use LibreCodeCoop\NfsePHP\Xml\DecisionNfseBuilder; use LibreCodeCoop\NfsePHP\Xml\DpsSigner; use LibreCodeCoop\NfsePHP\Xml\XmlBuilder; @@ -79,6 +81,29 @@ public function emit(DpsData $dps): ReceiptData return $this->parseReceiptResponse($body); } + public function emitDecision(DecisionNfseData $nfse): ReceiptData + { + $xml = (new DecisionNfseBuilder())->build($nfse); + $signed = $this->signer->sign($xml, $nfse->dps->cnpjPrestador); + + [$httpStatus, $body] = $this->postCompressedXml( + '/decisao-judicial/nfse', + 'xmlGZipB64', + $signed, + ); + + if ($httpStatus >= 400) { + throw new IssuanceException( + 'SEFIN gateway rejected decision-flow issuance (HTTP ' . $httpStatus . ')', + NfseErrorCode::IssuanceRejected, + $httpStatus, + $body, + ); + } + + return $this->parseReceiptResponse($body); + } + public function query(string $chaveAcesso): ReceiptData { [$httpStatus, $body] = $this->get('/nfse/' . $chaveAcesso); @@ -244,15 +269,23 @@ public function getDanfse(string $nfseXml): string * @return array{int, array} */ private function post(string $path, string $xmlPayload): array + { + return $this->postCompressedXml($path, 'dpsXmlGZipB64', $xmlPayload); + } + + /** + * @return array{int, array} + */ + private function postCompressedXml(string $path, string $field, string $xmlPayload): array { $compressedPayload = gzencode($xmlPayload); if ($compressedPayload === false) { - throw new NetworkException('Failed to compress DPS XML payload before transmission.'); + throw new NetworkException('Failed to compress fiscal XML payload before transmission.'); } $payload = json_encode([ - 'dpsXmlGZipB64' => base64_encode($compressedPayload), + $field => base64_encode($compressedPayload), ], JSON_THROW_ON_ERROR); return $this->fetchAndDecode($path, 'POST', $payload); diff --git a/tests/Unit/Http/InjectableTransportTest.php b/tests/Unit/Http/InjectableTransportTest.php index 99f1005..ce307f1 100644 --- a/tests/Unit/Http/InjectableTransportTest.php +++ b/tests/Unit/Http/InjectableTransportTest.php @@ -12,6 +12,8 @@ use LibreCodeCoop\NfsePHP\Config\EnvironmentConfig; use LibreCodeCoop\NfsePHP\Config\MunicipalParametersConfig; use LibreCodeCoop\NfsePHP\Contracts\XmlSignerInterface; +use LibreCodeCoop\NfsePHP\Dto\DecisionIssuerAddressData; +use LibreCodeCoop\NfsePHP\Dto\DecisionNfseData; use LibreCodeCoop\NfsePHP\Dto\DpsData; use LibreCodeCoop\NfsePHP\Dto\HttpResponseData; use LibreCodeCoop\NfsePHP\Dto\SubstitutionData; @@ -81,6 +83,96 @@ public function sign(string $xml, string $cnpj): string self::assertArrayHasKey('dpsXmlGZipB64', $payload); } + public function testDecisionIssuanceUsesDedicatedBypassEndpointAndPayloadContract(): void + { + $transport = new FakeHttpTransport(new HttpResponseData( + 201, + '{"nNFSe":"240","chaveAcesso":"DECISION-240","dataHoraProcessamento":"2026-10-04T12:00:00-03:00"}', + )); + + $client = new NfseClient( + environment: new EnvironmentConfig( + sandboxMode: true, + baseUrl: 'https://sefin.invalid.test/SefinNacional', + ), + cert: new CertConfig( + cnpj: '11222333000181', + pfxPath: '/does/not/exist.p12', + vaultPath: 'test/no-secret-required', + ), + secretStore: new NoOpSecretStore(), + signer: new class () implements XmlSignerInterface { + public function sign(string $xml, string $cnpj): string + { + return $xml; + } + }, + transport: $transport, + ); + + $receipt = $client->emitDecision(new DecisionNfseData( + dps: new DpsData( + cnpjPrestador: '11222333000181', + municipioIbge: '3303302', + itemListaServico: '001', + valorServico: '1000.00', + aliquota: '5.00', + discriminacao: 'Consultoria em tecnologia da informacao', + tipoAmbiente: 2, + serie: '1', + numeroDps: '42', + dataCompetencia: '2026-10-04', + codigoTributacaoNacional: '010701', + opcaoSimplesNacional: 1, + regimeEspecialTributacao: 0, + tipoRetencaoIss: 1, + indicadorTributacao: 0, + ), + numeroNfse: '240', + codigoNumerico: '123456789', + localEmissao: 'Niteroi', + localPrestacao: 'Niteroi', + descricaoTributacaoNacional: 'Consultoria em tecnologia da informacao', + emitenteNome: 'Prestador de Teste Ltda', + emitenteEndereco: new DecisionIssuerAddressData( + 'Rua de Teste', + '100', + 'Centro', + '3303302', + 'RJ', + '24000000', + ), + valorLiquido: '1000.00', + numeroDfse: '1', + codigoLocalIncidencia: '3303302', + localIncidencia: 'Niteroi', + baseCalculo: '1000.00', + aliquotaAplicada: '5.00', + valorIssqn: '50.00', + )); + + self::assertSame('240', $receipt->nfseNumber); + self::assertCount(1, $transport->requests); + + $request = $transport->requests[0]; + self::assertSame('POST', $request->method); + self::assertSame( + 'https://sefin.invalid.test/SefinNacional/decisao-judicial/nfse', + $request->url, + ); + self::assertNotNull($request->body); + + $payload = json_decode($request->body, true, 512, JSON_THROW_ON_ERROR); + self::assertSame(['xmlGZipB64'], array_keys($payload)); + + $compressed = base64_decode((string) $payload['xmlGZipB64'], true); + self::assertNotFalse($compressed); + $xml = gzdecode($compressed); + self::assertNotFalse($xml); + self::assertStringContainsString('102', $xml); + } + public function testSubstitutionUsesNormalIssuanceEndpointWithoutExtraMutationRequest(): void { $transport = new FakeHttpTransport(new HttpResponseData( From 5568e7fb28c918009ebe3065c9e31e2bc41a9ae3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:24:29 +0000 Subject: [PATCH 2/2] fix: mark decision issuance interface override Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/Http/NfseClient.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/NfseClient.php b/src/Http/NfseClient.php index 51d63d2..c707f55 100644 --- a/src/Http/NfseClient.php +++ b/src/Http/NfseClient.php @@ -81,6 +81,7 @@ public function emit(DpsData $dps): ReceiptData return $this->parseReceiptResponse($body); } + #[\Override] public function emitDecision(DecisionNfseData $nfse): ReceiptData { $xml = (new DecisionNfseBuilder())->build($nfse);