diff --git a/src/Danfse/DanfseTemplate.php b/src/Danfse/DanfseTemplate.php index d3f49bd..eaf4f88 100644 --- a/src/Danfse/DanfseTemplate.php +++ b/src/Danfse/DanfseTemplate.php @@ -80,6 +80,7 @@ public function buildData(array $nfse): array $enderEmit = $this->node($emit, 'enderNac'); $toma = $this->node($infDps, 'toma'); $endToma = $this->node($toma, 'end'); + $dest = $this->node($infDps, 'IBSCBS', 'dest'); $interm = $this->node($infDps, 'interm'); $endInterm = $this->node($interm, 'end'); $serv = $this->node($infDps, 'serv'); @@ -93,6 +94,26 @@ public function buildData(array $nfse): array $ibsCbsValores = $this->node($ibsCbs, 'valores'); $ibsCbsTotais = $this->node($ibsCbs, 'totCIBS'); + // The authorized DPS is the primary source for the provider. The + // authorized infNFSe/emit enriches omitted legacy address/name data. + $provider = $this->personDetails($prest); + $provider['nome'] = $this->val($prest, 'xNome') ?: $this->val($emit, 'xNome') ?: '-'; + $provider['cnpj_cpf'] = $this->formattedDocument($prest); + $provider['im'] = $this->val($prest, 'IM') ?: $this->val($emit, 'IM') ?: '-'; + $provider['telefone'] = $this->formatter->phone($this->val($prest, 'fone') ?: $this->val($emit, 'fone')); + $provider['email'] = strtolower($this->val($prest, 'email') ?: $this->val($emit, 'email')); + if ($this->node($prest, 'end') === []) { + $provider['endereco'] = $this->address($enderEmit); + $provider['municipio'] = $this->cityWithUf( + $this->val($infNfse, 'xLocEmi'), + $this->val($enderEmit, 'UF'), + ); + $provider['cep'] = $this->formatter->cep($this->val($enderEmit, 'CEP')); + $provider['ibge_cep'] = $this->ibgeCep($this->val($enderEmit, 'cMun'), $this->val($enderEmit, 'CEP')); + } + $provider['simples_nacional'] = OptanteSimplesNacional::labelFor($this->val($regTrib, 'opSimpNac')); + $provider['regime_sn'] = RegimeApuracaoTributariaSN::labelFor($this->val($regTrib, 'regApTribSN')); + $id = $this->val($infNfse, 'Id'); $chaveAcesso = str_starts_with($id, 'NFS') ? substr($id, 3) : $id; @@ -111,42 +132,23 @@ public function buildData(array $nfse): array 'situacao_nfse' => $this->statusLabel($this->val($infNfse, 'cStat')), 'finalidade_nfse' => $this->purposeLabel($this->val($infDps, 'IBSCBS', 'finNFSe')), - 'emitente' => [ - 'nome' => $this->val($emit, 'xNome') ?: '-', - 'cnpj_cpf' => $this->formattedDocument($emit), - 'im' => $this->val($prest, 'IM') ?: $this->val($emit, 'IM') ?: '-', - 'telefone' => $this->formatter->phone($this->val($prest, 'fone') ?: $this->val($emit, 'fone')), - 'email' => strtolower($this->val($prest, 'email') ?: $this->val($emit, 'email')), - 'endereco' => $this->address($enderEmit), - 'municipio' => $this->cityWithUf($this->val($infNfse, 'xLocEmi'), $this->val($enderEmit, 'UF')), - 'cep' => $this->formatter->cep($this->val($enderEmit, 'CEP')), - 'ibge_cep' => $this->ibgeCep($this->val($enderEmit, 'cMun'), $this->val($enderEmit, 'CEP')), - 'simples_nacional' => OptanteSimplesNacional::labelFor($this->val($regTrib, 'opSimpNac')), - 'regime_sn' => RegimeApuracaoTributariaSN::labelFor($this->val($regTrib, 'regApTribSN')), - ], - - 'tomador' => [ - 'nome' => $this->val($toma, 'xNome') ?: '-', - 'cnpj_cpf' => $this->formattedDocument($toma), - 'im' => $this->val($toma, 'IM') ?: '-', - 'telefone' => $this->formatter->phone($this->val($toma, 'fone')), - 'email' => strtolower($this->val($toma, 'email')), - 'endereco' => $this->address($endToma), - 'municipio' => $this->city($this->val($endToma, 'endNac', 'cMun')), - 'cep' => $this->formatter->cep($this->val($endToma, 'endNac', 'CEP')), - 'ibge_cep' => $this->ibgeCep($this->val($endToma, 'endNac', 'cMun'), $this->val($endToma, 'endNac', 'CEP')), - ], - - 'intermediario' => $interm === [] ? null : [ - 'nome' => $this->val($interm, 'xNome') ?: '-', - 'cnpj_cpf' => $this->formattedDocument($interm), - 'im' => $this->val($interm, 'IMPrestMun') ?: '-', - 'telefone' => $this->formatter->phone($this->val($interm, 'fone')), - 'email' => strtolower($this->val($interm, 'email')), - 'endereco' => $this->address($endInterm), - 'municipio' => $this->city($this->val($endInterm, 'endNac', 'cMun')), - 'cep' => $this->formatter->cep($this->val($endInterm, 'endNac', 'CEP')), - ], + 'emitente' => $provider, + 'tomador' => $toma === [] ? null : $this->personDetails($toma), + 'destinatario' => $dest === [] ? null : array_merge( + $this->personDetails($dest), + ['mesmo_tomador' => $this->samePerson($dest, $toma)], + ), + // The official XSD's indDest=0 is an explicit declaration + // that dest=toma, even if there is no separate dest group. + // When indDest=1, never override that with a name or inferred + // equality. Only legacy XML lacking indDest uses identity + // comparison as a presentation fallback. + 'destinatario_mesmo_tomador' => match ($this->val($infDps, 'IBSCBS', 'indDest')) { + '0' => true, + '1' => false, + default => $dest !== [] && $this->samePerson($dest, $toma), + }, + 'intermediario' => $interm === [] ? null : $this->personDetails($interm), 'servico' => [ 'codigo_trib_nacional' => $this->formatter->codTribNacional($this->val($cServ, 'cTribNac')), @@ -250,6 +252,69 @@ public function buildData(array $nfse): array ]; } + /** + * Presentation fields for people in the authorized DPS, keeping national + * IBGE/CEP and foreign city/postal codes distinct. Missing values stay + * missing: no municipality, state or identity is guessed. + * + * @param array $person + * @return array + */ + private function personDetails(array $person): array + { + $address = $this->node($person, 'end'); + $national = $this->node($address, 'endNac'); + $foreign = $this->node($address, 'endExt'); + + $municipio = '-'; + $ibgeCep = '-'; + $cep = '-'; + + if ($national !== []) { + $municipio = $this->city($this->val($national, 'cMun')); + $ibgeCep = $this->ibgeCep($this->val($national, 'cMun'), $this->val($national, 'CEP')); + $cep = $this->formatter->cep($this->val($national, 'CEP')); + } elseif ($foreign !== []) { + $city = $this->val($foreign, 'xCidade'); + $region = $this->val($foreign, 'xEstProvReg'); + $postal = $this->val($foreign, 'cEndPost'); + $municipio = $city !== '' ? $city . ($region !== '' ? ' - ' . $region : '') : '-'; + $ibgeCep = '- / ' . ($postal !== '' ? $postal : '-'); + $cep = $postal !== '' ? $postal : '-'; + } + + return [ + 'nome' => $this->val($person, 'xNome') ?: '-', + 'cnpj_cpf' => $this->formattedDocument($person), + 'im' => $this->val($person, 'IM') ?: $this->val($person, 'IMPrestMun') ?: '-', + 'telefone' => $this->formatter->phone($this->val($person, 'fone')), + 'email' => strtolower($this->val($person, 'email')), + 'endereco' => $this->address($address), + 'municipio' => $municipio, + 'ibge_cep' => $ibgeCep, + 'cep' => $cep, + ]; + } + + /** + * Equal verified identifiers permit the NT008 optional same-taker label. + * Names or addresses alone do not establish fiscal identity. + * + * @param array $left + * @param array $right + */ + private function samePerson(array $left, array $right): bool + { + foreach (['CNPJ', 'CPF', 'NIF'] as $key) { + $id = $this->val($left, $key); + if ($id !== '' && $id === $this->val($right, $key)) { + return true; + } + } + + return false; + } + /** * Navigate to a nested sub-array, returning [] when any step is missing. * diff --git a/src/Danfse/HTMLTemplate.php b/src/Danfse/HTMLTemplate.php index 266ef91..a7617ab 100644 --- a/src/Danfse/HTMLTemplate.php +++ b/src/Danfse/HTMLTemplate.php @@ -167,7 +167,7 @@ -
+
- - + + + - - - + +
@@ -187,28 +187,13 @@
- Nome / Nome Empresarial - - - E-mail - - Nome / Nome EmpresarialMunicípio / Sigla UFCódigo IBGE / CEP
- Endereço - - - Município - - - Código IBGE / CEP - - EndereçoE-mail
@@ -224,7 +209,10 @@ -
+ +
TOMADOR/ADQUIRENTE DA OPERAÇÃO NÃO IDENTIFICADO NA NFS-e
+ +
- + + - - - + + + +
@@ -248,38 +236,54 @@ Nome / Nome Empresarial - E-mail - - Município / Sigla UFCódigo IBGE / CEP
- Endereço - - - Município - - - Código IBGE / CEP - - EndereçoE-mail
+
+ + + + +
O DESTINATÁRIO É O PRÓPRIO TOMADOR/ADQUIRENTE DA OPERAÇÃO
+ +
DESTINATÁRIO DA OPERAÇÃO NÃO IDENTIFICADO NA NFS-e
+ +
+ + + + + + + + + + + + + + +
DESTINATÁRIO DA OPERAÇÃOCNPJ / CPF / NIFTelefone
Nome / Nome EmpresarialMunicípio / Sigla UFCódigo IBGE / CEP
EndereçoE-mail
+ -
+
- + + - - - + +
INTERMEDIÁRIO DO SERVIÇO - CNPJ / CPF + CNPJ / CPF / NIF @@ -296,31 +300,17 @@ Nome / Nome Empresarial - E-mail - - Município / Sigla UFCódigo IBGE / CEP
- Endereço - - - Município - - - CEP - - EndereçoE-mail
-
DESTINATÁRIO DA OPERAÇÃO NÃO IDENTIFICADO NA NFS-e -
INTERMEDIÁRIO DA OPERAÇÃO NÃO IDENTIFICADO NA NFS-e
-
+
INTERMEDIÁRIO DA OPERAÇÃO NÃO IDENTIFICADO NA NFS-e
diff --git a/tests/Unit/Danfse/DanfseTemplateTest.php b/tests/Unit/Danfse/DanfseTemplateTest.php index f0dfabe..55e8174 100644 --- a/tests/Unit/Danfse/DanfseTemplateTest.php +++ b/tests/Unit/Danfse/DanfseTemplateTest.php @@ -162,6 +162,136 @@ public function testTomadorAddressKeepsComplementAndOfficialMunicipalityCode(): self::assertSame('3550308 / 04578-000', $data['tomador']['ibge_cep']); } + public function testRecipientIsIndependentOfIntermediaryAndUsesAuthorizedDps(): void + { + $nfse = $this->fixtureNfseData(); + $dps = & $nfse['infNFSe']['DPS']['infDPS']; + unset($dps['interm']); + $dps['IBSCBS']['dest'] = [ + 'NIF' => 'PT-TEST-998877', + 'xNome' => 'DESTINATARIO SINTETICO', + 'end' => [ + 'endExt' => [ + 'cPais' => 'PT', + 'cEndPost' => '0000-001', + 'xCidade' => 'Cidade Ficticia', + 'xEstProvReg' => 'Distrito Ficticio', + ], + 'xLgr' => 'Rua Exemplo', + 'nro' => '1', + 'xBairro' => 'Bairro Modelo', + ], + 'email' => 'recipient@example.org', + ]; + + $template = new DanfseTemplate(); + $data = $template->buildData($nfse); + self::assertNull($data['intermediario']); + self::assertSame('PT-TEST-998877', $data['destinatario']['cnpj_cpf']); + self::assertSame('Cidade Ficticia - Distrito Ficticio', $data['destinatario']['municipio']); + self::assertSame('- / 0000-001', $data['destinatario']['ibge_cep']); + self::assertFalse($data['destinatario']['mesmo_tomador']); + + $html = $template->render($nfse, new DanfseConfig()); + self::assertStringContainsString('DESTINATÁRIO DA OPERAÇÃO', $html); + self::assertStringContainsString('recipient@example.org', $html); + self::assertStringContainsString('INTERMEDIÁRIO DA OPERAÇÃO NÃO IDENTIFICADO', $html); + self::assertStringNotContainsString('DESTINATÁRIO DA OPERAÇÃO NÃO IDENTIFICADO', $html); + } + + public function testSameRecipientAndTakerUseOfficialConciseMessage(): void + { + $nfse = $this->fixtureNfseData(); + $dps = & $nfse['infNFSe']['DPS']['infDPS']; + $dps['IBSCBS']['dest'] = [ + 'CNPJ' => $dps['toma']['CNPJ'], + 'xNome' => 'DESTINATARIO SINTETICO', + ]; + + $template = new DanfseTemplate(); + self::assertTrue($template->buildData($nfse)['destinatario']['mesmo_tomador']); + $html = $template->render($nfse, new DanfseConfig()); + self::assertStringContainsString('O DESTINATÁRIO É O PRÓPRIO TOMADOR/ADQUIRENTE DA OPERAÇÃO', $html); + self::assertStringNotContainsString('class="bordered-section party-destinatario"', $html); + } + + public function testOfficialIndDestZeroMeansSameTakerEvenWithoutDestGroup(): void + { + $nfse = $this->fixtureNfseData(); + $dps = & $nfse['infNFSe']['DPS']['infDPS']; + $dps['IBSCBS']['indDest'] = '0'; + unset($dps['IBSCBS']['dest']); + + $template = new DanfseTemplate(); + $data = $template->buildData($nfse); + self::assertNull($data['destinatario']); + self::assertTrue($data['destinatario_mesmo_tomador']); + + $html = $template->render($nfse, new DanfseConfig()); + self::assertStringContainsString('O DESTINATÁRIO É O PRÓPRIO TOMADOR/ADQUIRENTE DA OPERAÇÃO', $html); + self::assertStringNotContainsString('DESTINATÁRIO DA OPERAÇÃO NÃO IDENTIFICADO', $html); + } + + public function testOfficialIndDestOnePreventsIncorrectSameTakerSuppression(): void + { + $nfse = $this->fixtureNfseData(); + $dps = & $nfse['infNFSe']['DPS']['infDPS']; + $dps['IBSCBS']['indDest'] = '1'; + $dps['IBSCBS']['dest'] = [ + 'CNPJ' => $dps['toma']['CNPJ'], + 'xNome' => 'DESTINATARIO AUTORIZADO DISTINTO', + ]; + + $template = new DanfseTemplate(); + self::assertFalse($template->buildData($nfse)['destinatario_mesmo_tomador']); + $html = $template->render($nfse, new DanfseConfig()); + self::assertStringContainsString('class="bordered-section party-destinatario"', $html); + self::assertStringNotContainsString('O DESTINATÁRIO É O PRÓPRIO TOMADOR', $html); + } + + public function testMissingTakerAndRecipientHaveSeparateCondensedBlocks(): void + { + $nfse = $this->fixtureNfseData(); + unset($nfse['infNFSe']['DPS']['infDPS']['toma']); + $template = new DanfseTemplate(); + self::assertNull($template->buildData($nfse)['tomador']); + self::assertNull($template->buildData($nfse)['destinatario']); + $html = $template->render($nfse, new DanfseConfig()); + self::assertStringContainsString('TOMADOR/ADQUIRENTE DA OPERAÇÃO NÃO IDENTIFICADO NA NFS-e', $html); + self::assertStringContainsString('DESTINATÁRIO DA OPERAÇÃO NÃO IDENTIFICADO NA NFS-e', $html); + self::assertStringContainsString('class="bordered-section party-intermediario"', $html); + } + + public function testProviderPrefersAuthorizedDpsAddressAndPreservesLeadingZeroCep(): void + { + $nfse = $this->fixtureNfseData(); + $provider = & $nfse['infNFSe']['DPS']['infDPS']['prest']; + $provider['xNome'] = 'FORNECEDOR SINTETICO DA DPS'; + $provider['end'] = [ + 'endNac' => ['cMun' => '3550308', 'CEP' => '01001000'], + 'xLgr' => 'Rua da Amostra', + 'nro' => '10', + 'xBairro' => 'Centro', + ]; + + $data = (new DanfseTemplate())->buildData($nfse); + self::assertSame('FORNECEDOR SINTETICO DA DPS', $data['emitente']['nome']); + self::assertSame('São Paulo - SP', $data['emitente']['municipio']); + self::assertSame('3550308 / 01001-000', $data['emitente']['ibge_cep']); + self::assertStringContainsString('Rua da Amostra', $data['emitente']['endereco']); + } + + public function testIntermediaryKeepsIbgeAndCepInSameCell(): void + { + $nfse = $this->fixtureNfseData(); + $template = new DanfseTemplate(); + $data = $template->buildData($nfse); + self::assertSame('3518800 / 07095-130', $data['intermediario']['ibge_cep']); + $html = $template->render($nfse, new DanfseConfig()); + self::assertStringContainsString('class="bordered-section party-intermediario"', $html); + self::assertStringContainsString('3518800 / 07095-130', $html); + } + public function testIntermediarioIsNullWhenAbsent(): void { $nfseData = $this->fixtureNfseData();