Skip to content

Commit fae567e

Browse files
authored
feat: add getDanfse for ADN DANFSE artifact retrieval (#17)
* feat(exception): add ArtifactException for DANFSE retrieval errors Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> * feat(exception): add ArtifactRetrievalFailed error code Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> * feat(config): add danfseBaseUrl to EnvironmentConfig Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> * feat(contract): add getDanfse to NfseClientInterface Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> * feat(http): implement getDanfse via ADN DANFSE endpoint Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> * test(config): cover danfseBaseUrl defaults and override Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> * test(http): cover getDanfse success and error paths Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --------- Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent b3ba884 commit fae567e

File tree

7 files changed

+169
-4
lines changed

7 files changed

+169
-4
lines changed

src/Config/EnvironmentConfig.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,22 @@
2020
{
2121
private const BASE_URL_PROD = 'https://sefin.nfse.gov.br/SefinNacional';
2222
private const BASE_URL_SANDBOX = 'https://sefin.producaorestrita.nfse.gov.br/SefinNacional';
23+
private const DANFSE_BASE_URL_PROD = 'https://adn.nfse.gov.br/danfse';
24+
private const DANFSE_BASE_URL_SANDBOX = 'https://adn.producaorestrita.nfse.gov.br/danfse';
2325

2426
public string $baseUrl;
27+
public string $danfseBaseUrl;
2528

2629
public function __construct(
2730
public bool $sandboxMode = false,
2831
?string $baseUrl = null,
32+
?string $danfseBaseUrl = null,
2933
) {
3034
$this->baseUrl = $baseUrl ?? ($sandboxMode
3135
? self::BASE_URL_SANDBOX
3236
: self::BASE_URL_PROD);
37+
$this->danfseBaseUrl = $danfseBaseUrl ?? ($sandboxMode
38+
? self::DANFSE_BASE_URL_SANDBOX
39+
: self::DANFSE_BASE_URL_PROD);
3340
}
3441
}

src/Contracts/NfseClientInterface.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,11 @@ public function query(string $chaveAcesso): ReceiptData;
2626
* Cancel an existing NFS-e.
2727
*/
2828
public function cancel(string $chaveAcesso, string $motivo): bool;
29+
30+
/**
31+
* Retrieve the DANFSE (PDF rendering document) for an NFS-e from ADN.
32+
*
33+
* Returns the raw PDF bytes as a string.
34+
*/
35+
public function getDanfse(string $chaveAcesso): string;
2936
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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\Exception;
9+
10+
/**
11+
* Thrown when the ADN gateway returns an error during DANFSE artifact retrieval.
12+
*/
13+
class ArtifactException extends GatewayException
14+
{
15+
}

src/Exception/NfseErrorCode.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,7 @@ enum NfseErrorCode: string
2929

3030
/** Gateway returned an error when querying an NFS-e (HTTP 4xx/5xx). */
3131
case QueryFailed = 'QUERY_FAILED';
32+
33+
/** ADN gateway returned an error when retrieving DANFSE artifact (HTTP 4xx/5xx). */
34+
case ArtifactRetrievalFailed = 'ARTIFACT_RETRIEVAL_FAILED';
3235
}

src/Http/NfseClient.php

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use LibreCodeCoop\NfsePHP\Contracts\XmlSignerInterface;
1515
use LibreCodeCoop\NfsePHP\Dto\DpsData;
1616
use LibreCodeCoop\NfsePHP\Dto\ReceiptData;
17+
use LibreCodeCoop\NfsePHP\Exception\ArtifactException;
1718
use LibreCodeCoop\NfsePHP\Exception\CancellationException;
1819
use LibreCodeCoop\NfsePHP\Exception\IssuanceException;
1920
use LibreCodeCoop\NfsePHP\Exception\NetworkException;
@@ -80,7 +81,7 @@ public function query(string $chaveAcesso): ReceiptData
8081

