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
10 changes: 10 additions & 0 deletions src/cliente/distribuicao.teste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
13 changes: 13 additions & 0 deletions src/cliente/distribuicao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down Expand Up @@ -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,
};
}
7 changes: 5 additions & 2 deletions src/cliente/indice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LoteDistribuicaoNsu>;
/** GET /parametrizacao/{codigoMunicipio}/convenio no ADN - parâmetros de convênio do município. */
Expand Down Expand Up @@ -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),
});
},

Expand Down
34 changes: 28 additions & 6 deletions src/dps/serializacao.teste.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 },
},
},
Expand All @@ -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, /<CST>123<\/CST>/);
assert.match(xml, /<cClassTrib>123456<\/cClassTrib>/);
assert.match(xml, /<CSTReg>789<\/CSTReg>/);
assert.match(xml, /<cClassTribReg>789012<\/cClassTribReg>/);
});

test('rejeita IBSCBS.dest sem CNPJ, CPF, NIF ou cNaoNIF', () => {
const dados = dadosDpsExemplo();
dados.IBSCBS = {
Expand All @@ -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);
});
Expand All @@ -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);
});
Expand All @@ -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);
Expand Down
11 changes: 9 additions & 2 deletions src/dps/serializacao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<gTribRegular>${tag('CSTReg', reg.CSTReg)}${tag('cClassTribReg', reg.cClassTribReg)}</gTribRegular>`;
return `<gTribRegular>${tag('CSTReg', derivarCstDoClassTrib(reg.cClassTribReg))}${tag('cClassTribReg', reg.cClassTribReg)}</gTribRegular>`;
}

function xmlDiferimentoIbscbs(dif: DiferimentoIbscbs): string {
Expand All @@ -223,7 +230,7 @@ function xmlDiferimentoIbscbs(dif: DiferimentoIbscbs): string {
function xmlSituacaoTributariaIbscbs(sit: SituacaoTributariaIbscbs): string {
return (
`<gIBSCBS>` +
tag('CST', sit.CST) +
tag('CST', derivarCstDoClassTrib(sit.cClassTrib)) +
tag('cClassTrib', sit.cClassTrib) +
tag('cCredPres', sit.cCredPres) +
(sit.gTribRegular ? xmlTributacaoRegularIbscbs(sit.gTribRegular) : '') +
Expand Down
12 changes: 7 additions & 5 deletions src/dps/tipos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down
Loading