From dea40e26831cf69f40cea64ed5d63182fa833fd9 Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Soares Date: Fri, 14 Aug 2026 00:26:09 -0300 Subject: [PATCH] fix: registrarEvento, ultimoNsu e derivacao do CST no IBSCBS --- src/cliente/distribuicao.teste.ts | 10 +++++++++ src/cliente/distribuicao.ts | 13 ++++++++++++ src/cliente/indice.ts | 7 +++++-- src/dps/serializacao.teste.ts | 34 +++++++++++++++++++++++++------ src/dps/serializacao.ts | 11 ++++++++-- src/dps/tipos.ts | 12 ++++++----- 6 files changed, 72 insertions(+), 15 deletions(-) diff --git a/src/cliente/distribuicao.teste.ts b/src/cliente/distribuicao.teste.ts index 3a14f67..69b1297 100644 --- a/src/cliente/distribuicao.teste.ts +++ b/src/cliente/distribuicao.teste.ts @@ -55,3 +55,13 @@ test('lida com lote vazio (nenhum documento localizado)', () => { assert.equal(resultado.statusProcessamento, 'NENHUM_DOCUMENTO_LOCALIZADO'); assert.deepEqual(resultado.documentos, []); }); + +test('le ultimoNsu quando presente (cursor de paginacao, pode ser maior que o NSU dos documentos)', () => { + const resultado = normalizarLoteDistribuicao(loteExemplo({ UltimoNSU: 55 })); + assert.equal(resultado.ultimoNsu, 55); +}); + +test('ultimoNsu fica ausente quando a resposta nao traz o campo', () => { + const resultado = normalizarLoteDistribuicao(loteExemplo()); + assert.equal(resultado.ultimoNsu, undefined); +}); diff --git a/src/cliente/distribuicao.ts b/src/cliente/distribuicao.ts index b52f27e..10f2d7a 100644 --- a/src/cliente/distribuicao.ts +++ b/src/cliente/distribuicao.ts @@ -57,6 +57,15 @@ export interface LoteDistribuicaoNsu { ambiente: 'PRODUCAO' | 'HOMOLOGACAO'; versaoAplicativo?: string; dataHoraProcessamento: string; + /** + * Cursor oficial de paginação, quando o ADN devolve esse campo. Pode ser + * maior que o maior NSU presente em `documentos` - o ADN às vezes consome + * um NSU sem entregar documento correspondente a ele. Prefira este campo + * (quando presente) em vez do maior NSU do lote pra continuar a + * sincronização; usar só o maior NSU do lote pode pular um NSU "consumido" + * e perder o documento seguinte silenciosamente. + */ + ultimoNsu?: number; } function normalizarMensagens(lista: unknown): MensagemProcessamentoAdn[] { @@ -90,5 +99,9 @@ export function normalizarLoteDistribuicao(corpo: unknown): LoteDistribuicaoNsu ambiente: objeto.TipoAmbiente as 'PRODUCAO' | 'HOMOLOGACAO', versaoAplicativo: objeto.VersaoAplicativo as string | undefined, dataHoraProcessamento: objeto.DataHoraProcessamento as string, + // O campo nao aparece em toda resposta (so quando ha NSU consumido sem + // documento entregue) e a spec oficial nao documenta o nome com certeza - + // aceita as variacoes de casing ja observadas em uso real. + ultimoNsu: (objeto.UltimoNSU ?? objeto.ultimoNSU ?? objeto.UltNSU ?? objeto.ultNSU) as number | undefined, }; } diff --git a/src/cliente/indice.ts b/src/cliente/indice.ts index 0799df7..1095e13 100644 --- a/src/cliente/indice.ts +++ b/src/cliente/indice.ts @@ -72,7 +72,10 @@ export interface ClienteSefin { /** * GET /contribuintes/DFe/{nsu} no ADN - baixa o próximo lote de documentos * fiscais (até 50) a partir do NSU informado. Use `0` para sincronizar - * desde o início; o NSU do último documento recebido para continuar depois. + * desde o início. Para continuar depois, prefira `resultado.ultimoNsu` + * (quando presente) em vez do maior NSU dos documentos recebidos - o ADN + * pode consumir um NSU sem entregar documento nenhum pra ele, e avançar só + * pelo maior NSU do lote pula esse NSU e perde o próximo documento. */ baixarDfe(nsu: number, opcoes?: { cnpjConsulta?: string; lote?: boolean }): Promise; /** GET /parametrizacao/{codigoMunicipio}/convenio no ADN - parâmetros de convênio do município. */ @@ -186,7 +189,7 @@ export function criarClienteSefin(opcoes: OpcoesClienteSefin): ClienteSefin { registrarEvento(chaveAcesso, pedRegXmlAssinado) { return requisitar('POST', urlBase, `nfse/${chaveAcesso}/eventos`, { - pedRegXmlGZipB64: compactarGZipBase64(pedRegXmlAssinado), + pedidoRegistroEventoXmlGZipB64: compactarGZipBase64(pedRegXmlAssinado), }); }, diff --git a/src/dps/serializacao.teste.ts b/src/dps/serializacao.teste.ts index 449b354..edea0e9 100644 --- a/src/dps/serializacao.teste.ts +++ b/src/dps/serializacao.teste.ts @@ -1,4 +1,4 @@ -import assert from 'node:assert/strict'; +import assert from 'node:assert/strict'; import { cpSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -132,10 +132,9 @@ test('monta um XML de DPS com bloco IBSCBS valido contra o XSD oficial', () => { ], trib: { gIBSCBS: { - CST: '000', cClassTrib: '000001', cCredPres: '01', - gTribRegular: { CSTReg: '000', cClassTribReg: '000001' }, + gTribRegular: { cClassTribReg: '000001' }, gDif: { pDifUF: 0, pDifMun: 0, pDifCBS: 0 }, }, }, @@ -148,6 +147,29 @@ test('monta um XML de DPS com bloco IBSCBS valido contra o XSD oficial', () => { assert.ok(valido, `XML nao passou no XSD oficial: ${doc.validationErrors.join('; ')}`); }); +test('deriva o CST a partir dos 3 primeiros digitos do cClassTrib (RN 627), nunca informado a parte', () => { + const dados = dadosDpsExemplo(); + dados.IBSCBS = { + finNFSe: '0', + cIndOp: '000001', + indDest: '0', + valores: { + trib: { + gIBSCBS: { + cClassTrib: '123456', + gTribRegular: { cClassTribReg: '789012' }, + }, + }, + }, + }; + + const { xml } = montarXmlDps(dados); + assert.match(xml, /123<\/CST>/); + assert.match(xml, /123456<\/cClassTrib>/); + assert.match(xml, /789<\/CSTReg>/); + assert.match(xml, /789012<\/cClassTribReg>/); +}); + test('rejeita IBSCBS.dest sem CNPJ, CPF, NIF ou cNaoNIF', () => { const dados = dadosDpsExemplo(); dados.IBSCBS = { @@ -156,7 +178,7 @@ test('rejeita IBSCBS.dest sem CNPJ, CPF, NIF ou cNaoNIF', () => { indDest: '1', // @ts-expect-error -- teste propositalmente nao informa nenhuma identificacao dest: { xNome: 'Destinatario Exemplo' }, - valores: { trib: { gIBSCBS: { CST: '000', cClassTrib: '000001' } } }, + valores: { trib: { gIBSCBS: { cClassTrib: '000001' } } }, }; assert.throws(() => montarXmlDps(dados), ErroValidacaoDps); }); @@ -168,7 +190,7 @@ test('rejeita IBSCBS.imovel sem cCIB nem end', () => { cIndOp: '000001', indDest: '0', imovel: {}, - valores: { trib: { gIBSCBS: { CST: '000', cClassTrib: '000001' } } }, + valores: { trib: { gIBSCBS: { cClassTrib: '000001' } } }, }; assert.throws(() => montarXmlDps(dados), ErroValidacaoDps); }); @@ -182,7 +204,7 @@ test('rejeita documento de gReeRepRes sem dFeNacional, docFiscalOutro ou docOutr valores: { // @ts-expect-error -- teste propositalmente nao informa nenhuma referencia de documento gReeRepRes: [{ dtEmiDoc: new Date(), dtCompDoc: new Date(), tpReeRepRes: '01', vlrReeRepRes: 10 }], - trib: { gIBSCBS: { CST: '000', cClassTrib: '000001' } }, + trib: { gIBSCBS: { cClassTrib: '000001' } }, }, }; assert.throws(() => montarXmlDps(dados), ErroValidacaoDps); diff --git a/src/dps/serializacao.ts b/src/dps/serializacao.ts index a2b215a..ebb854a 100644 --- a/src/dps/serializacao.ts +++ b/src/dps/serializacao.ts @@ -212,8 +212,15 @@ function xmlImovel(imovel: ImovelIbscbs): string { throw new ErroValidacaoDps('IBSCBS.imovel precisa informar cCIB ou end.'); } +// RN 627 (SEFIN Nacional): o CST nunca e informado a parte, e sempre os 3 +// primeiros digitos do cClassTrib - derivar aqui evita gerar um par +// CST/cClassTrib inconsistente. +function derivarCstDoClassTrib(cClassTrib: string): string { + return cClassTrib.slice(0, 3); +} + function xmlTributacaoRegularIbscbs(reg: TributacaoRegularIbscbs): string { - return `${tag('CSTReg', reg.CSTReg)}${tag('cClassTribReg', reg.cClassTribReg)}`; + return `${tag('CSTReg', derivarCstDoClassTrib(reg.cClassTribReg))}${tag('cClassTribReg', reg.cClassTribReg)}`; } function xmlDiferimentoIbscbs(dif: DiferimentoIbscbs): string { @@ -223,7 +230,7 @@ function xmlDiferimentoIbscbs(dif: DiferimentoIbscbs): string { function xmlSituacaoTributariaIbscbs(sit: SituacaoTributariaIbscbs): string { return ( `` + - tag('CST', sit.CST) + + tag('CST', derivarCstDoClassTrib(sit.cClassTrib)) + tag('cClassTrib', sit.cClassTrib) + tag('cCredPres', sit.cCredPres) + (sit.gTribRegular ? xmlTributacaoRegularIbscbs(sit.gTribRegular) : '') + diff --git a/src/dps/tipos.ts b/src/dps/tipos.ts index e9457a1..1ccd3bf 100644 --- a/src/dps/tipos.ts +++ b/src/dps/tipos.ts @@ -164,9 +164,7 @@ export interface ImovelIbscbs { } export interface TributacaoRegularIbscbs { - /** Código de Situação Tributária (CST) aplicável na tributação regular. */ - CSTReg: string; - /** Código de Classificação Tributária aplicável na tributação regular. */ + /** Código de Classificação Tributária aplicável na tributação regular. CSTReg é derivado (3 primeiros dígitos), não informado à parte. */ cClassTribReg: string; } @@ -177,8 +175,12 @@ export interface DiferimentoIbscbs { } export interface SituacaoTributariaIbscbs { - /** Código de Situação Tributária (CST) do IBS e da CBS. */ - CST: string; + /** + * Código de Classificação Tributária do IBS e da CBS. O Código de Situação + * Tributária (CST) não é informado à parte: são sempre os 3 primeiros + * dígitos do cClassTrib (regra de negócio 627 do SEFIN Nacional) - o SDK + * deriva o CST daqui pra nunca gerar um par CST/cClassTrib inconsistente. + */ cClassTrib: string; /** Código e classificação do crédito presumido (2 dígitos), quando aplicável. */ cCredPres?: string;