ghinfe 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.env.example +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +413 -0
  4. package/docs/developer-guide.html +421 -0
  5. package/docs/openapi.yaml +428 -0
  6. package/examples/emitir-nfce.js +51 -0
  7. package/examples/emitir-nfe.js +83 -0
  8. package/examples/emitir-nfse.js +46 -0
  9. package/examples/endpoints-sefaz.js +14 -0
  10. package/examples/eventos-nfe.js +49 -0
  11. package/examples/gerar-chave.js +25 -0
  12. package/examples/gerar-danfe.js +75 -0
  13. package/examples/layouts-nfse.js +51 -0
  14. package/examples/mock-sefaz.js +52 -0
  15. package/examples/nfse-eventos.js +57 -0
  16. package/examples/reforma-ibscbs.js +27 -0
  17. package/examples/reforma-nfe-xml.js +52 -0
  18. package/examples/status-sefaz.js +12 -0
  19. package/examples/validar-xsd.js +48 -0
  20. package/package.json +68 -0
  21. package/schemas/nfe/PL_009_V4/consReciNFe_v4.00.xsd +9 -0
  22. package/schemas/nfe/PL_009_V4/consSitNFe_v4.00.xsd +9 -0
  23. package/schemas/nfe/PL_009_V4/consStatServ_v4.00.xsd +9 -0
  24. package/schemas/nfe/PL_009_V4/enviNFe_v3.10.xsd +9 -0
  25. package/schemas/nfe/PL_009_V4/enviNFe_v4.00.xsd +9 -0
  26. package/schemas/nfe/PL_009_V4/inutNFe_v4.00.xsd +9 -0
  27. package/schemas/nfe/PL_009_V4/leiauteNFe_v4.00.xsd +7339 -0
  28. package/schemas/nfe/PL_009_V4/nfe_v4.00.xsd +9 -0
  29. package/schemas/nfe/PL_009_V4/tiposBasico_v4.00.xsd +598 -0
  30. package/schemas/nfe/PL_009_V4/xmldsig-core-schema_v1.01.xsd +98 -0
  31. package/scripts/download-schemas.js +39 -0
  32. package/scripts/test-xsd.js +34 -0
  33. package/src/api/server.js +316 -0
  34. package/src/constants/sefaz.js +69 -0
  35. package/src/danfe/DanfeService.js +205 -0
  36. package/src/danfe/xml-extract.js +69 -0
  37. package/src/errors/GhinfeError.js +13 -0
  38. package/src/errors/SefazError.js +25 -0
  39. package/src/errors/XsdValidationError.js +15 -0
  40. package/src/index.js +60 -0
  41. package/src/nfce/builders/NFCeXmlBuilder.js +178 -0
  42. package/src/nfce/constants/qrcode.js +26 -0
  43. package/src/nfce/services/NFCeService.js +188 -0
  44. package/src/nfce/services/NFCeXmlService.js +38 -0
  45. package/src/nfce/types/NFCeDocument.js +42 -0
  46. package/src/nfce/utils/qrcode.utils.js +92 -0
  47. package/src/nfe/builders/NFeEventoXmlBuilder.js +110 -0
  48. package/src/nfe/builders/NFeXmlBuilder.js +215 -0
  49. package/src/nfe/mocks/sefaz-responses.js +195 -0
  50. package/src/nfe/services/NFeEventoService.js +95 -0
  51. package/src/nfe/services/NFeService.js +196 -0
  52. package/src/nfe/services/NFeSignService.js +111 -0
  53. package/src/nfe/services/NFeSoapService.js +437 -0
  54. package/src/nfe/services/NFeXmlService.js +41 -0
  55. package/src/nfe/services/NFeXsdService.js +56 -0
  56. package/src/nfe/types/NFeDocument.js +92 -0
  57. package/src/nfe/types/NFeEvento.js +44 -0
  58. package/src/nfse/NFSeMunicipioResolver.js +42 -0
  59. package/src/nfse/builders/NFSeEventoXmlBuilder.js +92 -0
  60. package/src/nfse/builders/NFSeXmlBuilder.js +16 -0
  61. package/src/nfse/constants/municipios.js +82 -0
  62. package/src/nfse/layouts/adapters.js +202 -0
  63. package/src/nfse/mocks/nfse-responses.js +142 -0
  64. package/src/nfse/services/NFSeService.js +191 -0
  65. package/src/nfse/services/NFSeSoapService.js +232 -0
  66. package/src/nfse/services/NFSeXmlService.js +12 -0
  67. package/src/nfse/types/NFSeDocument.js +54 -0
  68. package/src/queue/NFCeQueue.js +63 -0
  69. package/src/queue/NFSeQueue.js +59 -0
  70. package/src/queue/NFeQueue.js +76 -0
  71. package/src/reforma/ibscbs.js +175 -0
  72. package/src/sefaz/SefazEndpointResolver.js +95 -0
  73. package/src/sefaz/endpoints.registry.js +205 -0
  74. package/src/shared/fiscal.helpers.js +53 -0
  75. package/src/utils/certificate.utils.js +47 -0
  76. package/src/utils/chave.utils.js +117 -0
  77. package/src/utils/date.utils.js +26 -0
  78. package/src/utils/xml.utils.js +44 -0
  79. package/src/validation/XsdValidator.js +114 -0
  80. package/src/validation/schemas.js +14 -0