8182
public function cancel(string $chaveAcesso, string $motivo): bool
8283
{
83-
$eventoXml = $this->buildCancelEventXml($chaveAcesso, $motivo);
84+
$eventoXml = $this->buildCancelEventXml($chaveAcesso, $motivo);
8485
$signedEventoXml = $this->signer->sign($eventoXml, $this->cert->cnpj);
8586

8687
$compressedEventoXml = gzencode($signedEventoXml);
@@ -106,6 +107,34 @@ public function cancel(string $chaveAcesso, string $motivo): bool
106107
return true;
107108
}
108109

110+
#[\Override]
111+
public function getDanfse(string $chaveAcesso): string
112+
{
113+
$url = $this->environment->danfseBaseUrl . '/' . $chaveAcesso;
114+
115+
[$httpStatus, $body] = $this->getRawBytes($url);
116+
117+
if ($httpStatus >= 400) {
118+
throw new ArtifactException(
119+
'ADN gateway returned error for DANFSE retrieval (HTTP ' . $httpStatus . ')',
120+
NfseErrorCode::ArtifactRetrievalFailed,
121+
$httpStatus,
122+
[],
123+
);
124+
}
125+
126+
if ($body === '') {
127+
throw new ArtifactException(
128+
'ADN gateway returned empty body for DANFSE retrieval',
129+
NfseErrorCode::ArtifactRetrievalFailed,
130+
$httpStatus,
131+
[],
132+
);
133+
}
134+
135+
return $body;
136+
}
137+
109138
// -------------------------------------------------------------------------
110139
// Internal HTTP helpers
111140
// -------------------------------------------------------------------------
@@ -211,13 +240,13 @@ private function buildCancelEventXml(string $chaveAcesso, string $motivo): strin
211240
private function sslContextOptions(): array
212241
{
213242
$options = [
214-
'verify_peer' => true,
243+
'verify_peer' => true,
215244
'verify_peer_name' => true,
216245
];
217246

218247
if ($this->cert->transportCertificatePath !== null && $this->cert->transportPrivateKeyPath !== null) {
219248
$options['local_cert'] = $this->cert->transportCertificatePath;
220-
$options['local_pk'] = $this->cert->transportPrivateKeyPath;
249+
$options['local_pk'] = $this->cert->transportPrivateKeyPath;
221250
}
222251

223252
return $options;
@@ -257,6 +286,35 @@ private function fetchAndDecode(string $path, mixed $context): array
257286
return [$httpStatus, $decoded];
258287
}
259288

