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
20 changes: 17 additions & 3 deletions src/Danfse/DanfseTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
Expand All @@ -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')),
Expand Down Expand Up @@ -325,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
{
Expand All @@ -339,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 : '-');
}
Expand Down Expand Up @@ -426,6 +436,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 => '-',
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/Danfse/HTMLTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@
<td class="section-header"><span class="section-title">TRIBUTAÇÃO FEDERAL (EXCETO CBS)</span></td>
<td><span class="label">IRRF</span><span class="value"><?= $data['tributacao_federal']['irrf'] ?></span></td>
<td><span class="label">Contribuição Previdenciária - Retida</span><span class="value"><?= $data['tributacao_federal']['cp'] ?></span></td>
<td><span class="label">Contribuições Sociais - Retidas</span><span class="value"><?= $data['tributacao_federal']['csll'] ?></span></td>
<td><span class="label">Contribuições Sociais - Retidas</span><span class="value"><?= $data['tributacao_federal']['contribuicoes_sociais'] ?></span></td>
</tr>
<tr>
<td><span class="label">PIS - Débito Apuração Própria</span><span class="value"><?= $data['tributacao_federal']['pis'] ?></span></td>
Expand Down
5 changes: 4 additions & 1 deletion src/Dto/DpsData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
37 changes: 34 additions & 3 deletions tests/Unit/Danfse/DanfseGoldenReferenceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -155,25 +158,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();
Expand Down
21 changes: 18 additions & 3 deletions tests/Unit/Danfse/DanfseTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down Expand Up @@ -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();
Expand Down
17 changes: 17 additions & 0 deletions tests/Unit/Xml/XmlBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,23 @@ public function testBuildDpsIncludesFederalTaxationBlockWhenConfigured(): void
self::assertStringContainsString('<vRetCP>5.00</vRetCP>', $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('<tpRetPisCofins>4</tpRetPisCofins>', $xml);
self::assertStringContainsString('<vRetCSLL>1149.75</vRetCSLL>', $xml);
self::assertStringContainsString('<vRetIRRF>472.50</vRetIRRF>', $xml);
self::assertStringNotContainsString('<vPis>', $xml);
self::assertStringNotContainsString('<vCofins>', $xml);
}

public function testBuildDpsOmitsZeroValuedOptionalFederalRetentions(): void
{
$xml = $this->builder->buildDps($this->makeDps(
Expand Down
Loading