From de10d4d2989309d017a6b3b1a679cecb8b62689c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:09:53 +0000 Subject: [PATCH 1/2] fix(nfse): align consolidated social retentions with NT 007/008 Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/Danfse/DanfseTemplate.php | 6 ++++ src/Danfse/HTMLTemplate.php | 2 +- src/Dto/DpsData.php | 5 ++- .../Unit/Danfse/DanfseGoldenReferenceTest.php | 34 +++++++++++++++++-- tests/Unit/Xml/XmlBuilderTest.php | 17 ++++++++++ 5 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/Danfse/DanfseTemplate.php b/src/Danfse/DanfseTemplate.php index d3f49bd..cc0764e 100644 --- a/src/Danfse/DanfseTemplate.php +++ b/src/Danfse/DanfseTemplate.php @@ -181,6 +181,8 @@ public function buildData(array $nfse): array 'tributacao_federal' => [ 'irrf' => $this->currencyOrDash($this->val($tribFed, 'vRetIRRF')), 'cp' => $this->currencyOrDash($this->val($tribFed, 'vRetCP')), + 'contribuicoes_sociais' => $this->currencyOrDash($this->val($tribFed, 'vRetCSLL')), + // Alias de compatibilidade para consumidores de buildData(). 'csll' => $this->currencyOrDash($this->val($tribFed, 'vRetCSLL')), 'pis' => $this->currencyOrDash($this->val($tribFed, 'piscofins', 'vPis')), 'cofins' => $this->currencyOrDash($this->val($tribFed, 'piscofins', 'vCofins')), @@ -426,6 +428,10 @@ private function pisCofinsRetentionLabel(string $value): string '3' => '3 - PIS/COFINS/CSLL Retidos', '4' => '4 - PIS/COFINS Retidos, CSLL Não Retido', '5' => '5 - PIS Retido, COFINS/CSLL Não Retido', + '6' => '6 - COFINS Retido, PIS/CSLL Não Retido', + '7' => '7 - PIS Não Retido, COFINS/CSLL Retidos', + '8' => '8 - PIS/COFINS Não Retidos, CSLL Retido', + '9' => '9 - COFINS Não Retido, PIS/CSLL Retidos', default => '-', }; } diff --git a/src/Danfse/HTMLTemplate.php b/src/Danfse/HTMLTemplate.php index 266ef91..a666710 100644 --- a/src/Danfse/HTMLTemplate.php +++ b/src/Danfse/HTMLTemplate.php @@ -364,7 +364,7 @@ TRIBUTAÇÃO FEDERAL (EXCETO CBS) IRRF Contribuição Previdenciária - Retida - Contribuições Sociais - Retidas + Contribuições Sociais - Retidas PIS - Débito Apuração Própria diff --git a/src/Dto/DpsData.php b/src/Dto/DpsData.php index 0835054..842d5a2 100644 --- a/src/Dto/DpsData.php +++ b/src/Dto/DpsData.php @@ -179,7 +179,10 @@ public function __construct( /** Valor do IRRF. */ public string $federalValorIrrf = '', - /** Valor da CSLL retida. */ + /** + * Soma das retenções efetivas de PIS, COFINS e CSLL (NT 007/2026). + * O nome legado acompanha a tag XML vRetCSLL. + */ public string $federalValorCsll = '', /** Valor da contribuição previdenciária retida. */ diff --git a/tests/Unit/Danfse/DanfseGoldenReferenceTest.php b/tests/Unit/Danfse/DanfseGoldenReferenceTest.php index bfabaac..1936cd2 100644 --- a/tests/Unit/Danfse/DanfseGoldenReferenceTest.php +++ b/tests/Unit/Danfse/DanfseGoldenReferenceTest.php @@ -155,25 +155,53 @@ public function testAuthorizedXmlWithoutIbsCbsStillRendersRequiredV2Section(): v self::assertStringContainsString('DANFSe v2.0', $html); } - public function testFederalRetentionKeepsCsllSeparateFromPisAndCofins(): void + public function testFederalRetentionDisplaysConsolidatedPisCofinsWithoutCsll(): void { $nfse = $this->fixtureData(); $nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['piscofins']['tpRetPisCofins'] = '4'; - unset($nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['vRetCSLL']); + $nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['vRetCSLL'] = '1149.75'; $template = new DanfseTemplate(); $data = $template->buildData($nfse); $html = $template->render($nfse, new DanfseConfig()); - self::assertSame('-', $data['tributacao_federal']['csll']); + self::assertSame('R$ 1.149,75', $data['tributacao_federal']['contribuicoes_sociais']); self::assertSame( '4 - PIS/COFINS Retidos, CSLL Não Retido', $data['tributacao_federal']['descricao_retencao'], ); self::assertStringContainsString('Contribuições Sociais - Retidas', $html); + self::assertStringContainsString('R$ 1.149,75', $html); self::assertStringContainsString('Tipo de retenção PIS/COFINS/CSLL', $html); } + public function testNonRetainedSocialContributionsDoNotDisplayAnInventedAmount(): void + { + $nfse = $this->fixtureData(); + $nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['piscofins']['tpRetPisCofins'] = '0'; + unset($nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['vRetCSLL']); + + $data = (new DanfseTemplate())->buildData($nfse); + + self::assertSame('-', $data['tributacao_federal']['contribuicoes_sociais']); + } + + public function testSocialRetentionLabelsSupportAllNt007Codes(): void + { + $nfse = $this->fixtureData(); + $labels = [ + '6' => '6 - COFINS Retido, PIS/CSLL Não Retido', + '7' => '7 - PIS Não Retido, COFINS/CSLL Retidos', + '8' => '8 - PIS/COFINS Não Retidos, CSLL Retido', + '9' => '9 - COFINS Não Retido, PIS/CSLL Retidos', + ]; + + foreach ($labels as $code => $label) { + $nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['piscofins']['tpRetPisCofins'] = $code; + self::assertSame($label, (new DanfseTemplate())->buildData($nfse)['tributacao_federal']['descricao_retencao']); + } + } + public function testHomologationReferenceCarriesVisibleMarker(): void { $nfse = $this->fixtureData(); diff --git a/tests/Unit/Xml/XmlBuilderTest.php b/tests/Unit/Xml/XmlBuilderTest.php index 255f84b..dbc78c6 100644 --- a/tests/Unit/Xml/XmlBuilderTest.php +++ b/tests/Unit/Xml/XmlBuilderTest.php @@ -471,6 +471,23 @@ public function testBuildDpsIncludesFederalTaxationBlockWhenConfigured(): void self::assertStringContainsString('5.00', $xml); } + public function testBuildDpsKeepsConsolidatedPisCofinsRetentionsWhenCsllItselfIsNotRetained(): void + { + $xml = $this->builder->buildDps($this->makeDps( + federalPiscofinsSituacaoTributaria: '1', + federalPiscofinsTipoRetencao: '4', + federalPiscofinsBaseCalculo: '31500.00', + federalValorIrrf: '472.50', + federalValorCsll: '1149.75', + )); + + self::assertStringContainsString('4', $xml); + self::assertStringContainsString('1149.75', $xml); + self::assertStringContainsString('472.50', $xml); + self::assertStringNotContainsString('', $xml); + self::assertStringNotContainsString('', $xml); + } + public function testBuildDpsOmitsZeroValuedOptionalFederalRetentions(): void { $xml = $this->builder->buildDps($this->makeDps( From 8ed55aabad3077684dc5ab8fb1a41890065186c2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:18:37 +0000 Subject: [PATCH 2/2] fix(danfse): display BR for domestic IBGE locations per NT008 Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/Danfse/DanfseTemplate.php | 14 ++++++++++--- .../Unit/Danfse/DanfseGoldenReferenceTest.php | 3 +++ tests/Unit/Danfse/DanfseTemplateTest.php | 21 ++++++++++++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Danfse/DanfseTemplate.php b/src/Danfse/DanfseTemplate.php index cc0764e..c5492f7 100644 --- a/src/Danfse/DanfseTemplate.php +++ b/src/Danfse/DanfseTemplate.php @@ -168,7 +168,7 @@ public function buildData(array $nfse): array 'municipio_incidencia' => $this->locationWithUfAndCountry( $this->val($infNfse, 'xLocIncid'), $this->val($infNfse, 'cLocIncid'), - '', + $this->val($tribMun, 'cPaisResult'), ), 'regime_especial' => RegimeEspecialTributacao::labelFor($this->val($regTrib, 'regEspTrib')), 'valor_servico' => $this->formatter->currency($this->val($valores, 'vServPrest', 'vServ')), @@ -327,8 +327,11 @@ private function address(array $endereco): string } /** - * Show the municipality, UF and an optional country only when present in - * the authorized XML. The UF is resolved from the official IBGE code. + * Display ISO country BR for a recognized Brazilian IBGE municipality. + * The NT 008/2026 v1.02 DANFSe table explicitly uses BR as an example; + * this is a presentation inference from the authorized IBGE municipality, + * NOT a cPaisPrestacao XML value (which E0304 forbids for BR). + * For foreign operations, preserve the explicit XML country instead. */ private function locationWithUfAndCountry(string $city, string $ibge, string $country): string { @@ -341,6 +344,11 @@ private function locationWithUfAndCountry(string $city, string $ibge, string $co } } + $country = strtoupper(trim($country)); + if ($country === '' && $uf !== '') { + $country = 'BR'; + } + return ($city !== '' ? $city : '-') . ' / ' . ($uf !== '' ? $uf : '-') . ' / ' . ($country !== '' ? $country : '-'); } diff --git a/tests/Unit/Danfse/DanfseGoldenReferenceTest.php b/tests/Unit/Danfse/DanfseGoldenReferenceTest.php index 1936cd2..df514c3 100644 --- a/tests/Unit/Danfse/DanfseGoldenReferenceTest.php +++ b/tests/Unit/Danfse/DanfseGoldenReferenceTest.php @@ -38,6 +38,9 @@ public function testNationalPortalStyleUsesCompactGridsAndFooter(): void self::assertStringContainsString('TRIBUTAÇÃO MUNICIPAL (ISSQN)', $html); self::assertStringContainsString('BC ISSQN', $html); self::assertStringContainsString('VALOR LÍQUIDO DA NFS-e', $html); + // A Brazilian municipality authorizes deriving BR for display only; + // cPaisPrestacao remains absent from the issued domestic DPS XML. + self::assertStringContainsString('Niterói / RJ / BR', $html); self::assertStringContainsString('Nº NFS-e / CHAVE NFS-e', $html); } diff --git a/tests/Unit/Danfse/DanfseTemplateTest.php b/tests/Unit/Danfse/DanfseTemplateTest.php index f0dfabe..eb3d181 100644 --- a/tests/Unit/Danfse/DanfseTemplateTest.php +++ b/tests/Unit/Danfse/DanfseTemplateTest.php @@ -61,7 +61,7 @@ public function testBuildDataMapsKeyFields(): void self::assertSame('Operação Tributável', $data['tributacao_municipal']['tributacao_issqn']); self::assertSame('Retido pelo Tomador', $data['tributacao_municipal']['retencao_issqn']); self::assertSame('Sociedade de Profissionais', $data['tributacao_municipal']['regime_especial']); - self::assertSame('Niterói / RJ / -', $data['tributacao_municipal']['municipio_incidencia']); + self::assertSame('Niterói / RJ / BR', $data['tributacao_municipal']['municipio_incidencia']); // Totais self::assertSame('R$ 1.500,00', $data['totais']['valor_servico']); @@ -89,11 +89,26 @@ public function testOfficialLocationsAndProviderContactUseOnlyDpsData(): void self::assertSame('12345678', $data['emitente']['im']); self::assertSame('(00) 90000-0000', $data['emitente']['telefone']); self::assertSame('fiscal@example.org', $data['emitente']['email']); - self::assertSame('Curitiba / PR / -', $data['servico']['local_prestacao']); - self::assertSame('Curitiba / PR / -', $data['tributacao_municipal']['municipio_incidencia']); + self::assertSame('Curitiba / PR / BR', $data['servico']['local_prestacao']); + self::assertSame('Curitiba / PR / BR', $data['tributacao_municipal']['municipio_incidencia']); self::assertSame('0.0000.00.00', $data['servico']['codigo_nbs']); } + public function testAuthorizedForeignCountryOverridesBrazilianFallback(): void + { + $nfse = $this->fixtureNfseData(); + $nfse['infNFSe']['xLocPrestacao'] = ''; + $nfse['infNFSe']['DPS']['infDPS']['serv']['locPrest'] = ['cPaisPrestacao' => 'PT']; + $nfse['infNFSe']['xLocIncid'] = ''; + $nfse['infNFSe']['cLocIncid'] = ''; + $nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribMun']['cPaisResult'] = 'PT'; + + $data = (new DanfseTemplate())->buildData($nfse); + + self::assertSame('- / - / PT', $data['servico']['local_prestacao']); + self::assertSame('- / - / PT', $data['tributacao_municipal']['municipio_incidencia']); + } + public function testUnknownMunicipalityDoesNotInventAStateOrCountry(): void { $nfse = $this->fixtureNfseData();