289+
/**
290+
* Fetch a URL and return raw response bytes without JSON decoding.
291+
*
292+
* Used for binary endpoints such as ADN DANFSE (PDF artifact retrieval).
293+
* No mTLS is applied — DANFSE is accessible without client certificate.
294+
*
295+
* @return array{int, string}
296+
*/
297+
private function getRawBytes(string $url): array
298+
{
299+
$context = stream_context_create([
300+
'http' => [
301+
'method' => 'GET',
302+
'header' => "Accept: application/pdf\r\n",
303+
'ignore_errors' => true,
304+
],
305+
]);
306+
307+
$http_response_header = [];
308+
$body = file_get_contents($url, false, $context);
309+
$httpStatus = $this->parseHttpStatus($http_response_header);
310+
311+
if ($body === false) {
312+
throw new NetworkException('Failed to connect to ADN DANFSE gateway at ' . $url);
313+
}
314+
315+
return [$httpStatus, $body];
316+
}
317+
260318
/**
261319
* Extract the HTTP status code from the first response header line.
262320
*

tests/Unit/Config/EnvironmentConfigTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,26 @@ public function testCustomBaseUrlOverridesSandboxUrl(): void
5353

5454
self::assertSame($custom, $config->baseUrl);
5555
}
56+
57+
public function testDanfseBaseUrlDefaultsToProductionAdn(): void
58+
{
59+
$config = new EnvironmentConfig(sandboxMode: false);
60+
61+
self::assertSame('https://adn.nfse.gov.br/danfse', $config->danfseBaseUrl);
62+
}
63+
64+
public function testDanfseBaseUrlDefaultsToSandboxAdn(): void
65+
{
66+
$config = new EnvironmentConfig(sandboxMode: true);
67+
68+
self::assertSame('https://adn.producaorestrita.nfse.gov.br/danfse', $config->danfseBaseUrl);
69+
}
70+
71+
public function testCustomDanfseBaseUrlOverridesDefault(): void
72+
{
73+
$custom = 'http://localhost:9999/danfse';
74+
$config = new EnvironmentConfig(danfseBaseUrl: $custom);
75+
76+
self::assertSame($custom, $config->danfseBaseUrl);
77+
}
5678
}

tests/Unit/Http/NfseClientTest.php

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use LibreCodeCoop\NfsePHP\Config\EnvironmentConfig;
1414
use LibreCodeCoop\NfsePHP\Contracts\XmlSignerInterface;
1515
use LibreCodeCoop\NfsePHP\Dto\DpsData;
16+
use LibreCodeCoop\NfsePHP\Exception\ArtifactException;
1617
use LibreCodeCoop\NfsePHP\Exception\CancellationException;
1718
use LibreCodeCoop\NfsePHP\Exception\IssuanceException;
1819
use LibreCodeCoop\NfsePHP\Exception\NfseErrorCode;
@@ -278,13 +279,65 @@ public function testCancellationExceptionCarriesErrorCodeAndHttpStatus(): void
278279
}
279280
}
280281

282+
// -------------------------------------------------------------------------
283+
// getDanfse tests
284+
// -------------------------------------------------------------------------
285+
286+
public function testGetDanfseReturnsPdfBytesOnSuccess(): void
287+
{
288+
$fakePdfBytes = '%PDF-1.4 fake pdf content for testing';
289+
290+
self::$server->setResponseOfPath(
291+
'/danfse/abc-danfse-key-123',
292+
new Response($fakePdfBytes, ['Content-Type' => 'application/pdf'], 200)
293+
);
294+
295+
$client = $this->makeClient($this->signer, danfseBaseUrl: self::$server->getServerRoot() . '/danfse');
296+
297+
$pdf = $client->getDanfse('abc-danfse-key-123');
298+
299+
self::assertSame($fakePdfBytes, $pdf);
300+
}
301+
302+
public function testGetDanfseThrowsArtifactExceptionWhenGatewayReturnsError(): void
303+
{
304+
self::$server->setResponseOfPath(
305+
'/danfse/not-found-key',
306+
new Response('not found', ['Content-Type' => 'text/plain'], 404)
307+
);
308+
309+
$client = $this->makeClient($this->signer, danfseBaseUrl: self::$server->getServerRoot() . '/danfse');
310+
311+
$this->expectException(ArtifactException::class);
312+
$client->getDanfse('not-found-key');
313+
}
314+
315+
public function testArtifactExceptionCarriesErrorCodeAndHttpStatus(): void
316+
{
317+
self::$server->setResponseOfPath(
318+
'/danfse/server-error-key',
319+
new Response('internal error', ['Content-Type' => 'text/plain'], 500)
320+
);
321+
322+
$client = $this->makeClient($this->signer, danfseBaseUrl: self::$server->getServerRoot() . '/danfse');
323+
324+
try {
325+
$client->getDanfse('server-error-key');
326+
self::fail('Expected ArtifactException');
327+
} catch (ArtifactException $e) {
328+
self::assertSame(NfseErrorCode::ArtifactRetrievalFailed, $e->errorCode);
329+
self::assertSame(500, $e->httpStatus);
330+
}
331+
}
332+
281333
// -------------------------------------------------------------------------
282334

283-
private function makeClient(?XmlSignerInterface $signer = null): NfseClient
335+
private function makeClient(?XmlSignerInterface $signer = null, ?string $danfseBaseUrl = null): NfseClient
284336
{
285337
return new NfseClient(
286338
environment: new EnvironmentConfig(
287339
baseUrl: self::$server->getServerRoot() . '/SefinNacional',
340+
danfseBaseUrl: $danfseBaseUrl ?? self::$server->getServerRoot() . '/danfse',
288341
),
289342
cert: new CertConfig(
290343
cnpj: '29842527000145',

0 commit comments

Comments
 (0)