Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
- Emit NFS-e (`emit`)
- Query NFS-e (`query`)
- Cancel NFS-e (`cancel`)
- Retrieve DANFSE bytes (`getDanfse`)
- Generate the DANFSe PDF locally from the NFS-e XML (`getDanfse` / `Danfse\DanfseGenerator`)
- Sign DPS XML with PFX credentials
- Read secrets from OpenBao/Vault or an in-memory store

Expand Down
3 changes: 1 addition & 2 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ default-copyright = "2026 LibreCode coop and contributors"
[[annotations]]
path = [
".gitignore",
"composer.json",
"tests/Integration/.gitkeep"
"composer.json"
]
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 LibreCode coop and contributors"
Expand Down
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
},
"require": {
"php": "^8.2",
"ext-openssl": "*",
"ext-dom": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-soap": "*",
"bacon/bacon-qr-code": "^3.0",
"csharpru/vault-php": "^4.4",
"dompdf/dompdf": "^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
Expand Down
7 changes: 0 additions & 7 deletions src/Config/EnvironmentConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,15 @@
{
private const BASE_URL_PROD = 'https://sefin.nfse.gov.br/SefinNacional';
private const BASE_URL_SANDBOX = 'https://sefin.producaorestrita.nfse.gov.br/SefinNacional';
private const DANFSE_BASE_URL_PROD = 'https://adn.nfse.gov.br/danfse';
private const DANFSE_BASE_URL_SANDBOX = 'https://adn.producaorestrita.nfse.gov.br/danfse';

public string $baseUrl;
public string $danfseBaseUrl;

public function __construct(
public bool $sandboxMode = false,
?string $baseUrl = null,
?string $danfseBaseUrl = null,
) {
$this->baseUrl = $baseUrl ?? ($sandboxMode
? self::BASE_URL_SANDBOX
: self::BASE_URL_PROD);
$this->danfseBaseUrl = $danfseBaseUrl ?? ($sandboxMode
? self::DANFSE_BASE_URL_SANDBOX
: self::DANFSE_BASE_URL_PROD);
}
}
5 changes: 3 additions & 2 deletions src/Contracts/NfseClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ public function query(string $chaveAcesso): ReceiptData;
public function cancel(string $chaveAcesso, string $motivo): bool;

/**
* Retrieve the DANFSE (PDF rendering document) for an NFS-e from ADN.
* Generate the DANFSe (PDF auxiliary document) locally from an authorized
* NFS-e XML (the XML returned by emit()/query() in ReceiptData::$rawXml).
*
* Returns the raw PDF bytes as a string.
*/
public function getDanfse(string $chaveAcesso): string;
public function getDanfse(string $nfseXml): string;
Comment thread
vitormattos marked this conversation as resolved.
}
28 changes: 28 additions & 0 deletions src/Danfse/Config/DanfseConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Danfse\Config;

/**
* Immutable presentation options for the DANFSe.
*
* The provider logo is optional: pass a ready data URI via $logoDataUri, or a
* file path via $logoPath (a data URI takes precedence). When neither is given
* the header logo area stays empty.
*/
final readonly class DanfseConfig
{
public ?string $logoDataUri;

public function __construct(
?string $logoDataUri = null,
?string $logoPath = null,
public ?MunicipalityBranding $municipality = null,
) {
$this->logoDataUri = LogoLoader::resolve($logoDataUri, $logoPath);
}
}
41 changes: 41 additions & 0 deletions src/Danfse/Config/LogoLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Danfse\Config;

