Skip to content

Commit 9ecb0d4

Browse files
committed
feat: add NfseClient HTTP adapter for SEFIN Nacional REST API
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent a776b1e commit 9ecb0d4

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

src/Http/NfseClient.php

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreCodeCoop\NfsePHP\Http;
9+
10+
use LibreCodeCoop\NfsePHP\Contracts\NfseClientInterface;
11+
use LibreCodeCoop\NfsePHP\Contracts\SecretStoreInterface;
12+
use LibreCodeCoop\NfsePHP\Contracts\XmlSignerInterface;
13+
use LibreCodeCoop\NfsePHP\Dto\DpsData;
14+
use LibreCodeCoop\NfsePHP\Dto\ReceiptData;
15+
use LibreCodeCoop\NfsePHP\Exception\NfseException;
16+
use LibreCodeCoop\NfsePHP\Xml\DpsSigner;
17+
use LibreCodeCoop\NfsePHP\Xml\XmlBuilder;
18+
19+
/**
20+
* HTTP client for the SEFIN Nacional NFS-e REST API.
21+
*
22+
* Communicates with the SEFIN gateway to issue, query, and cancel NFS-e.
23+
* All requests carry a signed DPS XML payload.
24+
*
25+
* Gateway sandbox base URL: https://hml.nfse.fazenda.gov.br/NFS-e/api/v1
26+
* Gateway production base URL: https://nfse.fazenda.gov.br/NFS-e/api/v1
27+
*/
28+
class NfseClient implements NfseClientInterface
29+
{
30+
private const BASE_URL_PROD = 'https://nfse.fazenda.gov.br/NFS-e/api/v1';
31+
private const BASE_URL_SANDBOX = 'https://hml.nfse.fazenda.gov.br/NFS-e/api/v1';
32+
33+
private readonly string $baseUrl;
34+
private readonly XmlSignerInterface $signer;
35+
36+
public function __construct(
37+
private readonly SecretStoreInterface $secretStore,
38+
private readonly bool $sandboxMode = false,
39+
?string $baseUrlOverride = null,
40+
?XmlSignerInterface $signer = null,
41+
) {
42+
$this->baseUrl = $baseUrlOverride ?? ($sandboxMode ? self::BASE_URL_SANDBOX : self::BASE_URL_PROD);
43+
$this->signer = $signer ?? new DpsSigner($secretStore);
44+
}
45+
46+
public function emit(DpsData $dps): ReceiptData
47+
{
48+
$xml = (new XmlBuilder())->buildDps($dps);
49+
$signed = $this->signer->sign($xml, $dps->cnpjPrestador);
50+
51+
$response = $this->post('/dps', $signed);
52+
53+
return $this->parseReceiptResponse($response);
54+
}
55+
56+
public function query(string $chaveAcesso): ReceiptData
57+
{
58+
$response = $this->get('/dps/' . $chaveAcesso);
59+
60+
return $this->parseReceiptResponse($response);
61+
}
62+
63+
public function cancel(string $chaveAcesso, string $motivo): bool
64+
{
65+
$this->delete('/dps/' . $chaveAcesso, $motivo);
66+
67+
return true;
68+
}
69+
70+
// -------------------------------------------------------------------------
71+
// Internal HTTP helpers
72+
// -------------------------------------------------------------------------
73+
74+
/**
75+
* @return array<string, mixed>
76+
*/
77+
private function post(string $path, string $xmlPayload): array
78+
{
79+
$context = stream_context_create([
80+
'http' => [
81+
'method' => 'POST',
82+
'header' => "Content-Type: application/xml\r\nAccept: application/json\r\n",
83+
'content' => $xmlPayload,
84+
'ignore_errors' => true,
85+
],
86+
]);
87+
88+
return $this->request($path, $context);
89+
}
90+
91+
/**
92+
* @return array<string, mixed>
93+
*/
94+
private function get(string $path): array
95+
{
96+
$context = stream_context_create([
97+
'http' => [
98+
'method' => 'GET',
99+
'header' => "Accept: application/json\r\n",
100+
'ignore_errors' => true,
101+
],
102+
]);
103+
104+
return $this->request($path, $context);
105+
}
106+
107+
private function delete(string $path, string $motivo): void
108+
{
109+
$payload = json_encode(['motivo' => $motivo], JSON_THROW_ON_ERROR);
110+
$context = stream_context_create([
111+
'http' => [
112+
'method' => 'DELETE',
113+
'header' => "Content-Type: application/json\r\nAccept: application/json\r\n",
114+
'content' => $payload,
115+
'ignore_errors' => true,
116+
],
117+
]);
118+
119+
$this->request($path, $context);
120+
}
121+
122+
/**
123+
* @return array<string, mixed>
124+
*/
125+
private function request(string $path, mixed $context): array
126+
{
127+
$url = $this->baseUrl . $path;
128+
$body = file_get_contents($url, false, $context);
129+
130+
if ($body === false) {
131+
throw new NfseException('Failed to connect to SEFIN gateway at ' . $url);
132+
}
133+
134+
$decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
135+
136+
if (!is_array($decoded)) {
137+
throw new NfseException('Unexpected response format from SEFIN gateway');
138+
}
139+
140+
return $decoded;
141+
}
142+
143+
/**
144+
* @param array<string, mixed> $response
145+
*/
146+
private function parseReceiptResponse(array $response): ReceiptData
147+
{
148+
return new ReceiptData(
149+
nfseNumber: (string) ($response['nNFSe'] ?? $response['numero'] ?? ''),
150+
chaveAcesso: (string) ($response['chaveAcesso'] ?? $response['id'] ?? ''),
151+
dataEmissao: (string) ($response['dhEmi'] ?? $response['dataEmissao'] ?? ''),
152+
codigoVerificacao: isset($response['codigoVerificacao']) ? (string) $response['codigoVerificacao'] : null,
153+
rawXml: null,
154+
);
155+
}
156+
}

0 commit comments

Comments
 (0)