From 44f240a33e9b8ca138ac707a31c1753765a62e4b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:32:50 +0000 Subject: [PATCH] fix(danfse): render NT008 municipality header from authorized XML (#117) Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/danfse-v2-rendering.md | 1 + src/Danfse/DanfseTemplate.php | 19 ++++++++- src/Danfse/HTMLTemplate.php | 12 ++++-- tests/Unit/Danfse/DanfseTemplateTest.php | 52 ++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/docs/danfse-v2-rendering.md b/docs/danfse-v2-rendering.md index bc2fbd1..f163d91 100644 --- a/docs/danfse-v2-rendering.md +++ b/docs/danfse-v2-rendering.md @@ -12,6 +12,7 @@ uses synthetic/public-schema data rather than copyrighted screenshots. The stable contract covered by tests is: - authorized NFS-e identity, access key and QR-code presence; +- header municipality and UF only from `infNFSe/xLocEmi` and `emit/enderNac/UF`, suppressed for national taxation item 99; environment identification remains visible; - provider, taker and intermediary identity fields; - retained ISS labels from the DPS and municipal base/rate/assessed tax from the authorized `infNFSe/valores` (not the DPS `tribMun`); - foreign taker NIF without inventing a Brazilian CPF/CNPJ; diff --git a/src/Danfse/DanfseTemplate.php b/src/Danfse/DanfseTemplate.php index d3f49bd..01edd7c 100644 --- a/src/Danfse/DanfseTemplate.php +++ b/src/Danfse/DanfseTemplate.php @@ -106,7 +106,11 @@ public function buildData(array $nfse): array 'emissao_dps' => $this->formatter->dateTime($this->val($infDps, 'dhEmi')), 'ambiente' => Ambiente::fromValue($this->val($infDps, 'tpAmb'))->value, 'ambiente_gerador' => $this->val($infNfse, 'ambGer') ?: '-', - 'municipio_emissao' => $this->val($infNfse, 'xLocEmi') ?: '-', + 'municipio_emissao' => $this->headerMunicipality( + $this->val($infNfse, 'xLocEmi'), + $this->val($enderEmit, 'UF'), + $this->val($cServ, 'cTribNac'), + ), 'emitente_nfse' => $this->emitterLabel($this->val($infDps, 'tpEmit')), 'situacao_nfse' => $this->statusLabel($this->val($infNfse, 'cStat')), 'finalidade_nfse' => $this->purposeLabel($this->val($infDps, 'IBSCBS', 'finNFSe')), @@ -353,6 +357,19 @@ private function formattedNbs(string $code): string . substr($code, 5, 2) . '.' . substr($code, 7, 2); } + /** + * The NT 008 municipal heading is not the service location. Its city and UF + * come only from the authorized NFS-e, and item 99 suppresses the line. + */ + private function headerMunicipality(string $city, string $uf, string $nationalTaxCode): string + { + if (str_starts_with($nationalTaxCode, '99') || ($city === '' && $uf === '')) { + return ''; + } + + return ($city !== '' ? $city : '-') . ' / ' . ($uf !== '' ? $uf : '-'); + } + private function cityWithUf(string $city, string $uf): string { return ($city !== '' && $uf !== '') ? $city . ' - ' . $uf : '-'; diff --git a/src/Danfse/HTMLTemplate.php b/src/Danfse/HTMLTemplate.php index 266ef91..f9ee91c 100644 --- a/src/Danfse/HTMLTemplate.php +++ b/src/Danfse/HTMLTemplate.php @@ -46,7 +46,9 @@ .header-table td { padding: 2pt 3pt 3pt; vertical-align: middle; } .logo-cell { width: 32%; text-align: left; } .title-cell { width: 42%; text-align: center; } - .municipality-cell { width: 26%; font-size: 5.7pt; text-align: left; } + .municipality-cell { width: 26%; font-family: "Microsoft Sans Serif", Arial, sans-serif; text-align: left; } + .municipality-name { font-size: 8pt; font-weight: normal; } + .municipality-environment { font-size: 6pt; font-weight: normal; } .qr-container { text-align: center; } .qr-notice { font-size: 5.6pt; line-height: 1.15; margin-top: 2pt; text-align: left; } .access-key { white-space: nowrap; font-size: 6.6pt; } @@ -97,9 +99,11 @@ logoDataUri): ?> Prefeitura
- Município:
- Ambiente Gerador:
- Tipo de Ambiente: + +
Município:
+ +
Ambiente Gerador:
+
Tipo de Ambiente:
diff --git a/tests/Unit/Danfse/DanfseTemplateTest.php b/tests/Unit/Danfse/DanfseTemplateTest.php index f0dfabe..22b82b5 100644 --- a/tests/Unit/Danfse/DanfseTemplateTest.php +++ b/tests/Unit/Danfse/DanfseTemplateTest.php @@ -183,6 +183,58 @@ public function testTomadorNifIsPreserved(): void self::assertSame('AB-123', $data['tomador']['cnpj_cpf']); } + + public function testHeaderMunicipalityFollowsAuthorizedXmlAndItem99Rule(): void + { + $nfse = $this->fixtureNfseData(); + $nfse['infNFSe']['xLocEmi'] = 'Município de Teste'; + $nfse['infNFSe']['emit']['enderNac']['UF'] = 'MG'; + + $template = new DanfseTemplate(); + $data = $template->buildData($nfse); + self::assertSame('Município de Teste / MG', $data['municipio_emissao']); + + $html = $template->render($nfse, new DanfseConfig()); + self::assertStringContainsString('Município: Município de Teste / MG', $html); + self::assertStringContainsString('.municipality-name { font-size: 8pt;', $html); + self::assertStringContainsString('.municipality-environment { font-size: 6pt;', $html); + + $nfse['infNFSe']['DPS']['infDPS']['serv']['cServ']['cTribNac'] = '990000'; + self::assertSame('', $template->buildData($nfse)['municipio_emissao']); + $html = $template->render($nfse, new DanfseConfig()); + self::assertStringNotContainsString('Município: Município de Teste', $html); + self::assertStringContainsString('Ambiente Gerador:', $html); + self::assertStringContainsString('Tipo de Ambiente:', $html); + } + + public function testHeaderDoesNotInventAbsentMunicipalityOrUf(): void + { + $nfse = $this->fixtureNfseData(); + unset($nfse['infNFSe']['xLocEmi'], $nfse['infNFSe']['emit']['enderNac']['UF']); + $template = new DanfseTemplate(); + self::assertSame('', $template->buildData($nfse)['municipio_emissao']); + self::assertStringNotContainsString('Município:', $template->render($nfse, new DanfseConfig())); + + $nfse['infNFSe']['xLocEmi'] = 'Cidade Sintética'; + self::assertSame('Cidade Sintética / -', $template->buildData($nfse)['municipio_emissao']); + + unset($nfse['infNFSe']['xLocEmi']); + $nfse['infNFSe']['emit']['enderNac']['UF'] = 'SP'; + self::assertSame('- / SP', $template->buildData($nfse)['municipio_emissao']); + } + + public function testHomologationHeaderRetainsWarningAndEnvironmentWhenMunicipalitySuppressed(): void + { + $nfse = $this->fixtureNfseData(); + $nfse['infNFSe']['DPS']['infDPS']['tpAmb'] = '2'; + $nfse['infNFSe']['DPS']['infDPS']['serv']['cServ']['cTribNac'] = '990000'; + + $html = (new DanfseTemplate())->render($nfse, new DanfseConfig()); + self::assertStringContainsString('NFS-e SEM VALIDADE JURÍDICA', $html); + self::assertStringContainsString('Tipo de Ambiente:', $html); + self::assertStringNotContainsString('Município: Niterói / RJ', $html); + } + public function testRenderProducesHtmlWithQrCodeAndAccessKey(): void { $html = (new DanfseTemplate())->render($this->fixtureNfseData(), new DanfseConfig());