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,437 @@
1
+ import axios from 'axios';
2
+ import { SEFAZ_STATUS } from '../../constants/sefaz.js';
3
+ import { SefazError } from '../../errors/SefazError.js';
4
+ import { GhinfeError } from '../../errors/GhinfeError.js';
5
+ import {
6
+ SEFAZ_MOCK_RESPONSES,
7
+ buildMockProtNFeXml,
8
+ buildMockRetEnvEventoXml,
9
+ buildMockRetConsStatServXml,
10
+ wrapSoapResponse,
11
+ wrapSoapEventoResponse,
12
+ wrapSoapStatusResponse,
13
+ } from '../mocks/sefaz-responses.js';
14
+ import { UF_IBGE } from '../../constants/sefaz.js';
15
+ import { SEFAZ_SERVICO, SefazEndpointResolver } from '../../sefaz/SefazEndpointResolver.js';
16
+
17
+ /**
18
+ * Extrai cStat e xMotivo de um XML de retorno SEFAZ.
19
+ * @param {string} xml
20
+ */
21
+ function parseRetorno(xml) {
22
+ const cStatMatch = xml.match(/<cStat>(\d+)<\/cStat>/);
23
+ const xMotivoMatch = xml.match(/<xMotivo>([^<]+)<\/xMotivo>/);
24
+ const nProtMatch = xml.match(/<nProt>(\d+)<\/nProt>/);
25
+ const chNFeMatch = xml.match(/<chNFe>(\d{44})<\/chNFe>/);
26
+
27
+ return {
28
+ cStat: cStatMatch?.[1] ?? '999',
29
+ xMotivo: xMotivoMatch?.[1] ?? 'Retorno não identificado',
30
+ nProt: nProtMatch?.[1],
31
+ chNFe: chNFeMatch?.[1],
32
+ xmlRetorno: xml,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Serviço de comunicação SOAP com a SEFAZ.
38
+ * Suporta modo mock para desenvolvimento sem certificado/homologação.
39
+ */
40
+ export class NFeSoapService {
41
+ /**
42
+ * @param {{ mock?: boolean, endpoint?: string, eventoEndpoint?: string, timeout?: number, mockScenario?: string, mockEventoScenario?: string }} [options]
43
+ */
44
+ constructor(options = {}) {
45
+ this.mock = options.mock ?? true;
46
+ this.endpoint = options.endpoint;
47
+ this.eventoEndpoint = options.eventoEndpoint;
48
+ this.statusEndpoint = options.statusEndpoint;
49
+ this.timeout = options.timeout ?? 30000;
50
+ this.mockScenario = options.mockScenario ?? 'autorizado';
51
+ this.mockEventoScenario = options.mockEventoScenario ?? 'registrado';
52
+ this.mockStatusScenario = options.mockStatusScenario ?? 'ok';
53
+ }
54
+
55
+ /**
56
+ * Monta envelope SOAP para NFeAutorizacao4.
57
+ * @param {string} xmlNFeAssinado
58
+ * @param {string} idLote
59
+ */
60
+ buildAutorizacaoEnvelope(xmlNFeAssinado, idLote) {
61
+ const nfeBase64 = Buffer.from(xmlNFeAssinado, 'utf-8').toString('base64');
62
+
63
+ return `<?xml version="1.0" encoding="utf-8"?>
64
+ <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
65
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
66
+ xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
67
+ <soap12:Body>
68
+ <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeAutorizacao4">
69
+ <enviNFe versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
70
+ <idLote>${idLote}</idLote>
71
+ <indSinc>1</indSinc>
72
+ <NFe>${nfeBase64}</NFe>
73
+ </enviNFe>
74
+ </nfeDadosMsg>
75
+ </soap12:Body>
76
+ </soap12:Envelope>`;
77
+ }
78
+
79
+ /**
80
+ * Envia NF-e para autorização na SEFAZ (ou mock).
81
+ * @param {string} xmlNFeAssinado
82
+ * @param {string} chaveAcesso
83
+ * @param {string} [idLote]
84
+ */
85
+ async autorizar(xmlNFeAssinado, chaveAcesso, idLote = '1') {
86
+ if (this.mock) {
87
+ return this.#mockAutorizar(chaveAcesso);
88
+ }
89
+
90
+ if (!this.endpoint) {
91
+ throw new GhinfeError(
92
+ 'Endpoint SEFAZ não configurado. Defina options.endpoint ou use mock: true',
93
+ 'SEFAZ_ENDPOINT_MISSING'
94
+ );
95
+ }
96
+
97
+ const envelope = this.buildAutorizacaoEnvelope(xmlNFeAssinado, idLote);
98
+
99
+ try {
100
+ const response = await axios.post(this.endpoint, envelope, {
101
+ headers: {
102
+ 'Content-Type': 'application/soap+xml; charset=utf-8',
103
+ },
104
+ timeout: this.timeout,
105
+ responseType: 'text',
106
+ });
107
+
108
+ const bodyMatch = response.data.match(/<nfeResultMsg[^>]*>([\s\S]*?)<\/nfeResultMsg>/);
109
+ const innerXml = bodyMatch?.[1] ?? response.data;
110
+ return this.#processarRetorno(innerXml);
111
+ } catch (error) {
112
+ if (axios.isAxiosError(error)) {
113
+ throw new GhinfeError(
114
+ `Falha na comunicação SOAP com SEFAZ: ${error.message}`,
115
+ 'SEFAZ_HTTP_ERROR',
116
+ { status: error.response?.status, data: error.response?.data }
117
+ );
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Simula envio à SEFAZ com cenários configuráveis.
125
+ * @param {string} chaveAcesso
126
+ */
127
+ async #mockAutorizar(chaveAcesso) {
128
+ await new Promise((r) => setTimeout(r, 300));
129
+
130
+ const scenarioMap = {
131
+ autorizado: SEFAZ_MOCK_RESPONSES.autorizado,
132
+ duplicidade: SEFAZ_MOCK_RESPONSES.rejeicaoDuplicidade,
133
+ schema: SEFAZ_MOCK_RESPONSES.rejeicaoSchema,
134
+ certificado: SEFAZ_MOCK_RESPONSES.rejeicaoCertificado,
135
+ paralisado: SEFAZ_MOCK_RESPONSES.servicoParalisado,
136
+ };
137
+
138
+ const resultado = scenarioMap[this.mockScenario] ?? SEFAZ_MOCK_RESPONSES.autorizado;
139
+ const innerXml = buildMockProtNFeXml(chaveAcesso, resultado);
140
+ const soapXml = wrapSoapResponse(innerXml);
141
+
142
+ return this.#processarRetorno(innerXml, { soapXml, mock: true });
143
+ }
144
+
145
+ /**
146
+ * @param {string} xml
147
+ * @param {Record<string, unknown>} [meta]
148
+ */
149
+ #processarRetorno(xml, meta = {}) {
150
+ const retorno = parseRetorno(xml);
151
+
152
+ if (retorno.cStat === SEFAZ_STATUS.AUTORIZADO) {
153
+ return {
154
+ sucesso: true,
155
+ autorizado: true,
156
+ cStat: retorno.cStat,
157
+ xMotivo: retorno.xMotivo,
158
+ protocolo: retorno.nProt,
159
+ chaveAcesso: retorno.chNFe,
160
+ xmlProtocolo: xml,
161
+ ...meta,
162
+ };
163
+ }
164
+
165
+ if (retorno.cStat.startsWith('1')) {
166
+ return {
167
+ sucesso: true,
168
+ autorizado: false,
169
+ cStat: retorno.cStat,
170
+ xMotivo: retorno.xMotivo,
171
+ xmlProtocolo: xml,
172
+ ...meta,
173
+ };
174
+ }
175
+
176
+ throw new SefazError(retorno.cStat, retorno.xMotivo, { xmlRetorno: xml, ...meta });
177
+ }
178
+
179
+ /**
180
+ * Define cenário do mock em runtime.
181
+ * @param {'autorizado'|'duplicidade'|'schema'|'certificado'|'paralisado'} scenario
182
+ */
183
+ setMockScenario(scenario) {
184
+ this.mockScenario = scenario;
185
+ }
186
+
187
+ /**
188
+ * Monta envelope SOAP para NFeRecepcaoEvento4.
189
+ * @param {string} xmlEventoAssinado
190
+ * @param {string} idLote
191
+ */
192
+ buildEventoEnvelope(xmlEventoAssinado, idLote = '1') {
193
+ return `<?xml version="1.0" encoding="utf-8"?>
194
+ <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
195
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
196
+ xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
197
+ <soap12:Body>
198
+ <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeRecepcaoEvento4">
199
+ ${xmlEventoAssinado}
200
+ </nfeDadosMsg>
201
+ </soap12:Body>
202
+ </soap12:Envelope>`;
203
+ }
204
+
205
+ /**
206
+ * Envia evento NF-e (cancelamento, CC-e) para SEFAZ.
207
+ * @param {string} xmlEventoAssinado
208
+ * @param {string} chaveAcesso
209
+ * @param {string} tpEvento
210
+ * @param {string} [idLote]
211
+ */
212
+ async enviarEvento(xmlEventoAssinado, chaveAcesso, tpEvento, idLote = '1') {
213
+ if (this.mock) {
214
+ return this.#mockEnviarEvento(chaveAcesso, tpEvento);
215
+ }
216
+
217
+ const endpoint = this.eventoEndpoint ?? this.endpoint;
218
+ if (!endpoint) {
219
+ throw new GhinfeError(
220
+ 'Endpoint SEFAZ de eventos não configurado. Defina eventoEndpoint ou use mock: true',
221
+ 'SEFAZ_EVENTO_ENDPOINT_MISSING'
222
+ );
223
+ }
224
+
225
+ const envelope = this.buildEventoEnvelope(xmlEventoAssinado, idLote);
226
+
227
+ try {
228
+ const response = await axios.post(endpoint, envelope, {
229
+ headers: { 'Content-Type': 'application/soap+xml; charset=utf-8' },
230
+ timeout: this.timeout,
231
+ responseType: 'text',
232
+ });
233
+
234
+ const bodyMatch = response.data.match(/<nfeResultMsg[^>]*>([\s\S]*?)<\/nfeResultMsg>/);
235
+ const innerXml = bodyMatch?.[1] ?? response.data;
236
+ return this.#processarRetornoEvento(innerXml);
237
+ } catch (error) {
238
+ if (axios.isAxiosError(error)) {
239
+ throw new GhinfeError(
240
+ `Falha na comunicação SOAP (evento) com SEFAZ: ${error.message}`,
241
+ 'SEFAZ_HTTP_ERROR',
242
+ { status: error.response?.status, data: error.response?.data }
243
+ );
244
+ }
245
+ throw error;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * @param {string} chaveAcesso
251
+ * @param {string} tpEvento
252
+ */
253
+ async #mockEnviarEvento(chaveAcesso, tpEvento) {
254
+ await new Promise((r) => setTimeout(r, 200));
255
+
256
+ const scenarioMap = {
257
+ registrado: SEFAZ_MOCK_RESPONSES.eventoRegistrado,
258
+ rejeitado: SEFAZ_MOCK_RESPONSES.eventoRejeitado,
259
+ };
260
+
261
+ const resultado = scenarioMap[this.mockEventoScenario] ?? SEFAZ_MOCK_RESPONSES.eventoRegistrado;
262
+ const innerXml = buildMockRetEnvEventoXml(chaveAcesso, tpEvento, resultado);
263
+ const soapXml = wrapSoapEventoResponse(innerXml);
264
+
265
+ return this.#processarRetornoEvento(innerXml, { soapXml, mock: true });
266
+ }
267
+
268
+ /**
269
+ * @param {string} xml
270
+ * @param {Record<string, unknown>} [meta]
271
+ */
272
+ #processarRetornoEvento(xml, meta = {}) {
273
+ const cStatMatches = [...xml.matchAll(/<cStat>(\d+)<\/cStat>/g)];
274
+ const xMotivoMatches = [...xml.matchAll(/<xMotivo>([^<]+)<\/xMotivo>/g)];
275
+ const nProtMatch = xml.match(/<nProt>(\d+)<\/nProt>/);
276
+ const tpEventoMatch = xml.match(/<tpEvento>(\d+)<\/tpEvento>/);
277
+
278
+ const cStatLote = cStatMatches[0]?.[1] ?? '999';
279
+ const cStatEvento = cStatMatches[1]?.[1] ?? cStatLote;
280
+ const xMotivo = xMotivoMatches.at(-1)?.[1] ?? 'Retorno não identificado';
281
+
282
+ if (cStatEvento === SEFAZ_STATUS.EVENTO_REGISTRADO) {
283
+ return {
284
+ sucesso: true,
285
+ registrado: true,
286
+ cStat: cStatEvento,
287
+ xMotivo,
288
+ protocolo: nProtMatch?.[1],
289
+ tpEvento: tpEventoMatch?.[1],
290
+ xmlRetorno: xml,
291
+ ...meta,
292
+ };
293
+ }
294
+
295
+ if (cStatLote.startsWith('1') && cStatEvento.startsWith('1')) {
296
+ return {
297
+ sucesso: true,
298
+ registrado: false,
299
+ cStat: cStatEvento,
300
+ xMotivo,
301
+ xmlRetorno: xml,
302
+ ...meta,
303
+ };
304
+ }
305
+
306
+ throw new SefazError(cStatEvento, xMotivo, { xmlRetorno: xml, ...meta });
307
+ }
308
+
309
+ /**
310
+ * @param {'registrado'|'rejeitado'} scenario
311
+ */
312
+ setMockEventoScenario(scenario) {
313
+ this.mockEventoScenario = scenario;
314
+ }
315
+
316
+ /**
317
+ * Monta XML de consulta de status do serviço.
318
+ * @param {string} uf
319
+ * @param {number} [ambiente]
320
+ */
321
+ buildStatusServicoXml(uf, ambiente = 2) {
322
+ const cUF = UF_IBGE[uf?.toUpperCase()] ?? '35';
323
+ return `<?xml version="1.0" encoding="UTF-8"?>
324
+ <consStatServ xmlns="http://www.portalfiscal.inf.br/nfe" versao="4.00">
325
+ <tpAmb>${ambiente}</tpAmb>
326
+ <cUF>${cUF}</cUF>
327
+ <xServ>STATUS</xServ>
328
+ </consStatServ>`;
329
+ }
330
+
331
+ /**
332
+ * Consulta status do webservice SEFAZ (NfeStatusServico4).
333
+ * @param {string} uf
334
+ * @param {number} [ambiente]
335
+ */
336
+ async consultarStatus(uf, ambiente = 2) {
337
+ if (this.mock) {
338
+ return this.#mockConsultarStatus(uf);
339
+ }
340
+
341
+ const statusEndpoint =
342
+ this.statusEndpoint ??
343
+ (() => {
344
+ try {
345
+ return SefazEndpointResolver.resolver({
346
+ uf,
347
+ ambiente,
348
+ servico: SEFAZ_SERVICO.STATUS,
349
+ });
350
+ } catch {
351
+ return null;
352
+ }
353
+ })();
354
+
355
+ if (!statusEndpoint) {
356
+ throw new GhinfeError(
357
+ `Endpoint de status SEFAZ não disponível para UF ${uf}`,
358
+ 'SEFAZ_STATUS_ENDPOINT_MISSING'
359
+ );
360
+ }
361
+
362
+ const dados = this.buildStatusServicoXml(uf, ambiente);
363
+ const envelope = `<?xml version="1.0" encoding="utf-8"?>
364
+ <soap12:Envelope xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
365
+ <soap12:Body>
366
+ <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeStatusServico4">
367
+ ${dados}
368
+ </nfeDadosMsg>
369
+ </soap12:Body>
370
+ </soap12:Envelope>`;
371
+
372
+ try {
373
+ const response = await axios.post(statusEndpoint, envelope, {
374
+ headers: { 'Content-Type': 'application/soap+xml; charset=utf-8' },
375
+ timeout: this.timeout,
376
+ responseType: 'text',
377
+ });
378
+ const bodyMatch = response.data.match(/<nfeResultMsg[^>]*>([\s\S]*?)<\/nfeResultMsg>/);
379
+ return this.#processarRetornoStatus(bodyMatch?.[1] ?? response.data);
380
+ } catch (error) {
381
+ if (axios.isAxiosError(error)) {
382
+ throw new GhinfeError(
383
+ `Falha na consulta de status SEFAZ: ${error.message}`,
384
+ 'SEFAZ_HTTP_ERROR',
385
+ { status: error.response?.status }
386
+ );
387
+ }
388
+ throw error;
389
+ }
390
+ }
391
+
392
+ /** @param {string} uf */
393
+ async #mockConsultarStatus(uf) {
394
+ await new Promise((r) => setTimeout(r, 150));
395
+ const scenarioMap = {
396
+ ok: SEFAZ_MOCK_RESPONSES.statusOk,
397
+ paralisado: SEFAZ_MOCK_RESPONSES.statusParalisado,
398
+ };
399
+ const resultado = scenarioMap[this.mockStatusScenario ?? 'ok'] ?? SEFAZ_MOCK_RESPONSES.statusOk;
400
+ const cUF = UF_IBGE[uf?.toUpperCase()] ?? '35';
401
+ const innerXml = buildMockRetConsStatServXml(resultado, cUF);
402
+ return this.#processarRetornoStatus(innerXml, {
403
+ soapXml: wrapSoapStatusResponse(innerXml),
404
+ mock: true,
405
+ });
406
+ }
407
+
408
+ /**
409
+ * @param {string} xml
410
+ * @param {Record<string, unknown>} [meta]
411
+ */
412
+ #processarRetornoStatus(xml, meta = {}) {
413
+ const cStat = xml.match(/<cStat>(\d+)<\/cStat>/)?.[1] ?? '999';
414
+ const xMotivo = xml.match(/<xMotivo>([^<]+)<\/xMotivo>/)?.[1] ?? 'Retorno não identificado';
415
+ const tMed = xml.match(/<tMed>([^<]+)<\/tMed>/)?.[1];
416
+ const cUF = xml.match(/<cUF>(\d+)<\/cUF>/)?.[1];
417
+ const emOperacao = cStat === SEFAZ_STATUS.SERVICO_EM_OPERACAO;
418
+
419
+ return {
420
+ sucesso: emOperacao,
421
+ emOperacao,
422
+ cStat,
423
+ xMotivo,
424
+ tMed,
425
+ cUF,
426
+ xmlRetorno: xml,
427
+ ...meta,
428
+ };
429
+ }
430
+
431
+ /**
432
+ * @param {'ok'|'paralisado'} scenario
433
+ */
434
+ setMockStatusScenario(scenario) {
435
+ this.mockStatusScenario = scenario;
436
+ }
437
+ }
@@ -0,0 +1,41 @@
1
+ import { NFeXmlBuilder } from '../builders/NFeXmlBuilder.js';
2
+ import { NFeXsdService } from './NFeXsdService.js';
3
+ import { reformaHabilitada } from '../../reforma/ibscbs.js';
4
+
5
+ /**
6
+ * Serviço responsável pela geração e validação XSD do XML da NF-e.
7
+ */
8
+ export class NFeXmlService {
9
+ /**
10
+ * @param {{ validarXsd?: boolean }} [options]
11
+ */
12
+ constructor(options = {}) {
13
+ this.builder = new NFeXmlBuilder();
14
+ this.xsdService = new NFeXsdService();
15
+ this.validarXsd = options.validarXsd ?? false;
16
+ }
17
+
18
+ /**
19
+ * @param {import('../types/NFeDocument.js').NFeDocument} documento
20
+ * @param {{ validarXsd?: boolean }} [options]
21
+ */
22
+ gerarXml(documento, options = {}) {
23
+ const xml = this.builder.build(documento);
24
+ const deveValidar = options.validarXsd ?? this.validarXsd;
25
+
26
+ // PL_009 não conhece IBSCBS — não validar XSD quando reforma está ativa
27
+ if (deveValidar && !reformaHabilitada(documento)) {
28
+ this.xsdService.validarNFe(xml, { preAssinatura: true });
29
+ }
30
+
31
+ return xml;
32
+ }
33
+
34
+ /**
35
+ * @param {string} xml
36
+ * @param {{ preAssinatura?: boolean }} [options]
37
+ */
38
+ validarXml(xml, options = {}) {
39
+ return this.xsdService.validarNFe(xml, options);
40
+ }
41
+ }
@@ -0,0 +1,56 @@
1
+ import { XsdValidator } from '../../validation/XsdValidator.js';
2
+ import { XSD_SCHEMAS } from '../../validation/schemas.js';
3
+
4
+ /**
5
+ * Validação XSD específica para NF-e / NFC-e (layout 4.00).
6
+ */
7
+ export class NFeXsdService {
8
+ /**
9
+ * Valida XML da NF-e (modelo 55) contra schema oficial.
10
+ * @param {string} xml
11
+ * @param {{ preAssinatura?: boolean }} [options]
12
+ * preAssinatura=true injeta stub estrutural (Signature) para validar infNFe antes de assinar
13
+ */
14
+ validarNFe(xml, options = {}) {
15
+ return XsdValidator.validar(xml, XSD_SCHEMAS.NFE, {
16
+ injetarAssinaturaStub: options.preAssinatura ?? !xml.includes('</Signature>'),
17
+ modelo: '55',
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Valida XML da NFC-e (modelo 65) contra schema oficial.
23
+ * @param {string} xml
24
+ * @param {{ preAssinatura?: boolean }} [options]
25
+ */
26
+ validarNFCe(xml, options = {}) {
27
+ return XsdValidator.validar(xml, XSD_SCHEMAS.NFE, {
28
+ injetarAssinaturaStub: options.preAssinatura ?? !xml.includes('</Signature>'),
29
+ modelo: '65',
30
+ });
31
+ }
32
+
33
+ /**
34
+ * @param {string} xml
35
+ * @param {'55'|'65'} [modelo]
36
+ */
37
+ tentarValidar(xml, modelo = '55') {
38
+ return modelo === '65' ? this.tentarValidarNFCe(xml) : this.tentarValidarNFe(xml);
39
+ }
40
+
41
+ /** @param {string} xml */
42
+ tentarValidarNFe(xml) {
43
+ return XsdValidator.tentarValidar(xml, XSD_SCHEMAS.NFE, {
44
+ injetarAssinaturaStub: !xml.includes('</Signature>'),
45
+ modelo: '55',
46
+ });
47
+ }
48
+
49
+ /** @param {string} xml */
50
+ tentarValidarNFCe(xml) {
51
+ return XsdValidator.tentarValidar(xml, XSD_SCHEMAS.NFE, {
52
+ injetarAssinaturaStub: !xml.includes('</Signature>'),
53
+ modelo: '65',
54
+ });
55
+ }
56
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @typedef {Object} NFeEndereco
3
+ * @property {string} logradouro
4
+ * @property {string} numero
5
+ * @property {string} [complemento]
6
+ * @property {string} bairro
7
+ * @property {string} codigoMunicipio IBGE 7 dígitos
8
+ * @property {string} municipio
9
+ * @property {string} uf
10
+ * @property {string} cep
11
+ * @property {string} [telefone]
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} NFeEmitente
16
+ * @property {string} cnpj
17
+ * @property {string} razaoSocial
18
+ * @property {string} [nomeFantasia]
19
+ * @property {string} ie
20
+ * @property {string} crt 1=Simples, 2=Simples excesso, 3=Normal
21
+ * @property {NFeEndereco} endereco
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} NFeDestinatario
26
+ * @property {string} [cnpj]
27
+ * @property {string} [cpf]
28
+ * @property {string} razaoSocial
29
+ * @property {string} [ie]
30
+ * @property {number} [indIEDest] 1=Contribuinte, 2=Isento, 9=Não contribuinte
31
+ * @property {NFeEndereco} endereco
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} NFeItem
36
+ * @property {string} codigo
37
+ * @property {string} descricao
38
+ * @property {string} ncm
39
+ * @property {string} cfop
40
+ * @property {string} unidade
41
+ * @property {number} quantidade
42
+ * @property {number} valorUnitario
43
+ * @property {number} valorTotal
44
+ * @property {string} [ean]
45
+ * @property {string} [cst] CST ICMS (ex: "00")
46
+ * @property {number} [aliquotaIcms]
47
+ * @property {number} [valorIcms]
48
+ * @property {import('../../reforma/ibscbs.js').TributacaoRTC} [reforma] IBS/CBS (PL_010)
49
+ */
50
+
51
+ /**
52
+ * @typedef {Object} NFeFrete
53
+ * @property {number} modalidade 0=Emitente, 1=Destinatário, 9=Sem frete
54
+ * @property {number} [valor]
55
+ * @property {string} [transportadora]
56
+ * @property {string} [cnpjTransportadora]
57
+ */
58
+
59
+ /**
60
+ * @typedef {Object} NFePagamento
61
+ * @property {string} forma 01=Dinheiro, 03=Cartão crédito, etc.
62
+ * @property {number} valor
63
+ */
64
+
65
+ /**
66
+ * @typedef {Object} NFeIde
67
+ * @property {string} uf Sigla UF (ex: "SP")
68
+ * @property {string} codigoMunicipio IBGE
69
+ * @property {number} [serie]
70
+ * @property {number} numero
71
+ * @property {number} [tipoEmissao] 1=Normal
72
+ * @property {number} [finalidade] 1=Normal
73
+ * @property {number} [consumidorFinal] 0=Não, 1=Sim
74
+ * @property {number} [presenca] 1=Presencial
75
+ * @property {number} [naturezaOperacao]
76
+ */
77
+
78
+ /**
79
+ * @typedef {Object} NFeDocumento
80
+ * @property {string} [chaveAcesso] 44 dígitos — gerada automaticamente se omitida
81
+ * @property {number} [ambiente] 1=Produção, 2=Homologação
82
+ * @property {boolean} [reformaTributaria] força totais IBS/CBS mesmo sem item.reforma
83
+ * @property {NFeIde} ide
84
+ * @property {NFeEmitente} emitente
85
+ * @property {NFeDestinatario} destinatario
86
+ * @property {NFeItem[]} itens
87
+ * @property {NFeFrete} [frete]
88
+ * @property {NFePagamento[]} [pagamentos]
89
+ * @property {string} [informacoesComplementares]
90
+ */
91
+
92
+ export {};
@@ -0,0 +1,44 @@
1
+ /** Tipos de evento NF-e conforme tabela SEFAZ. */
2
+ export const NFE_EVENTOS = {
3
+ CARTA_CORRECAO: '110110',
4
+ CANCELAMENTO: '110111',
5
+ CIENCIA_OPERACAO: '210210',
6
+ CONFIRMACAO_OPERACAO: '210200',
7
+ DESCONHECIMENTO_OPERACAO: '210220',
8
+ OPERACAO_NAO_REALIZADA: '210240',
9
+ };
10
+
11
+ /** Texto fixo exigido na CC-e. */
12
+ export const CCE_COND_USO =
13
+ 'A Carta de Correcao e disciplinada pelo paragrafo 1o-A do art. 7o do Convenio S/N, ' +
14
+ 'de 15 de dezembro de 1970 e pode ser utilizada para regularizacao de erro ocorrido ' +
15
+ 'na emissao de documento fiscal, desde que o erro nao esteja relacionado com: ' +
16
+ 'I - as variaveis que determinam o valor do imposto tais como: base de calculo, ' +
17
+ 'aliquota, diferenca de preco, quantidade, valor da operacao ou da prestacao; ' +
18
+ 'II - a correcao de dados cadastrais que implique mudanca do remetente ou do destinatario; ' +
19
+ 'III - a data de emissao ou de saida.';
20
+
21
+ /**
22
+ * @typedef {Object} NFeEventoBase
23
+ * @property {string} chaveAcesso Chave da NF-e (44 dígitos)
24
+ * @property {string} cnpj CNPJ do autor do evento
25
+ * @property {string} uf Sigla UF
26
+ * @property {number} [ambiente] 1=Produção, 2=Homologação
27
+ * @property {number} [sequencia] nSeqEvento (default 1)
28
+ * @property {string} [idLote] ID do lote de eventos
29
+ */
30
+
31
+ /**
32
+ * @typedef {NFeEventoBase & {
33
+ * protocolo: string,
34
+ * justificativa: string,
35
+ * }} NFeCancelamento
36
+ */
37
+
38
+ /**
39
+ * @typedef {NFeEventoBase & {
40
+ * correcao: string,
41
+ * }} NFeCartaCorrecao
42
+ */
43
+
44
+ export {};