/**
* Reads an image file from disk and builds a base64 data URI for embedding in
* the DANFSe HTML (dompdf has remote loading disabled).
*/
final class LogoLoader
{
/**
* Resolve a logo to a data URI: a ready data URI takes precedence, otherwise
* a file path is loaded from disk, otherwise null (no logo).
*/
public static function resolve(?string $dataUri, ?string $path): ?string
{
return $dataUri ?? ($path !== null ? self::pathToDataUri($path) : null);
}

public static function pathToDataUri(string $path): string
{
if (!is_readable($path)) {
throw new \InvalidArgumentException("Logo file not found or unreadable: {$path}");
}

$contents = file_get_contents($path);

if ($contents === false) {
throw new \RuntimeException("Could not read logo file: {$path}");
}

$mime = mime_content_type($path) ?: 'image/png';

return 'data:' . $mime . ';base64,' . base64_encode($contents);
}
}
29 changes: 29 additions & 0 deletions src/Danfse/Config/MunicipalityBranding.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Danfse\Config;

/**
* Optional municipal issuer branding shown on the DANFSe header.
*
* The logo accepts either a file path or a ready data URI; a data URI takes
* precedence when both are supplied.
*/
final readonly class MunicipalityBranding
{
public ?string $logoDataUri;

public function __construct(
public string $name,
public string $department = '',
public string $email = '',
?string $logoDataUri = null,
?string $logoPath = null,
) {
$this->logoDataUri = LogoLoader::resolve($logoDataUri, $logoPath);
}
}
105 changes: 105 additions & 0 deletions src/Danfse/DanfseGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Danfse;

use Dompdf\Dompdf;
use Dompdf\Options;
use LibreCodeCoop\NfsePHP\Danfse\Config\DanfseConfig;
use LibreCodeCoop\NfsePHP\Exception\ArtifactException;
use LibreCodeCoop\NfsePHP\Exception\NfseErrorCode;

/**
* Generates the DANFSe (PDF auxiliary document) locally from an authorized
* NFS-e Nacional XML, replacing the (sunset) ADN generation API.
*
* Usage:
* $pdf = (new DanfseGenerator())->generateFromXml($nfseXml);
*/
final class DanfseGenerator
{
public function __construct(
private readonly DanfseConfig $config = new DanfseConfig(),
) {
}

/**
* Render the DANFSe PDF from the NFS-e XML and return its raw bytes.
*/
public function generateFromXml(string $xml): string
{
$html = $this->generateHtml($xml);

try {
$options = new Options();
$options->set('isHtml5ParserEnabled', true);
$options->set('isRemoteEnabled', false);
$options->set('defaultFont', 'Helvetica');

$dompdf = new Dompdf($options);
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();

return (string) $dompdf->output();
} catch (\Throwable $e) {
throw new ArtifactException(
'Failed to render DANFSe PDF: ' . $e->getMessage(),
NfseErrorCode::ArtifactRetrievalFailed,
previous: $e,
);
}
}

/**
* Render the intermediate HTML (useful for inspection and testing).
*/
public function generateHtml(string $xml): string
{
try {
$data = (new XmlToArray())->convert($xml);
} catch (\Throwable $e) {
throw new ArtifactException(
'Failed to parse NFS-e XML for DANFSe generation: ' . $e->getMessage(),
NfseErrorCode::ArtifactRetrievalFailed,
previous: $e,
);
}

$this->assertAuthorizedNfse($data);

return (new DanfseTemplate())->render($data, $this->config);
}

/**
* @param array<string, mixed> $data
*/
private function assertAuthorizedNfse(array $data): void
{
$infNfse = $data['infNFSe'] ?? null;
if (!is_array($infNfse)) {
throw $this->invalidNfseXml();
}

$id = $infNfse['Id'] ?? null;
if (!is_string($id) || trim($id) === '' || !str_starts_with(trim($id), 'NFS')) {
throw $this->invalidNfseXml();
}

if (!is_array($infNfse['DPS']['infDPS'] ?? null)) {
throw $this->invalidNfseXml();
}
}

private function invalidNfseXml(): ArtifactException
{
return new ArtifactException(
'Failed to generate DANFSe: XML does not contain an authorized NFS-e.',
NfseErrorCode::ArtifactRetrievalFailed,
);
}
}
Loading
Loading