@@ -0,0 +1,42 @@
1
+ import { NFSE_MUNICIPIOS } from './constants/municipios.js';
2
+ import { GhinfeError } from '../errors/GhinfeError.js';
3
+
4
+ /**
5
+ * Resolve endpoint NFS-e por código IBGE do município.
6
+ */
7
+ export class NFSeMunicipioResolver {
8
+ /** @type {Record<string, object>} */
9
+ static #custom = {};
10
+
11
+ /**
12
+ * Registra endpoint customizado para um município.
13
+ * @param {string} codigoMunicipio IBGE 7 dígitos
14
+ * @param {{ nome?: string, homologacao: string, producao: string, layout?: string }} config
15
+ */
16
+ static registrar(codigoMunicipio, config) {
17
+ this.#custom[codigoMunicipio] = config;
18
+ }
19
+
20
+ /**
21
+ * @param {string} codigoMunicipio
22
+ * @param {number} [ambiente] 1=Produção, 2=Homologação
23
+ */
24
+ static resolver(codigoMunicipio, ambiente = 2) {
25
+ const config = this.#custom[codigoMunicipio] ?? NFSE_MUNICIPIOS[codigoMunicipio];
26
+
27
+ if (!config) {
28
+ throw new GhinfeError(
29
+ `Endpoint NFS-e não configurado para município ${codigoMunicipio}. Use NFSeMunicipioResolver.registrar()`,
30
+ 'NFSE_MUNICIPIO_NOT_FOUND'
31
+ );
32
+ }
33
+
34
+ const url = ambiente === 1 ? config.producao : config.homologacao;
35
+ return { url, layout: config.layout ?? 'abrasf', nome: config.nome };
36
+ }
37
+
38
+ /** @returns {string[]} */
39
+ static listarMunicipios() {
40
+ return [...new Set([...Object.keys(NFSE_MUNICIPIOS), ...Object.keys(this.#custom)])];
41
+ }
42
+ }
@@ -0,0 +1,92 @@
1
+ import { create } from 'xmlbuilder2';
2
+ import { NFSE_NAMESPACE } from '../constants/municipios.js';
3
+ import { GhinfeError } from '../../errors/GhinfeError.js';
4
+
5
+ /**
6
+ * XML de cancelamento e substituição de NFS-e (padrão ABRASF 2.04).
7
+ */
8
+ export class NFSeEventoXmlBuilder {
9
+ /**
10
+ * @param {{
11
+ * numeroNfse: string,
12
+ * cnpjPrestador: string,
13
+ * inscricaoMunicipal: string,
14
+ * codigoMunicipio: string,
15
+ * codigoCancelamento?: string,
16
+ * }} dados
17
+ */
18
+ buildCancelamento(dados) {
19
+ if (!dados.numeroNfse) throw new GhinfeError('numeroNfse é obrigatório', 'NFSE_CANCEL_NUMERO');
20
+ if (!dados.cnpjPrestador) throw new GhinfeError('cnpjPrestador é obrigatório', 'NFSE_CANCEL_CNPJ');
21
+
22
+ const id = `Cancelamento_${dados.numeroNfse}`;
23
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
24
+ .ele('CancelarNfseEnvio', { xmlns: NFSE_NAMESPACE })
25
+ .ele('Pedido')
26
+ .ele('InfPedidoCancelamento', { Id: id });
27
+
28
+ const inf = doc;
29
+ const ident = inf.ele('IdentificacaoNfse');
30
+ ident.ele('Numero').txt(String(dados.numeroNfse)).up();
31
+ ident.ele('Cnpj').txt(dados.cnpjPrestador.replace(/\D/g, '')).up();
32
+ ident.ele('InscricaoMunicipal').txt(dados.inscricaoMunicipal).up();
33
+ ident.ele('CodigoMunicipio').txt(dados.codigoMunicipio).up();
34
+ ident.up();
35
+ inf.ele('CodigoCancelamento').txt(dados.codigoCancelamento ?? '2').up();
36
+
37
+ return doc.end({ prettyPrint: false, headless: false });
38
+ }
39
+
40
+ /**
41
+ * Substituição = pedido de cancelamento da NFS-e substituída + novo RPS.
42
+ * O XML do novo RPS deve ser passado já montado (GerarNfseEnvio / lote).
43
+ *
44
+ * @param {{
45
+ * numeroNfseSubstituida: string,
46
+ * cnpjPrestador: string,
47
+ * inscricaoMunicipal: string,
48
+ * codigoMunicipio: string,
49
+ * codigoCancelamento?: string,
50
+ * xmlRpsSubstituto: string,
51
+ * }} dados
52
+ */
53
+ buildSubstituicao(dados) {
54
+ if (!dados.numeroNfseSubstituida) {
55
+ throw new GhinfeError('numeroNfseSubstituida é obrigatório', 'NFSE_SUBST_NUMERO');
56
+ }
57
+ if (!dados.xmlRpsSubstituto) {
58
+ throw new GhinfeError('xmlRpsSubstituto é obrigatório', 'NFSE_SUBST_RPS');
59
+ }
60
+
61
+ const cancelamento = this.buildCancelamento({
62
+ numeroNfse: dados.numeroNfseSubstituida,
63
+ cnpjPrestador: dados.cnpjPrestador,
64
+ inscricaoMunicipal: dados.inscricaoMunicipal,
65
+ codigoMunicipio: dados.codigoMunicipio,
66
+ codigoCancelamento: dados.codigoCancelamento ?? '1',
67
+ });
68
+
69
+ // Extrai InfPedidoCancelamento e Rps interno do RPS substituto
70
+ const pedidoMatch = cancelamento.match(/<Pedido>[\s\S]*?<\/Pedido>/);
71
+ const rpsInner = this.#extrairRps(dados.xmlRpsSubstituto);
72
+
73
+ return `<?xml version="1.0" encoding="UTF-8"?>
74
+ <SubstituirNfseEnvio xmlns="${NFSE_NAMESPACE}">
75
+ <SubstituicaoNfse Id="Substituicao_${dados.numeroNfseSubstituida}">
76
+ ${pedidoMatch?.[0] ?? ''}
77
+ <Rps>${rpsInner}</Rps>
78
+ </SubstituicaoNfse>
79
+ </SubstituirNfseEnvio>`;
80
+ }
81
+
82
+ /** @param {string} xmlRps */
83
+ #extrairRps(xmlRps) {
84
+ const inf = xmlRps.match(/<(InfRps|InfDeclaracaoPrestacaoServico)[\s\S]*?<\/\1>/);
85
+ if (inf) return inf[0];
86
+ const rps = xmlRps.match(/<Rps[\s\S]*?<\/Rps>/);
87
+ if (rps) {
88
+ return rps[0].replace(/^<Rps[^>]*>/, '').replace(/<\/Rps>$/, '');
89
+ }
90
+ return xmlRps.replace(/^<\?xml[^?]*\?>/, '').trim();
91
+ }
92
+ }
@@ -0,0 +1,16 @@
1
+ import { obterAdapter } from '../layouts/adapters.js';
2
+ import { NFSE_LAYOUTS } from '../constants/municipios.js';
3
+
4
+ /**
5
+ * Gera XML RPS conforme layout municipal (ABRASF, GINFES, Betha, ISSNet).
6
+ */
7
+ export class NFSeXmlBuilder {
8
+ /**
9
+ * @param {import('../types/NFSeDocument.js').NFSeDocumento} documento
10
+ * @param {string} [layout] abrasf | ginfes | betha | issnet
11
+ * @returns {string}
12
+ */
13
+ buildGerarNfse(documento, layout = NFSE_LAYOUTS.ABRASF) {
14
+ return obterAdapter(layout).build(documento);
15
+ }
16
+ }
@@ -0,0 +1,82 @@
1
+ /** Layouts NFS-e suportados pela biblioteca. */
2
+ export const NFSE_LAYOUTS = {
3
+ ABRASF: 'abrasf',
4
+ GINFES: 'ginfes',
5
+ BETHA: 'betha',
6
+ ISSNET: 'issnet',
7
+ };
8
+
9
+ /** Layout ABRASF 2.04 — padrão mais adotado pelos municípios. */
10
+ export const NFSE_LAYOUT = '2.04';
11
+ export const NFSE_NAMESPACE = 'http://www.abrasf.org.br/nfse.xsd';
12
+ export const GINFES_NAMESPACE = 'http://www.ginfes.com.br/servico_enviar_lote_rps_envio';
13
+ export const BETHA_NAMESPACE = 'http://www.betha.com.br/e-nota-contribuinte-ws';
14
+ export const ISSNET_NAMESPACE = 'http://www.issnetonline.com.br/webserviceabrasf/vsd/servico_enviar_lote_rps_envio.xsd';
15
+
16
+ /**
17
+ * Endpoints NFS-e por município (IBGE).
18
+ * layout: abrasf | ginfes | betha | issnet
19
+ * URLs são exemplos — valide com a prefeitura / provedor.
20
+ */
21
+ export const NFSE_MUNICIPIOS = {
22
+ '3550308': {
23
+ nome: 'São Paulo/SP',
24
+ layout: NFSE_LAYOUTS.ABRASF,
25
+ homologacao: 'https://nfe.prefeitura.sp.gov.br/ws/lotenfe.asmx',
26
+ producao: 'https://nfe.prefeitura.sp.gov.br/ws/lotenfe.asmx',
27
+ },
28
+ '3304557': {
29
+ nome: 'Rio de Janeiro/RJ',
30
+ layout: NFSE_LAYOUTS.ABRASF,
31
+ homologacao: 'https://notacarioca.rio.gov.br/WSNacional/nfse.asmx',
32
+ producao: 'https://notacarioca.rio.gov.br/WSNacional/nfse.asmx',
33
+ },
34
+ '3106200': {
35
+ nome: 'Belo Horizonte/MG',
36
+ layout: NFSE_LAYOUTS.ABRASF,
37
+ homologacao: 'https://bhissdigital.pbh.gov.br/bhiss-ws/nfse',
38
+ producao: 'https://bhissdigital.pbh.gov.br/bhiss-ws/nfse',
39
+ },
40
+ '4106902': {
41
+ nome: 'Curitiba/PR',
42
+ layout: NFSE_LAYOUTS.ABRASF,
43
+ homologacao: 'https://isscuritiba.curitiba.pr.gov.br/Iss.NfseWebService/NfseService.svc',
44
+ producao: 'https://isscuritiba.curitiba.pr.gov.br/Iss.NfseWebService/NfseService.svc',
45
+ },
46
+ '4314902': {
47
+ nome: 'Porto Alegre/RS',
48
+ layout: NFSE_LAYOUTS.BETHA,
49
+ homologacao: 'https://e-gov.betha.com.br/e-nota-contribuinte-ws/nfseWS',
50
+ producao: 'https://e-gov.betha.com.br/e-nota-contribuinte-ws/nfseWS',
51
+ },
52
+ '4205407': {
53
+ nome: 'Florianópolis/SC',
54
+ layout: NFSE_LAYOUTS.BETHA,
55
+ homologacao: 'https://e-gov.betha.com.br/e-nota-contribuinte-test-ws/nfseWS',
56
+ producao: 'https://e-gov.betha.com.br/e-nota-contribuinte-ws/nfseWS',
57
+ },
58
+ '2927408': {
59
+ nome: 'Salvador/BA',
60
+ layout: NFSE_LAYOUTS.GINFES,
61
+ homologacao: 'https://homologacao.ginfes.com.br/ServiceGinfesImpl',
62
+ producao: 'https://producao.ginfes.com.br/ServiceGinfesImpl',
63
+ },
64
+ '2611606': {
65
+ nome: 'Recife/PE',
66
+ layout: NFSE_LAYOUTS.ISSNET,
67
+ homologacao: 'https://www.issnetonline.com.br/webserviceabrasf/homologacao/servicos.asmx',
68
+ producao: 'https://www.issnetonline.com.br/webserviceabrasf/producao/servicos.asmx',
69
+ },
70
+ '2304400': {
71
+ nome: 'Fortaleza/CE',
72
+ layout: NFSE_LAYOUTS.ISSNET,
73
+ homologacao: 'https://iss.fortaleza.ce.gov.br/group/guest/nfe',
74
+ producao: 'https://iss.fortaleza.ce.gov.br/group/guest/nfe',
75
+ },
76
+ mock: {
77
+ nome: 'Mock GHINFE',
78
+ layout: NFSE_LAYOUTS.ABRASF,
79
+ homologacao: 'http://localhost/mock/nfse',
80
+ producao: 'http://localhost/mock/nfse',
81
+ },
82
+ };
@@ -0,0 +1,202 @@
1
+ import { create } from 'xmlbuilder2';
2
+ import {
3
+ NFSE_NAMESPACE,
4
+ GINFES_NAMESPACE,
5
+ BETHA_NAMESPACE,
6
+ ISSNET_NAMESPACE,
7
+ NFSE_LAYOUTS,
8
+ } from '../constants/municipios.js';
9
+ import { formatSefazDateTime } from '../../utils/date.utils.js';
10
+ import { GhinfeError } from '../../errors/GhinfeError.js';
11
+
12
+ /**
13
+ * Monta o bloco InfRps / InfDeclaracaoPrestacaoServico comum aos layouts.
14
+ * @param {import('xmlbuilder2').XMLBuilder} parent
15
+ * @param {import('../types/NFSeDocument.js').NFSeDocumento} documento
16
+ * @param {string} idAttr
17
+ */
18
+ function appendInfRps(parent, documento, idAttr = 'InfRps') {
19
+ const dataEmissao = formatSefazDateTime();
20
+ const idRps = `RPS${documento.numero}`;
21
+ const valorServicos = documento.servico.valorServicos;
22
+ const valorIss = documento.servico.valorIss ?? 0;
23
+ const baseCalculo = valorServicos - (documento.servico.valorDeducoes ?? 0);
24
+ const valorLiquido =
25
+ valorServicos -
26
+ (documento.servico.descontoIncondicionado ?? 0) -
27
+ (documento.servico.descontoCondicionado ?? 0);
28
+
29
+ const inf = parent.ele(idAttr, { Id: idRps });
30
+
31
+ const idRpsNode = inf.ele('IdentificacaoRps');
32
+ idRpsNode.ele('Numero').txt(String(documento.numero)).up();
33
+ idRpsNode.ele('Serie').txt(documento.serie ?? 'NF').up();
34
+ idRpsNode.ele('Tipo').txt(String(documento.tipo ?? 1)).up();
35
+ idRpsNode.up();
36
+
37
+ inf
38
+ .ele('DataEmissao').txt(dataEmissao).up()
39
+ .ele('NaturezaOperacao').txt(String(documento.naturezaOperacao ?? 1)).up()
40
+ .ele('OptanteSimplesNacional').txt(String(documento.optanteSimplesNacional ?? 1)).up()
41
+ .ele('IncentivadorCultural').txt(String(documento.incentivadorCultural ?? 2)).up()
42
+ .ele('Status').txt('1').up();
43
+
44
+ const servico = inf.ele('Servico');
45
+ const valores = servico.ele('Valores');
46
+ valores
47
+ .ele('ValorServicos').txt(dec(valorServicos)).up()
48
+ .ele('ValorDeducoes').txt(dec(documento.servico.valorDeducoes ?? 0)).up()
49
+ .ele('ValorPis').txt(dec(documento.servico.valorPis ?? 0)).up()
50
+ .ele('ValorCofins').txt(dec(documento.servico.valorCofins ?? 0)).up()
51
+ .ele('ValorInss').txt(dec(documento.servico.valorInss ?? 0)).up()
52
+ .ele('ValorIr').txt(dec(documento.servico.valorIr ?? 0)).up()
53
+ .ele('ValorCsll').txt(dec(documento.servico.valorCsll ?? 0)).up()
54
+ .ele('IssRetido').txt(String(documento.servico.issRetido ?? 2)).up()
55
+ .ele('ValorIss').txt(dec(valorIss)).up()
56
+ .ele('BaseCalculo').txt(dec(baseCalculo)).up()
57
+ .ele('Aliquota').txt(dec(documento.servico.aliquotaIss ?? 0, 4)).up()
58
+ .ele('ValorLiquidoNfse').txt(dec(valorLiquido)).up()
59
+ .ele('DescontoIncondicionado').txt(dec(documento.servico.descontoIncondicionado ?? 0)).up()
60
+ .ele('DescontoCondicionado').txt(dec(documento.servico.descontoCondicionado ?? 0)).up();
61
+ valores.up();
62
+
63
+ servico
64
+ .ele('ItemListaServico').txt(documento.servico.itemListaServico).up()
65
+ .ele('Discriminacao').txt(documento.servico.discriminacao).up()
66
+ .ele('CodigoMunicipio').txt(documento.servico.codigoMunicipio).up();
67
+ servico.up();
68
+
69
+ const prestador = inf.ele('Prestador');
70
+ prestador
71
+ .ele('Cnpj').txt(documento.cnpjPrestador.replace(/\D/g, '')).up()
72
+ .ele('InscricaoMunicipal').txt(documento.inscricaoMunicipal).up();
73
+ prestador.up();
74
+
75
+ const tomador = inf.ele('Tomador');
76
+ const identTomador = tomador.ele('IdentificacaoTomador').ele('CpfCnpj');
77
+ const docTomador = (documento.tomador.cnpj ?? documento.tomador.cpf ?? '').replace(/\D/g, '');
78
+ if (docTomador.length === 14) identTomador.ele('Cnpj').txt(docTomador).up();
79
+ else if (docTomador.length === 11) identTomador.ele('Cpf').txt(docTomador).up();
80
+ identTomador.up().up();
81
+ tomador.ele('RazaoSocial').txt(documento.tomador.razaoSocial).up();
82
+
83
+ if (documento.tomador.endereco) {
84
+ const end = tomador.ele('Endereco');
85
+ const e = documento.tomador.endereco;
86
+ end.ele('Endereco').txt(e.logradouro).up();
87
+ end.ele('Numero').txt(e.numero).up();
88
+ if (e.complemento) end.ele('Complemento').txt(e.complemento).up();
89
+ end.ele('Bairro').txt(e.bairro).up();
90
+ end.ele('CodigoMunicipio').txt(e.codigoMunicipio).up();
91
+ end.ele('Uf').txt(e.uf).up();
92
+ end.ele('Cep').txt(e.cep.replace(/\D/g, '')).up();
93
+ end.up();
94
+ }
95
+
96
+ if (documento.tomador.email) {
97
+ tomador.ele('Contato').ele('Email').txt(documento.tomador.email).up().up();
98
+ }
99
+ tomador.up();
100
+
101
+ if (documento.informacoesComplementares) {
102
+ inf.ele('Observacao').txt(documento.informacoesComplementares).up();
103
+ }
104
+
105
+ return inf;
106
+ }
107
+
108
+ /** @param {number} v @param {number} [d] */
109
+ function dec(v, d = 2) {
110
+ return Number(v).toFixed(d);
111
+ }
112
+
113
+ /**
114
+ * @param {import('../types/NFSeDocument.js').NFSeDocumento} documento
115
+ */
116
+ function validarDocumento(documento) {
117
+ if (!documento.numero) throw new GhinfeError('numero do RPS é obrigatório', 'NFSE_NUMERO_REQUIRED');
118
+ if (!documento.cnpjPrestador) throw new GhinfeError('cnpjPrestador é obrigatório', 'NFSE_CNPJ_REQUIRED');
119
+ if (!documento.inscricaoMunicipal) throw new GhinfeError('inscricaoMunicipal é obrigatória', 'NFSE_IM_REQUIRED');
120
+ if (!documento.servico) throw new GhinfeError('servico é obrigatório', 'NFSE_SERVICO_REQUIRED');
121
+ if (!documento.tomador) throw new GhinfeError('tomador é obrigatório', 'NFSE_TOMADOR_REQUIRED');
122
+ }
123
+
124
+ /**
125
+ * Adapters de layout municipal.
126
+ * Mantêm o mesmo objeto de domínio e trocam o wrapper/namespace XML.
127
+ */
128
+ export const NFSeLayoutAdapters = {
129
+ [NFSE_LAYOUTS.ABRASF]: {
130
+ nome: 'ABRASF 2.04',
131
+ build(documento) {
132
+ validarDocumento(documento);
133
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
134
+ .ele('GerarNfseEnvio', { xmlns: NFSE_NAMESPACE });
135
+ const rps = doc.ele('Rps');
136
+ appendInfRps(rps, documento, 'InfRps');
137
+ return doc.end({ prettyPrint: false, headless: false });
138
+ },
139
+ },
140
+
141
+ [NFSE_LAYOUTS.GINFES]: {
142
+ nome: 'GINFES',
143
+ build(documento) {
144
+ validarDocumento(documento);
145
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
146
+ .ele('EnviarLoteRpsEnvio', { xmlns: GINFES_NAMESPACE });
147
+ const lote = doc.ele('LoteRps', { Id: `Lote${documento.numero}`, versao: '3.00' });
148
+ lote.ele('NumeroLote').txt(String(documento.numero)).up();
149
+ lote.ele('Cnpj').txt(documento.cnpjPrestador.replace(/\D/g, '')).up();
150
+ lote.ele('InscricaoMunicipal').txt(documento.inscricaoMunicipal).up();
151
+ lote.ele('QuantidadeRps').txt('1').up();
152
+ const lista = lote.ele('ListaRps').ele('Rps');
153
+ appendInfRps(lista, documento, 'InfRps');
154
+ return doc.end({ prettyPrint: false, headless: false });
155
+ },
156
+ },
157
+
158
+ [NFSE_LAYOUTS.BETHA]: {
159
+ nome: 'Betha',
160
+ build(documento) {
161
+ validarDocumento(documento);
162
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
163
+ .ele('GerarNfseEnvio', { xmlns: BETHA_NAMESPACE });
164
+ const rps = doc.ele('Rps');
165
+ appendInfRps(rps, documento, 'InfDeclaracaoPrestacaoServico');
166
+ return doc.end({ prettyPrint: false, headless: false });
167
+ },
168
+ },
169
+
170
+ [NFSE_LAYOUTS.ISSNET]: {
171
+ nome: 'ISSNet',
172
+ build(documento) {
173
+ validarDocumento(documento);
174
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
175
+ .ele('EnviarLoteRpsEnvio', { xmlns: ISSNET_NAMESPACE });
176
+ const lote = doc.ele('LoteRps', { Id: `Lote${documento.numero}`, versao: '2.04' });
177
+ lote.ele('NumeroLote').txt(String(documento.numero)).up();
178
+ lote.ele('CpfCnpj').ele('Cnpj').txt(documento.cnpjPrestador.replace(/\D/g, '')).up().up();
179
+ lote.ele('InscricaoMunicipal').txt(documento.inscricaoMunicipal).up();
180
+ lote.ele('QuantidadeRps').txt('1').up();
181
+ const lista = lote.ele('ListaRps').ele('Rps');
182
+ appendInfRps(lista, documento, 'InfDeclaracaoPrestacaoServico');
183
+ return doc.end({ prettyPrint: false, headless: false });
184
+ },
185
+ },
186
+ };
187
+
188
+ /**
189
+ * @param {string} layout
190
+ * @returns {typeof NFSeLayoutAdapters[string]}
191
+ */
192
+ export function obterAdapter(layout) {
193
+ const key = (layout ?? NFSE_LAYOUTS.ABRASF).toLowerCase();
194
+ const adapter = NFSeLayoutAdapters[key];
195
+ if (!adapter) {
196
+ throw new GhinfeError(
197
+ `Layout NFS-e não suportado: ${layout}. Use: ${Object.keys(NFSE_LAYOUTS).join(', ')}`,
198
+ 'NFSE_LAYOUT_UNSUPPORTED'
199
+ );
200
+ }
201
+ return adapter;
202
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Respostas mock NFS-e para desenvolvimento.
3
+ */
4
+ export const NFSE_MOCK_RESPONSES = {
5
+ autorizada: {
6
+ sucesso: true,
7
+ codigo: 'E000',
8
+ mensagem: 'NFS-e autorizada com sucesso',
9
+ numeroNfse: '2026000001',
10
+ codigoVerificacao: 'ABC1-DEF2',
11
+ },
12
+ rejeicao: {
13
+ sucesso: false,
14
+ codigo: 'E001',
15
+ mensagem: 'Rejeição: RPS já convertido em NFS-e',
16
+ },
17
+ cancelada: {
18
+ sucesso: true,
19
+ codigo: 'E000',
20
+ mensagem: 'NFS-e cancelada com sucesso',
21
+ numeroNfse: '2026000001',
22
+ },
23
+ substituida: {
24
+ sucesso: true,
25
+ codigo: 'E000',
26
+ mensagem: 'NFS-e substituída com sucesso',
27
+ numeroNfse: '2026000002',
28
+ codigoVerificacao: 'XYZ9-SUB1',
29
+ numeroNfseSubstituida: '2026000001',
30
+ },
31
+ };
32
+
33
+ /**
34
+ * @param {string} numeroRps
35
+ * @param {{ codigo: string, mensagem: string, numeroNfse?: string, codigoVerificacao?: string }} resultado
36
+ */
37
+ export function buildMockGerarNfseResponse(numeroRps, resultado) {
38
+ const dh = new Date().toISOString();
39
+
40
+ if (resultado.codigo === 'E000') {
41
+ return `<?xml version="1.0" encoding="UTF-8"?>
42
+ <GerarNfseResposta xmlns="http://www.abrasf.org.br/nfse.xsd">
43
+ <ListaNfse>
44
+ <CompNfse>
45
+ <Nfse versao="2.04">
46
+ <InfNfse Id="NFS${resultado.numeroNfse}">
47
+ <Numero>${resultado.numeroNfse}</Numero>
48
+ <CodigoVerificacao>${resultado.codigoVerificacao}</CodigoVerificacao>
49
+ <DataEmissao>${dh}</DataEmissao>
50
+ <IdentificacaoRps>
51
+ <Numero>${numeroRps}</Numero>
52
+ <Serie>NF</Serie>
53
+ <Tipo>1</Tipo>
54
+ </IdentificacaoRps>
55
+ </InfNfse>
56
+ </Nfse>
57
+ </CompNfse>
58
+ </ListaNfse>
59
+ </GerarNfseResposta>`;
60
+ }
61
+
62
+ return `<?xml version="1.0" encoding="UTF-8"?>
63
+ <GerarNfseResposta xmlns="http://www.abrasf.org.br/nfse.xsd">
64
+ <ListaMensagemRetorno>
65
+ <MensagemRetorno>
66
+ <Codigo>${resultado.codigo}</Codigo>
67
+ <Mensagem>${resultado.mensagem}</Mensagem>
68
+ </MensagemRetorno>
69
+ </ListaMensagemRetorno>
70
+ </GerarNfseResposta>`;
71
+ }
72
+
73
+ /**
74
+ * @param {string} numeroNfse
75
+ * @param {{ codigo: string, mensagem: string }} resultado
76
+ */
77
+ export function buildMockCancelarNfseResponse(numeroNfse, resultado) {
78
+ const dh = new Date().toISOString();
79
+ if (resultado.codigo === 'E000') {
80
+ return `<?xml version="1.0" encoding="UTF-8"?>
81
+ <CancelarNfseResposta xmlns="http://www.abrasf.org.br/nfse.xsd">
82
+ <RetCancelamento>
83
+ <NfseCancelamento>
84
+ <Confirmacao>
85
+ <Pedido>
86
+ <InfPedidoCancelamento>
87
+ <IdentificacaoNfse>
88
+ <Numero>${numeroNfse}</Numero>
89
+ </IdentificacaoNfse>
90
+ </InfPedidoCancelamento>
91
+ </Pedido>
92
+ <DataHora>${dh}</DataHora>
93
+ </Confirmacao>
94
+ </NfseCancelamento>
95
+ </RetCancelamento>
96
+ </CancelarNfseResposta>`;
97
+ }
98
+ return `<?xml version="1.0" encoding="UTF-8"?>
99
+ <CancelarNfseResposta xmlns="http://www.abrasf.org.br/nfse.xsd">
100
+ <ListaMensagemRetorno>
101
+ <MensagemRetorno>
102
+ <Codigo>${resultado.codigo}</Codigo>
103
+ <Mensagem>${resultado.mensagem}</Mensagem>
104
+ </MensagemRetorno>
105
+ </ListaMensagemRetorno>
106
+ </CancelarNfseResposta>`;
107
+ }
108
+
109
+ /**
110
+ * @param {{ codigo: string, mensagem: string, numeroNfse?: string, codigoVerificacao?: string, numeroNfseSubstituida?: string }} resultado
111
+ */
112
+ export function buildMockSubstituirNfseResponse(resultado) {
113
+ const dh = new Date().toISOString();
114
+ if (resultado.codigo === 'E000') {
115
+ return `<?xml version="1.0" encoding="UTF-8"?>
116
+ <SubstituirNfseResposta xmlns="http://www.abrasf.org.br/nfse.xsd">
117
+ <RetSubstituicao>
118
+ <NfseSubstituida>
119
+ <InfNfse>
120
+ <Numero>${resultado.numeroNfseSubstituida ?? '2026000001'}</Numero>
121
+ </InfNfse>
122
+ </NfseSubstituida>
123
+ <NfseSubstituidora>
124
+ <InfNfse Id="NFS${resultado.numeroNfse}">
125
+ <Numero>${resultado.numeroNfse}</Numero>
126
+ <CodigoVerificacao>${resultado.codigoVerificacao}</CodigoVerificacao>
127
+ <DataEmissao>${dh}</DataEmissao>
128
+ </InfNfse>
129
+ </NfseSubstituidora>
130
+ </RetSubstituicao>
131
+ </SubstituirNfseResposta>`;
132
+ }
133
+ return `<?xml version="1.0" encoding="UTF-8"?>
134
+ <SubstituirNfseResposta xmlns="http://www.abrasf.org.br/nfse.xsd">
135
+ <ListaMensagemRetorno>
136
+ <MensagemRetorno>
137
+ <Codigo>${resultado.codigo}</Codigo>
138
+ <Mensagem>${resultado.mensagem}</Mensagem>
139
+ </MensagemRetorno>
140
+ </ListaMensagemRetorno>
141
+ </SubstituirNfseResposta>`;
142
+ }