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 @@
IRRF= $data['tributacao_federal']['irrf'] ?> |
Contribuição Previdenciária - Retida= $data['tributacao_federal']['cp'] ?> |
- Contribuições Sociais - Retidas= $data['tributacao_federal']['csll'] ?> |
+ Contribuições Sociais - Retidas= $data['tributacao_federal']['contribuicoes_sociais'] ?> |
| PIS - Débito Apuração Própria= $data['tributacao_federal']['pis'] ?> |
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();