diff --git a/src/Danfse/DanfseTemplate.php b/src/Danfse/DanfseTemplate.php index f671dbf..eaf033d 100644 --- a/src/Danfse/DanfseTemplate.php +++ b/src/Danfse/DanfseTemplate.php @@ -103,7 +103,12 @@ public function buildData(array $nfse): array 'numero_dps' => $this->val($infDps, 'nDPS') ?: '-', 'serie_dps' => $this->val($infDps, 'serie') ?: '-', 'emissao_dps' => $this->formatter->dateTime($this->val($infDps, 'dhEmi')), - 'ambiente' => Ambiente::fromValue($this->val($infDps, 'tpAmb'))->value, + 'ambiente' => Ambiente::fromValue($this->val($infDps, 'tpAmb'))->value, + 'ambiente_gerador' => $this->val($infNfse, 'ambGer') ?: '-', + 'municipio_emissao' => $this->val($infNfse, 'xLocEmi') ?: '-', + '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')), 'emitente' => [ 'nome' => $this->val($emit, 'xNome') ?: '-', @@ -164,9 +169,12 @@ public function buildData(array $nfse): array 'tributacao_federal' => [ 'irrf' => $this->currencyOrDash($this->val($tribFed, 'vRetIRRF')), 'cp' => $this->currencyOrDash($this->val($tribFed, 'vRetCP')), - 'csll' => $this->currencyOrDash($this->val($tribFed, 'vRetCSLL')), + 'contribuicoes_sociais' => $this->currencyOrDash($this->val($tribFed, 'vRetCSLL')), 'pis' => $this->currencyOrDash($this->val($tribFed, 'piscofins', 'vPis')), 'cofins' => $this->currencyOrDash($this->val($tribFed, 'piscofins', 'vCofins')), + 'descricao_retencao' => $this->pisCofinsRetentionLabel( + $this->val($tribFed, 'piscofins', 'tpRetPisCofins'), + ), ], 'totais' => [ @@ -194,20 +202,39 @@ public function buildData(array $nfse): array 'municipais' => $this->percentOrDash($this->val($totTrib, 'pTotTribMun')), ], - 'ibs_cbs' => $ibsCbs === [] ? null : [ + 'ibs_cbs' => [ + 'cst_classificacao' => $this->joinedOrDash( + $this->val($infDps, 'IBSCBS', 'valores', 'trib', 'gIBSCBS', 'CST'), + $this->val($infDps, 'IBSCBS', 'valores', 'trib', 'gIBSCBS', 'cClassTrib'), + ), + 'indicador_operacao' => $this->val($infDps, 'IBSCBS', 'cIndOp') ?: '-', 'localidade_incidencia' => $this->val($ibsCbs, 'xLocalidadeIncid') ?: $this->val($ibsCbs, 'cLocalidadeIncid') ?: '-', + 'exclusoes_reducoes' => $this->sumCurrency( + $this->val($infNfse, 'valores', 'vISSQN'), + $this->val($tribFed, 'piscofins', 'vPis'), + $this->val($tribFed, 'piscofins', 'vCofins'), + ), 'base_calculo' => $this->currencyOrDash($this->val($ibsCbsValores, 'vBC')), 'aliquota_ibs_uf' => $this->percentOrDash($this->val($ibsCbsValores, 'uf', 'pIBSUF')), 'aliquota_ibs_municipal' => $this->percentOrDash($this->val($ibsCbsValores, 'mun', 'pIBSMun')), 'aliquota_cbs' => $this->percentOrDash($this->val($ibsCbsValores, 'fed', 'pCBS')), 'total_ibs' => $this->currencyOrDash($this->val($ibsCbsTotais, 'gIBS', 'vIBSTot')), 'total_cbs' => $this->currencyOrDash($this->val($ibsCbsTotais, 'gCBS', 'vCBS')), + 'total_ibs_cbs' => $ibsCbs === [] ? '-' : $this->sumCurrency( + $this->val($ibsCbsTotais, 'gIBS', 'vIBSTot'), + $this->val($ibsCbsTotais, 'gCBS', 'vCBS'), + ), 'valor_total_nfse' => $this->currencyOrDash($this->val($ibsCbsTotais, 'vTotNF')), ], - 'informacoes_complementares' => $this->val($serv, 'infoCompl', 'xInfComp'), + 'informacoes_complementares' => $this->complementaryInformation( + $this->val($serv, 'infoCompl', 'xInfComp'), + $this->percentOrDash($this->val($totTrib, 'pTotTribFed')), + $this->percentOrDash($this->val($totTrib, 'pTotTribEst')), + $this->percentOrDash($this->val($totTrib, 'pTotTribMun')), + ), ]; } @@ -318,6 +345,69 @@ private function sumCurrency(string ...$values): string return $hasValue ? $this->formatter->currency((string) $total) : '-'; } + private function emitterLabel(string $value): string + { + return match ($value) { + '1' => 'Prestador', + '2' => 'Tomador', + '3' => 'Intermediário', + default => '-', + }; + } + + private function statusLabel(string $value): string + { + return $value === '100' ? 'NFS-e Gerada' : ($value !== '' ? $value : '-'); + } + + private function purposeLabel(string $value): string + { + return match ($value) { + '0' => 'NFS-e regular', + '1' => 'NFS-e de crédito', + '2' => 'NFS-e de débito', + default => '-', + }; + } + + private function pisCofinsRetentionLabel(string $value): string + { + return match ($value) { + '0' => '0 - PIS/COFINS/CSLL Não Retidos', + '1' => '1 - PIS/COFINS Retidos', + '2' => '2 - PIS/COFINS Não Retidos', + '3' => '3 - PIS/COFINS/CSLL Retidos', + '4' => '4 - PIS/COFINS Retidos, CSLL Não Retido', + '5' => '5 - PIS Retido, COFINS/CSLL Não Retido', + default => '-', + }; + } + + private function joinedOrDash(string ...$values): string + { + if (array_filter($values, static fn (string $value): bool => $value !== '') === []) { + return '-'; + } + + return implode(' / ', array_map(static fn (string $value): string => $value !== '' ? $value : '-', $values)); + } + + private function complementaryInformation( + string $existing, + string $federal, + string $state, + string $municipal, + ): string { + $taxes = sprintf( + 'Totais aproximados dos Tributos cfe. Lei n° 12.741/2012: Federais: %s; Estaduais: %s; Municipais: %s;', + $federal, + $state, + $municipal, + ); + + return trim($existing) === '' ? $taxes : trim($existing) . "\n" . $taxes; + } + private function generateQrCode(string $chaveAcesso, Ambiente $ambiente): string { $renderer = new ImageRenderer( diff --git a/src/Danfse/HTMLTemplate.php b/src/Danfse/HTMLTemplate.php index a55bf11..35e531f 100644 --- a/src/Danfse/HTMLTemplate.php +++ b/src/Danfse/HTMLTemplate.php @@ -151,24 +151,12 @@ - - - - logoDataUri): ?> - - - - -
Prefeitura - name) ?>
- department): ?> - department) ?>
- - email): ?> - email) ?> - -
+ logoDataUri): ?> + Prefeitura
+ Município:
+ Ambiente Gerador:
+ Tipo de Ambiente: @@ -219,6 +207,22 @@ + + + + + + +
+ Emitente da NFS-e + + + Situação da NFS-e + + + Finalidade + +
@@ -479,26 +483,29 @@ - CSLL - Retida - + Contribuições Sociais - Retidas + - + PIS - Débito Apuração Própria - + COFINS - Débito Apuração Própria + + Descrição Contrib. Sociais - Retidas + + -
@@ -507,10 +514,24 @@ + + + + + +
+ CST / cClassTrib + + + Indicador de Operação + + Localidade de Incidência + Exclusões e Reduções da Base de Cálculo + +
Base de Cálculo @@ -523,6 +544,10 @@ Alíquota IBS Município + Reduções de Alíquota IBS/CBS + - +
@@ -544,7 +569,6 @@
-
@@ -577,38 +601,17 @@ Total das Retenções Federais - - PIS/COFINS - Débito Apur. Própria - - Valor Líquido da NFS-e - - -
- - -
- - - - - - - -
- TOTAIS APROXIMADOS DOS TRIBUTOS -
- Federais - - - Estaduais - + + Total do IBS/CBS + - Municipais - + + Valor Líquido da NFS-e + IBS/CBS +
diff --git a/src/Dto/DpsData.php b/src/Dto/DpsData.php index 9475ea7..ea4ab4b 100644 --- a/src/Dto/DpsData.php +++ b/src/Dto/DpsData.php @@ -33,6 +33,12 @@ public function __construct( /** Descrição do serviço prestado. */ public string $discriminacao, + /** Optional provider phone published in the DPS. */ + public string $prestadorTelefone = '', + + /** Optional provider e-mail published in the DPS. */ + public string $prestadorEmail = '', + /** Tipo de ambiente (1-Produção | 2-Homologação). */ public int $tipoAmbiente = 2, diff --git a/src/Xml/XmlBuilder.php b/src/Xml/XmlBuilder.php index ed6499d..9b53e7b 100644 --- a/src/Xml/XmlBuilder.php +++ b/src/Xml/XmlBuilder.php @@ -55,6 +55,15 @@ public function buildDps(DpsData $dps): string $prest = $doc->createElement('prest'); $cnpj = $doc->createElement('CNPJ', $dps->cnpjPrestador); $prest->appendChild($cnpj); + + if ($dps->prestadorTelefone !== '') { + $prest->appendChild($doc->createElement('fone', $dps->prestadorTelefone)); + } + + if ($dps->prestadorEmail !== '') { + $prest->appendChild($doc->createElement('email', htmlspecialchars($dps->prestadorEmail, ENT_XML1))); + } + $prest->appendChild($this->buildRegTrib($doc, $dps)); $infDps->appendChild($prest); diff --git a/tests/Unit/Danfse/DanfseGoldenReferenceTest.php b/tests/Unit/Danfse/DanfseGoldenReferenceTest.php index 55d7ec5..4cccbf0 100644 --- a/tests/Unit/Danfse/DanfseGoldenReferenceTest.php +++ b/tests/Unit/Danfse/DanfseGoldenReferenceTest.php @@ -104,7 +104,7 @@ public function testIbsCbsReferenceRendersOnlyAuthorizedTotals(): void self::assertStringContainsString('TRIBUTAÇÃO IBS / CBS', $html); } - public function testLegacyAuthorizedXmlWithoutIbsCbsKeepsLegacyLayout(): void + public function testAuthorizedXmlWithoutIbsCbsStillRendersRequiredV2Section(): void { $nfse = $this->fixtureData(); unset($nfse['infNFSe']['IBSCBS']); @@ -113,11 +113,32 @@ public function testLegacyAuthorizedXmlWithoutIbsCbsKeepsLegacyLayout(): void $data = $template->buildData($nfse); $html = $template->render($nfse, new DanfseConfig()); - self::assertNull($data['ibs_cbs']); - self::assertStringNotContainsString('TRIBUTAÇÃO IBS / CBS', $html); + self::assertIsArray($data['ibs_cbs']); + self::assertSame('-', $data['ibs_cbs']['base_calculo']); + self::assertSame('-', $data['ibs_cbs']['total_ibs_cbs']); + self::assertStringContainsString('TRIBUTAÇÃO IBS / CBS', $html); self::assertStringContainsString('DANFSe v2.0', $html); } + public function testFederalRetentionUsesOfficialV2Semantics(): void + { + $nfse = $this->fixtureData(); + $nfse['infNFSe']['DPS']['infDPS']['valores']['trib']['tribFed']['piscofins']['tpRetPisCofins'] = '4'; + $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('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('Descrição Contrib. Sociais - Retidas', $html); + } + public function testHomologationReferenceCarriesVisibleMarker(): void { $nfse = $this->fixtureData(); diff --git a/tests/Unit/Danfse/DanfseTemplateTest.php b/tests/Unit/Danfse/DanfseTemplateTest.php index 25d10cc..dfbc875 100644 --- a/tests/Unit/Danfse/DanfseTemplateTest.php +++ b/tests/Unit/Danfse/DanfseTemplateTest.php @@ -95,6 +95,11 @@ public function testRenderProducesHtmlWithQrCodeAndAccessKey(): void self::assertStringContainsString('', $html); self::assertStringContainsString('DANFSe v2.0', $html); + self::assertStringContainsString('Município:', $html); + self::assertStringContainsString('Ambiente Gerador:', $html); + self::assertStringContainsString('Tipo de Ambiente:', $html); + self::assertStringContainsString('Situação da NFS-e', $html); + self::assertStringContainsString('Finalidade', $html); self::assertStringContainsString('3303302112233450000195000000000000100000000001', $html); self::assertStringContainsString('data:image/svg+xml;base64,', $html); // Production environment: no homologação watermark @@ -133,11 +138,12 @@ public function testBuildDataMapsAuthorizedIbsCbsTotalsWithoutRecalculating(): v self::assertSame('R$ 1.515,75', $data['ibs_cbs']['valor_total_nfse']); } - public function testRenderShowsIbsCbsSectionOnlyWhenAuthorizedXmlContainsIt(): void + public function testRenderShowsRequiredIbsCbsSectionEvenWithoutAuthorizedIbsCbsGroup(): void { $withoutIbsCbs = (new DanfseTemplate())->render($this->fixtureNfseData(), new DanfseConfig()); - self::assertStringNotContainsString('TRIBUTAÇÃO IBS / CBS', $withoutIbsCbs); + self::assertStringContainsString('TRIBUTAÇÃO IBS / CBS', $withoutIbsCbs); + self::assertStringContainsString('Exclusões e Reduções da Base de Cálculo', $withoutIbsCbs); $nfseData = $this->fixtureNfseData(); $nfseData['infNFSe']['IBSCBS'] = [ diff --git a/tests/Unit/Xml/XmlBuilderTest.php b/tests/Unit/Xml/XmlBuilderTest.php index 48da978..2e1a196 100644 --- a/tests/Unit/Xml/XmlBuilderTest.php +++ b/tests/Unit/Xml/XmlBuilderTest.php @@ -87,6 +87,21 @@ public function testBuildDpsContainsCnpjPrestador(): void self::assertStringContainsString('11222333000181', $xml); } + public function testBuildDpsPublishesOptionalProviderContact(): void + { + $xml = $this->builder->buildDps($this->makeDps( + prestadorTelefone: '21969203370', + prestadorEmail: 'adm@librecode.coop', + )); + + $normalized = str_replace(["\n", ' '], '', $xml); + + self::assertStringContainsString( + '1122233300018121969203370adm@librecode.coop', + $normalized, + ); + } + public function testBuildDpsContainsMunicipioIbge(): void { $dps = $this->makeDps(municipioIbge: '3303302'); @@ -896,6 +911,8 @@ private function makeDps( string $valorServico = '1000.00', string $aliquota = '5.00', string $discriminacao = 'Consultoria em TI', + string $prestadorTelefone = '', + string $prestadorEmail = '', string $serie = '00001', string $numeroDps = '1', bool $issRetido = false, @@ -951,6 +968,8 @@ private function makeDps( valorServico: $valorServico, aliquota: $aliquota, discriminacao: $discriminacao, + prestadorTelefone: $prestadorTelefone, + prestadorEmail: $prestadorEmail, serie: $serie, numeroDps: $numeroDps, codigoTributacaoNacional: $codigoTributacaoNacional,