From efe84041e3e367da2939c8560454716010195510 Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Soares Date: Sat, 15 Aug 2026 11:37:15 -0300 Subject: [PATCH] fix(dps): rejeita quebra de linha e outro caractere de controle em campos TSString Varios campos de texto livre do schema oficial (xLgr, xBairro, nro, xCpl, email, o xNome do IBSCBS.dest, xDoc/xDocFiscal/xTipoChaveDFe/xTpReeRepRes do gReeRepRes) rejeitam quebra de linha - confirmado validando contra o XSD oficial. Sem essa checagem local, a SEFIN Nacional recusaria a DPS inteira (E1235) so por causa desse campo. xDescServ e o xNome do prestador/tomador/interm continuam livres (schema nao restringe). --- src/dps/serializacao.teste.ts | 63 ++++++++++++++++++++++++++++++++ src/dps/serializacao.ts | 67 ++++++++++++++++++++++++----------- 2 files changed, 110 insertions(+), 20 deletions(-) diff --git a/src/dps/serializacao.teste.ts b/src/dps/serializacao.teste.ts index edea0e9..75d4630 100644 --- a/src/dps/serializacao.teste.ts +++ b/src/dps/serializacao.teste.ts @@ -209,3 +209,66 @@ test('rejeita documento de gReeRepRes sem dFeNacional, docFiscalOutro ou docOutr }; assert.throws(() => montarXmlDps(dados), ErroValidacaoDps); }); + +// Confirmado contra o XSD oficial (tiposSimples_v1.01.xsd): a maioria dos +// campos de texto livre (tipo TSString) rejeita quebra de linha - so +// xDescServ (TSStringComQuebraDeLinha) e o xNome do prestador/tomador/ +// interm (TSNomeRazaoSocial, sem pattern) sao excecao. Sem validacao local, +// a SEFIN Nacional recusaria a DPS inteira (E1235) so por causa desse +// campo - visto na pratica com "informacoes complementares" em outro +// sistema de emissao de NFS-e. +test('rejeita quebra de linha em xLgr, xBairro, nro, xCpl e email (TSString)', () => { + const comLgr = dadosDpsExemplo(); + comLgr.toma!.end = { endNac: { cMun: '3550308', CEP: '01001000' }, xLgr: 'Rua\nExemplo', nro: '100', xBairro: 'Centro' }; + assert.throws(() => montarXmlDps(comLgr), ErroValidacaoDps); + + const comBairro = dadosDpsExemplo(); + comBairro.toma!.end = { endNac: { cMun: '3550308', CEP: '01001000' }, xLgr: 'Rua Exemplo', nro: '100', xBairro: 'Centro\nSul' }; + assert.throws(() => montarXmlDps(comBairro), ErroValidacaoDps); + + const comEmail = dadosDpsExemplo(); + comEmail.prest.email = 'contato@exemplo.com\n'; + assert.throws(() => montarXmlDps(comEmail), ErroValidacaoDps); +}); + +test('rejeita quebra de linha no xNome do IBSCBS.dest e no xDoc do gReeRepRes (TSDesc150/TSDesc255)', () => { + const comDest = dadosDpsExemplo(); + comDest.IBSCBS = { + finNFSe: '0', + cIndOp: '000001', + indDest: '1', + dest: { CNPJ: '98765432000155', xNome: 'Destinatario\nExemplo' }, + valores: { trib: { gIBSCBS: { cClassTrib: '000001' } } }, + }; + assert.throws(() => montarXmlDps(comDest), ErroValidacaoDps); + + const comDoc = dadosDpsExemplo(); + comDoc.IBSCBS = { + finNFSe: '0', + cIndOp: '000001', + indDest: '0', + valores: { + gReeRepRes: [ + { + docOutro: { nDoc: '123', xDoc: 'Recibo\nde despesa' }, + dtEmiDoc: new Date('2026-06-02T00:00:00Z'), + dtCompDoc: new Date('2026-06-02T00:00:00Z'), + tpReeRepRes: '99', + xTpReeRepRes: 'Reembolso diverso', + vlrReeRepRes: 50, + }, + ], + trib: { gIBSCBS: { cClassTrib: '000001' } }, + }, + }; + assert.throws(() => montarXmlDps(comDoc), ErroValidacaoDps); +}); + +test('permite quebra de linha em xDescServ e no xNome do prestador/tomador (schema nao restringe)', () => { + const dados = dadosDpsExemplo(); + dados.serv.cServ.xDescServ = 'Desenvolvimento\nde software sob demanda'; + dados.toma!.xNome = 'Cliente\nExemplo'; + const { xml } = montarXmlDps(dados); + assert.match(xml, /Desenvolvimento\nde software sob demanda<\/xDescServ>/); + assert.match(xml, /Cliente\nExemplo<\/xNome>/); +}); diff --git a/src/dps/serializacao.ts b/src/dps/serializacao.ts index ebb854a..b785481 100644 --- a/src/dps/serializacao.ts +++ b/src/dps/serializacao.ts @@ -77,13 +77,13 @@ function xmlDocumentoFiscal(doc: { CNPJ?: string; CPF?: string }): string { throw new ErroValidacaoDps('É necessário informar CNPJ ou CPF.'); } -function xmlEndereco(end: Endereco): string { +function xmlEndereco(end: Endereco, contexto: string): string { return ( `${tag('cMun', end.cMun)}${tag('CEP', end.CEP)}` + - tag('xLgr', end.xLgr) + - tag('nro', end.nro) + - tag('xCpl', end.xCpl) + - tag('xBairro', end.xBairro) + tagSemQuebra('xLgr', end.xLgr, contexto) + + tagSemQuebra('nro', end.nro, contexto) + + tagSemQuebra('xCpl', end.xCpl, contexto) + + tagSemQuebra('xBairro', end.xBairro, contexto) ); } @@ -92,26 +92,26 @@ function xmlPrestador(prest: Prestador): string { xmlDocumentoFiscal(prest) + tag('IM', prest.IM) + tag('xNome', prest.xNome) + - (prest.end ? `${xmlEndereco(prest.end)}` : '') + + (prest.end ? `${xmlEndereco(prest.end, 'prest.end')}` : '') + tag('fone', prest.fone) + - tag('email', prest.email) + + tagSemQuebra('email', prest.email, 'prest') + `${tag('opSimpNac', prest.regTrib.opSimpNac)}${tag('regApTribSN', prest.regTrib.regApTribSN)}${tag('regEspTrib', prest.regTrib.regEspTrib)}`; return `${corpo}`; } -function xmlPessoaConteudo(pessoa: Pessoa | DestinatarioIbscbs): string { +function xmlPessoaConteudo(pessoa: Pessoa | DestinatarioIbscbs, contexto: string): string { return ( xmlDocumentoFiscal(pessoa) + ('IM' in pessoa ? tag('IM', pessoa.IM) : '') + tag('xNome', pessoa.xNome) + - (pessoa.end ? `${xmlEndereco(pessoa.end)}` : '') + + (pessoa.end ? `${xmlEndereco(pessoa.end, `${contexto}.end`)}` : '') + tag('fone', pessoa.fone) + - tag('email', pessoa.email) + tagSemQuebra('email', pessoa.email, contexto) ); } function xmlPessoa(elemento: 'toma' | 'interm', pessoa: Pessoa): string { - return `<${elemento}>${xmlPessoaConteudo(pessoa)}`; + return `<${elemento}>${xmlPessoaConteudo(pessoa, elemento)}`; } function xmlServico(serv: Servico): string { @@ -179,10 +179,10 @@ function xmlDest(dest: DestinatarioIbscbs): string { return ( `` + xmlIdentificacaoDest(dest, 'IBSCBS.dest') + - tag('xNome', dest.xNome) + - (dest.end ? `${xmlEndereco(dest.end)}` : '') + + tagSemQuebra('xNome', dest.xNome, 'IBSCBS.dest') + + (dest.end ? `${xmlEndereco(dest.end, 'IBSCBS.dest.end')}` : '') + tag('fone', dest.fone) + - tag('email', dest.email) + + tagSemQuebra('email', dest.email, 'IBSCBS.dest') + `` ); } @@ -197,7 +197,11 @@ function xmlEnderecoImovel(end: EnderecoImovel): string { throw new ErroValidacaoDps('IBSCBS.imovel.end precisa informar CEP ou endExt.'); } return ( - escolha + tag('xLgr', end.xLgr) + tag('nro', end.nro) + tag('xCpl', end.xCpl) + tag('xBairro', end.xBairro) + escolha + + tagSemQuebra('xLgr', end.xLgr, 'IBSCBS.imovel.end') + + tagSemQuebra('nro', end.nro, 'IBSCBS.imovel.end') + + tagSemQuebra('xCpl', end.xCpl, 'IBSCBS.imovel.end') + + tagSemQuebra('xBairro', end.xBairro, 'IBSCBS.imovel.end') ); } @@ -246,15 +250,18 @@ function xmlRefNFSe(refs: string[]): string { function xmlDocumentoReferenciadoReeRepRes(doc: DocumentoReeRepRes): string { if (doc.dFeNacional) { const d = doc.dFeNacional; - return `${tag('tipoChaveDFe', d.tipoChaveDFe)}${tag('xTipoChaveDFe', d.xTipoChaveDFe)}${tag('chaveDFe', d.chaveDFe)}`; + const contexto = 'IBSCBS.valores.gReeRepRes.dFeNacional'; + return `${tag('tipoChaveDFe', d.tipoChaveDFe)}${tagSemQuebra('xTipoChaveDFe', d.xTipoChaveDFe, contexto)}${tag('chaveDFe', d.chaveDFe)}`; } if (doc.docFiscalOutro) { const d = doc.docFiscalOutro; - return `${tag('cMunDocFiscal', d.cMunDocFiscal)}${tag('nDocFiscal', d.nDocFiscal)}${tag('xDocFiscal', d.xDocFiscal)}`; + const contexto = 'IBSCBS.valores.gReeRepRes.docFiscalOutro'; + return `${tag('cMunDocFiscal', d.cMunDocFiscal)}${tag('nDocFiscal', d.nDocFiscal)}${tagSemQuebra('xDocFiscal', d.xDocFiscal, contexto)}`; } if (doc.docOutro) { const d = doc.docOutro; - return `${tag('nDoc', d.nDoc)}${tag('xDoc', d.xDoc)}`; + const contexto = 'IBSCBS.valores.gReeRepRes.docOutro'; + return `${tag('nDoc', d.nDoc)}${tagSemQuebra('xDoc', d.xDoc, contexto)}`; } throw new ErroValidacaoDps( 'IBSCBS.valores.gReeRepRes: cada documento precisa informar dFeNacional, docFiscalOutro ou docOutro.' @@ -262,7 +269,8 @@ function xmlDocumentoReferenciadoReeRepRes(doc: DocumentoReeRepRes): string { } function xmlFornecedorReeRepRes(fornec: FornecedorReeRepRes): string { - return `${xmlIdentificacaoDest(fornec, 'IBSCBS.valores.gReeRepRes.fornec')}${tag('xNome', fornec.xNome)}`; + const contexto = 'IBSCBS.valores.gReeRepRes.fornec'; + return `${xmlIdentificacaoDest(fornec, contexto)}${tagSemQuebra('xNome', fornec.xNome, contexto)}`; } function xmlDocumentoReeRepRes(doc: DocumentoReeRepRes): string { @@ -273,7 +281,7 @@ function xmlDocumentoReeRepRes(doc: DocumentoReeRepRes): string { tag('dtEmiDoc', formatarData(doc.dtEmiDoc)) + tag('dtCompDoc', formatarData(doc.dtCompDoc)) + tag('tpReeRepRes', doc.tpReeRepRes) + - tag('xTpReeRepRes', doc.xTpReeRepRes) + + tagSemQuebra('xTpReeRepRes', doc.xTpReeRepRes, 'IBSCBS.valores.gReeRepRes') + tagNum('vlrReeRepRes', doc.vlrReeRepRes) + `` ); @@ -312,3 +320,22 @@ function tag(nome: string, valor: string | undefined): string { function tagNum(nome: string, valor: number | undefined): string { return valor === undefined ? '' : `<${nome}>${formatarDecimal(valor)}`; } + +// A maioria dos campos de texto livre do schema (tipo TSString em +// tiposSimples_v1.01.xsd) rejeita quebra de linha, tabulacao ou qualquer +// outro caractere de controle - confirmado validando contra o XSD oficial. +// Submeter um deles faz a SEFIN Nacional recusar a DPS inteira (E1235) em +// vez de so o valor problematico. xDescServ e a unica excecao conhecida +// (tipo TSStringComQuebraDeLinha, permite quebra de linha de proposito) e +// por isso continua usando tag() direto. Validar aqui da um erro local e +// claro, em vez de deixar a rejeicao acontecer do lado do governo. +function tagSemQuebra(nome: string, valor: string | undefined, contexto: string): string { + if (valor === undefined || valor === '') return ''; + // eslint-disable-next-line no-control-regex + if (/[\x00-\x1f\x7f]/.test(valor)) { + throw new ErroValidacaoDps( + `${contexto}.${nome} nao pode conter quebra de linha, tabulacao ou outro caractere de controle.` + ); + } + return tag(nome, valor); +}