Skip to content

IbsCbsUtil: item com cClassTrib inválido/nulo herda silenciosamente a classificação do item anterior #349

Description

@scastrodev

Versão: 4.1.1 (bug também presente no master atual)
Classe: br.com.swconsultoria.nfe.util.IbsCbsUtil
Severidade: alta — gera NF-e/NFC-e autorizada com tributação de IBS/CBS incorreta, sem erro nem aviso

Resumo

IbsCbsUtil mantém a classificação tributária do último item processado em campos de instância
(cstIbsCbs, classTribIbsCbs). Quando montaImpostosDet é chamado com um cClassTrib nulo, vazio ou
inexistente na tabela, esses campos não são limpos — e a validação verifica o campo, não o argumento.

O resultado é que, a partir do segundo item de um documento, um cClassTrib inválido não lança exceção:
o item recebe silenciosamente o CST e o cClassTrib do item anterior, e o documento é autorizado
com o imposto errado naquele item.

O comportamento depende da posição do item no documento: o mesmo cClassTrib inválido lança exceção se
estiver no primeiro item e passa despercebido em qualquer outra posição.

Reprodução

ObjectMapper mapper = new ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
List<CstDTO> tabela = mapper.readValue(
        getClass().getResourceAsStream("/classificacoes-tributarias.json"),
        new TypeReference<List<CstDTO>>() {});

// Uma IbsCbsUtil para o documento inteiro (é como ela precisa ser usada:
// mapTotais acumula por item e alimenta preencheTotaisIbsCsb()).
IbsCbsUtil util = new IbsCbsUtil(tabela, DocumentoEnum.NFCE);

TTribNFe item1 = util.montaImpostosDet("000001", det("10.00")); // válido
TTribNFe item2 = util.montaImpostosDet(null,     det("10.00")); // INVÁLIDO — deveria lançar
TTribNFe item3 = util.montaImpostosDet("200013", det("10.00")); // válido

// helper
static TNFe.InfNFe.Det det(String vProd) {
    TNFe.InfNFe.Det d = new TNFe.InfNFe.Det();
    TNFe.InfNFe.Det.Prod p = new TNFe.InfNFe.Det.Prod();
    p.setVProd(vProd);
    p.setQCom("1.0000");
    d.setProd(p);
    d.setImposto(new TNFe.InfNFe.Det.Imposto());
    return d;
}

Resultado obtido

item1 [000001] -> CST=000  cClassTrib=000001  vBC=10.00  vCBS=0.09
item2 [null]   -> CST=000  cClassTrib=000001  vBC=10.00  vCBS=0.09   <-- herdou do item1, sem exceção
item3 [200013] -> CST=200  cClassTrib=200013  vBC=10.00  vCBS=0.00
TOTAIS: vBCIBSCBS=30.00  vCBS=0.18

O item 2 não tem classificação tributária, mas saiu no XML como CST 000 / cClassTrib 000001
(tributação integral) — a classificação do item 1 — e foi somado normalmente aos totalizadores.

Resultado esperado

NfeException: CClassTrib inválido ou não encontrado: null, independentemente da posição do item.

Contraprova

Com uma IbsCbsUtil nova por chamada, a exceção é lançada corretamente. O bug só se manifesta quando a
instância é reutilizada entre itens — que é exatamente o uso previsto, já que mapTotais acumula por item
para alimentar preencheTotaisIbsCsb().

Causa

Dois defeitos que se compõem.

1. filtraCClasstrib não limpa o estado quando a busca falha (IbsCbsUtil.java:356-368):

private void filtraCClasstrib(String cclassTrib, String cclassTribRegular) {
    buscarCstEClassificacao(cclassTrib).ifPresent(entry -> {
        cstIbsCbs = entry.getKey();
        classTribIbsCbs = entry.getValue();
    });                                    // <-- ifPresent sem else: campos mantêm o valor anterior

    if (cclassTribRegular != null && !cclassTribRegular.isEmpty()) {
        buscarCstEClassificacao(cclassTribRegular).ifPresent(entry -> {
            cstIbsCbsTribRegular = entry.getKey();
            classTribIbsCbsTribRegular = entry.getValue();
        });                                // <-- mesmo problema
    }
}

2. validaClassTrib valida o campo, não o argumento (IbsCbsUtil.java:378-381):

private void validaClassTrib(String cclassTrib) throws NfeException {
    if (classTribIbsCbs == null) {         // <-- campo, que pode ser do item anterior
        throw new NfeException("CClassTrib inválido ou não encontrado: " + cclassTrib);
    }
    ...
}

Com a instância recém-construída, classTribIbsCbs é null e a exceção sai. Depois de um item válido, o
campo carrega a classificação daquele item e satisfaz a validação de qualquer item subsequente, válido ou não.

montaImpostosDet (IbsCbsUtil.java:207-214) então monta a tag a partir dos campos:

filtraCClasstrib(cclassTrib, cclassTribRegular);
validaClassTrib(cclassTrib);
calcularBaseCalculoIBSCBS(det);

TTribNFe ibsCbs = new TTribNFe();
ibsCbs.setCST(cstIbsCbs.getCst());                      // <-- do item anterior
ibsCbs.setCClassTrib(classTribIbsCbs.getCClassTrib());  // <-- do item anterior

Correção sugerida

Fazer filtraCClasstrib atribuir ou limpar, em vez de só atribuir quando encontra:

private void filtraCClasstrib(String cclassTrib, String cclassTribRegular) {
    Optional<AbstractMap.SimpleEntry<CstDTO, ClassificacaoTributariaDTO>> achado =
            buscarCstEClassificacao(cclassTrib);
    cstIbsCbs       = achado.map(AbstractMap.SimpleEntry::getKey).orElse(null);
    classTribIbsCbs = achado.map(AbstractMap.SimpleEntry::getValue).orElse(null);

    cstIbsCbsTribRegular       = null;
    classTribIbsCbsTribRegular = null;
    if (cclassTribRegular != null && !cclassTribRegular.isEmpty()) {
        Optional<AbstractMap.SimpleEntry<CstDTO, ClassificacaoTributariaDTO>> achadoRegular =
                buscarCstEClassificacao(cclassTribRegular);
        cstIbsCbsTribRegular       = achadoRegular.map(AbstractMap.SimpleEntry::getKey).orElse(null);
        classTribIbsCbsTribRegular = achadoRegular.map(AbstractMap.SimpleEntry::getValue).orElse(null);
    }
}

Isso preserva a validação existente (classTribIbsCbs == null passa a significar "o cClassTrib desta
chamada não foi encontrado") e mantém coerente a checagem de IndTribRegular em validaClassTrib, que hoje
também pode ser satisfeita por um classTribIbsCbsTribRegular remanescente de um item anterior.

Nota: buscarCstEClassificacao (IbsCbsUtil.java:370-376) faz
entry.getValue().getCClassTrib().equals(cclassTrib), então um cClassTrib nulo simplesmente não casa e o
Optional volta vazio — não há NPE. A correção acima é suficiente para nulo, vazio e código inexistente.

Impacto

Em automação comercial (PDV/supermercado), é comum um SKU estar sem cClassTrib no cadastro durante a
transição da Reforma Tributária. Com este bug, esse item não gera rejeição: ele sai no XML com a
classificação do item anterior do cupom e a nota é autorizada com tributação errada.

Se o item anterior for de cesta básica (alíquota zero), o produto sem cadastro sai desonerado; se for
tributado integralmente, sai com imposto cheio. O erro é silencioso, depende da ordem dos itens no documento,
e — a partir do Split Payment — se traduz em retenção financeira incorreta na liquidação.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions