@nfewizard/shared 1.0.2 → 1.1.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.
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/adapters/SchemaLoader.js +1 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/adapters/SchemaLoader.js.map +1 -1
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/base/BaseNFe.d.ts +1 -1
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/base/GerarConsulta.js +1 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/base/GerarConsulta.js.map +1 -1
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/environment/LoadCertificate.d.ts +5 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/environment/LoadCertificate.js +30 -9
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/environment/LoadCertificate.js.map +1 -1
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/utils/XmlParser.d.ts +65 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/utils/XmlParser.js +189 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/utils/XmlParser.js.map +1 -1
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/utils/index.d.ts +2 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/utils/index.js +1 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/utils/index.js.map +1 -1
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/utils/validateSchema.d.ts +96 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/utils/validateSchema.js +252 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/src/utils/validateSchema.js.map +1 -0
- package/.rollup.cache/usr/projetos/nfewizard/nfewizard-io-idle-old/packages/shared/dist/tsconfig.tsbuildinfo +1 -1
- package/README.md +47 -1
- package/dist/index.cjs +470 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +470 -10
- package/dist/index.mjs.map +1 -1
- package/dist/src/base/BaseNFe.d.ts +1 -1
- package/dist/src/environment/LoadCertificate.d.ts +5 -0
- package/dist/src/utils/XmlParser.d.ts +65 -0
- package/dist/src/utils/index.d.ts +2 -0
- package/dist/src/utils/validateSchema.d.ts +96 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,9 +15,55 @@ pnpm add @nfewizard/shared
|
|
|
15
15
|
- ✅ **XmlBuilder** - Assinatura digital e geração de SOAP
|
|
16
16
|
- ✅ **HttpClient** - Cliente HTTP configurado para SEFAZ
|
|
17
17
|
- ✅ **LoadCertificate** - Carregamento de certificados A1
|
|
18
|
-
- ✅ **SchemaLoader** -
|
|
18
|
+
- ✅ **SchemaLoader** - Mapeamento de schemas XSD por operação fiscal
|
|
19
|
+
- ✅ **NFE_SchemaValidate** - Validação de XML contra schema XSD com relatório humanizado
|
|
19
20
|
- ✅ **Utilities** - Funções auxiliares (validações, formatações)
|
|
20
21
|
|
|
22
|
+
## NFE_SchemaValidate
|
|
23
|
+
|
|
24
|
+
Função pública que valida um XML fiscal contra o schema XSD oficial, disponível para uso direto ou via facade (`NFeWizard.NFE_SchemaValidate`, `NFSe.NFSe_SchemaValidate`).
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { NFE_SchemaValidate, SchemaValidationResult } from '@nfewizard/shared';
|
|
28
|
+
|
|
29
|
+
const result: SchemaValidationResult = await NFE_SchemaValidate(
|
|
30
|
+
xmlString,
|
|
31
|
+
'NFeAutorizacao',
|
|
32
|
+
{ validator: 'validateSchemaJsBased' } // opcional
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// result.success — boolean
|
|
36
|
+
// result.message — mensagem-resumo humanizada
|
|
37
|
+
// result.errors[] — SchemaValidationIssue[]: { raw, humanized, element?, line?, column?, expected? }
|
|
38
|
+
// result.report — string multilinha estilo SEFAZ-RS
|
|
39
|
+
// result.tableRows — Array pronto para console.table
|
|
40
|
+
// result.schema — arquivo XSD utilizado
|
|
41
|
+
// result.metodo — SchemaValidateMethod informado
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Métodos disponíveis (`SchemaValidateMethod`)
|
|
45
|
+
|
|
46
|
+
| Valor | Schema XSD |
|
|
47
|
+
|-------|-----------|
|
|
48
|
+
| `'NFeAutorizacao'` / `'NFEAutorizacao'` | `enviNFe_v4.00.xsd` (envelope adicionado automaticamente se ausente) |
|
|
49
|
+
| `'NFEStatusServico'` | `consStatServ_v4.00.xsd` |
|
|
50
|
+
| `'NFEConsultaProtocolo'` | `consSitNFe_v4.00.xsd` |
|
|
51
|
+
| `'RecepcaoEvento'` | `envEvento_v1.00.xsd` |
|
|
52
|
+
| `'NFeDistribuicaoDFe'` | `distDFeInt_v1.01.xsd` |
|
|
53
|
+
| `'NFEInutilizacao'` | `inutNFe_v4.00.xsd` |
|
|
54
|
+
| `'NFERetAutorizacao'` | `consReciNFe_v4.00.xsd` |
|
|
55
|
+
| `'CTeDistribuicaoDFe'` | `cte/distDFeInt_v1.00.xsd` |
|
|
56
|
+
| `'NFSe_Autorizacao'` | `nfse/DPS_v1.01.xsd` |
|
|
57
|
+
| `'NFSe_Consulta'` / `'NFSe_Distribuicao'` | `nfse/NFSe_v1.01.xsd` |
|
|
58
|
+
| `'NFSe_Eventos'` | `nfse/pedRegEvento_v1.01.xsd` |
|
|
59
|
+
| `'NFSe_ParametrosMunicipais'` | — (API REST, sem XSD) |
|
|
60
|
+
|
|
61
|
+
### Seleção do validador
|
|
62
|
+
|
|
63
|
+
1. `options.validator` — escolha explícita do caller.
|
|
64
|
+
2. `options.environment.getConfig().lib?.useForSchemaValidation` — valor do config da lib.
|
|
65
|
+
3. `'validateSchemaJsBased'` — padrão (sem JDK).
|
|
66
|
+
|
|
21
67
|
## Licença
|
|
22
68
|
|
|
23
69
|
GPL-3.0
|
package/dist/index.cjs
CHANGED
|
@@ -456,6 +456,195 @@ class XmlParser {
|
|
|
456
456
|
}
|
|
457
457
|
return jsonBody;
|
|
458
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Converte um XML de envio (`enviNFe`) ou uma `NFe` solo em uma
|
|
461
|
+
* estrutura JSON compatível com o tipo `NFe` esperado pelos serviços
|
|
462
|
+
* de Autorização de NFe/NFCe (`{ idLote, indSinc, NFe }`).
|
|
463
|
+
*
|
|
464
|
+
* Formatos aceitos:
|
|
465
|
+
* - Envelope `enviNFe` contendo uma ou mais `NFe`.
|
|
466
|
+
* - `nfeProc` (XML autorizado) — extrai a `NFe` interna.
|
|
467
|
+
* - `NFe` solo — empacota com defaults (`idLote=1`, `indSinc=1`).
|
|
468
|
+
*
|
|
469
|
+
* Defaults são aplicáveis somente para os campos do envelope; o
|
|
470
|
+
* conteúdo de `infNFe` não é alterado e segue o fluxo normal de
|
|
471
|
+
* geração/assinatura no service.
|
|
472
|
+
*
|
|
473
|
+
* @param xml String XML de entrada.
|
|
474
|
+
* @param overrides Permite sobrescrever `idLote`/`indSinc` quando o XML
|
|
475
|
+
* não contém envelope.
|
|
476
|
+
*/
|
|
477
|
+
convertXmlEnvioNFeToJson(xml, overrides) {
|
|
478
|
+
logger.info('Convertendo XML de envio para JSON', {
|
|
479
|
+
context: 'XmlParser',
|
|
480
|
+
method: 'convertXmlEnvioNFeToJson',
|
|
481
|
+
});
|
|
482
|
+
const jsonData = this.parseNfeLikeXml(xml);
|
|
483
|
+
let envelope = this.findInObj(jsonData, 'enviNFe');
|
|
484
|
+
if (!envelope) {
|
|
485
|
+
const nfeProc = this.findInObj(jsonData, 'nfeProc');
|
|
486
|
+
const nfeNode = nfeProc ? this.findInObj(nfeProc, 'NFe') : this.findInObj(jsonData, 'NFe');
|
|
487
|
+
if (!nfeNode) {
|
|
488
|
+
throw new Error('XML inválido: não foi possível localizar `enviNFe` ou `NFe`.');
|
|
489
|
+
}
|
|
490
|
+
envelope = {
|
|
491
|
+
idLote: overrides?.idLote ?? '1',
|
|
492
|
+
indSinc: overrides?.indSinc ?? 1,
|
|
493
|
+
NFe: nfeNode,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
// Normaliza CNPJ/CPF -> CNPJCPF nas NFe (formato esperado pela lib)
|
|
497
|
+
const nfeList = Array.isArray(envelope.NFe) ? envelope.NFe : [envelope.NFe];
|
|
498
|
+
nfeList.forEach((n) => this.normalizeNfeForLibFormat(n));
|
|
499
|
+
return {
|
|
500
|
+
idLote: envelope.idLote ?? overrides?.idLote ?? '1',
|
|
501
|
+
indSinc: Number(envelope.indSinc ?? overrides?.indSinc ?? 1),
|
|
502
|
+
NFe: envelope.NFe,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Converte um XML autorizado (`nfeProc`) ou uma `NFe` solo em uma
|
|
507
|
+
* estrutura JSON compatível com o gerador de DANFE
|
|
508
|
+
* (`{ NFe, protNFe? }`), além de retornar a chave de acesso quando
|
|
509
|
+
* disponível.
|
|
510
|
+
*
|
|
511
|
+
* @param xml String XML de entrada.
|
|
512
|
+
*/
|
|
513
|
+
convertXmlNfeProcToJson(xml) {
|
|
514
|
+
logger.info('Convertendo nfeProc para JSON', {
|
|
515
|
+
context: 'XmlParser',
|
|
516
|
+
method: 'convertXmlNfeProcToJson',
|
|
517
|
+
});
|
|
518
|
+
const jsonData = this.parseNfeLikeXml(xml);
|
|
519
|
+
const nfeProc = this.findInObj(jsonData, 'nfeProc');
|
|
520
|
+
const NFe = nfeProc ? this.findInObj(nfeProc, 'NFe') : this.findInObj(jsonData, 'NFe');
|
|
521
|
+
if (!NFe) {
|
|
522
|
+
throw new Error('XML inválido: não foi possível localizar `NFe` ou `nfeProc`.');
|
|
523
|
+
}
|
|
524
|
+
const protNFe = nfeProc ? this.findInObj(nfeProc, 'protNFe') : this.findInObj(jsonData, 'protNFe');
|
|
525
|
+
const chFromProt = protNFe?.infProt?.chNFe;
|
|
526
|
+
const idAttr = NFe?.infNFe?.Id ?? '';
|
|
527
|
+
const chFromId = typeof idAttr === 'string' ? idAttr.replace(/^NFe/, '') : '';
|
|
528
|
+
const chave = chFromProt || chFromId || '';
|
|
529
|
+
const data = { NFe };
|
|
530
|
+
if (protNFe)
|
|
531
|
+
data.protNFe = protNFe;
|
|
532
|
+
return { data, chave };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Faz o parse de um XML de NFe/NFCe (envio, retorno ou solo) preservando
|
|
536
|
+
* atributos e elevando-os para o nível do elemento (ex.: `Id` em `infNFe`,
|
|
537
|
+
* `versao` em `NFe`, `nItem` em `det`). Necessário para alimentar o
|
|
538
|
+
* fluxo interno da lib que espera atributos como propriedades comuns.
|
|
539
|
+
*/
|
|
540
|
+
parseNfeLikeXml(xml) {
|
|
541
|
+
const jsonAsString = convert__namespace.xml2json(xml, {
|
|
542
|
+
compact: true,
|
|
543
|
+
spaces: 2,
|
|
544
|
+
ignoreAttributes: false,
|
|
545
|
+
ignoreDeclaration: true,
|
|
546
|
+
trim: true,
|
|
547
|
+
ignoreInstruction: true,
|
|
548
|
+
ignoreComment: true,
|
|
549
|
+
ignoreCdata: true,
|
|
550
|
+
ignoreDoctype: true,
|
|
551
|
+
textFn: this.removeJsonTextAttribute,
|
|
552
|
+
});
|
|
553
|
+
const parsed = JSON.parse(jsonAsString);
|
|
554
|
+
this.liftAttributes(parsed);
|
|
555
|
+
return parsed;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Eleva, recursivamente, as chaves de `_attributes` para o próprio nível
|
|
559
|
+
* do elemento e remove o nó `_attributes`. Compatível com a saída de
|
|
560
|
+
* `xml-js` em modo compacto.
|
|
561
|
+
*/
|
|
562
|
+
liftAttributes(node) {
|
|
563
|
+
if (!node || typeof node !== 'object')
|
|
564
|
+
return;
|
|
565
|
+
if (Array.isArray(node)) {
|
|
566
|
+
node.forEach(item => this.liftAttributes(item));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (node._attributes && typeof node._attributes === 'object') {
|
|
570
|
+
for (const attrKey of Object.keys(node._attributes)) {
|
|
571
|
+
if (!(attrKey in node)) {
|
|
572
|
+
node[attrKey] = node._attributes[attrKey];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
delete node._attributes;
|
|
576
|
+
}
|
|
577
|
+
for (const key of Object.keys(node)) {
|
|
578
|
+
const value = node[key];
|
|
579
|
+
if (value && typeof value === 'object') {
|
|
580
|
+
this.liftAttributes(value);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Normaliza uma `NFe` parseada de XML para o formato interno da lib:
|
|
586
|
+
* - converte `CNPJ`/`CPF`/`idEstrangeiro` em `CNPJCPF` em `emit`,
|
|
587
|
+
* `dest`, `transp.transporta` e `NFref.refNFP`.
|
|
588
|
+
* - remove atributos elevados que a lib re-aplica via `$` ao montar
|
|
589
|
+
* o XML final (`infNFe.versao`, `det.nItem`), evitando duplicidade
|
|
590
|
+
* no XML gerado.
|
|
591
|
+
*
|
|
592
|
+
* Observação: `infRespTec` mantém o campo `CNPJ` original — a lib não
|
|
593
|
+
* unifica documento para esse nó.
|
|
594
|
+
*/
|
|
595
|
+
normalizeNfeForLibFormat(nfeNode) {
|
|
596
|
+
const infNFe = nfeNode?.infNFe;
|
|
597
|
+
if (!infNFe || typeof infNFe !== 'object')
|
|
598
|
+
return;
|
|
599
|
+
// versao é re-aplicado pela lib via $ ao montar `infNFe`
|
|
600
|
+
delete infNFe.versao;
|
|
601
|
+
// O atributo `Id` no XML vem com prefixo "NFe" (ex.: "NFe4126..."),
|
|
602
|
+
// mas a lib espera apenas a chave de 44 dígitos em `infNFe.Id` —
|
|
603
|
+
// ela própria re-prefixa com "NFe" ao calcular `chaveAcesso`.
|
|
604
|
+
if (typeof infNFe.Id === 'string' && /^NFe\d{44}$/.test(infNFe.Id)) {
|
|
605
|
+
infNFe.Id = infNFe.Id.slice(3);
|
|
606
|
+
}
|
|
607
|
+
this.unifyDocFields(infNFe.emit);
|
|
608
|
+
this.unifyDocFields(infNFe.dest);
|
|
609
|
+
this.unifyDocFields(infNFe.transp?.transporta);
|
|
610
|
+
// nItem é re-aplicado pela lib via $ em cada `det` quando é array.
|
|
611
|
+
// Por isso normalizamos sempre para array (no XML, com 1 item, viraria
|
|
612
|
+
// objeto único e a lib não re-aplicaria nItem, quebrando a validação XSD).
|
|
613
|
+
const det = infNFe.det;
|
|
614
|
+
if (Array.isArray(det)) {
|
|
615
|
+
det.forEach((d) => { if (d && typeof d === 'object')
|
|
616
|
+
delete d.nItem; });
|
|
617
|
+
}
|
|
618
|
+
else if (det && typeof det === 'object') {
|
|
619
|
+
delete det.nItem;
|
|
620
|
+
infNFe.det = [det];
|
|
621
|
+
}
|
|
622
|
+
const NFref = infNFe.NFref;
|
|
623
|
+
if (Array.isArray(NFref)) {
|
|
624
|
+
NFref.forEach((ref) => this.unifyDocFields(ref?.refNFP));
|
|
625
|
+
}
|
|
626
|
+
else if (NFref && typeof NFref === 'object') {
|
|
627
|
+
this.unifyDocFields(NFref.refNFP);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Em um nó (ex.: `emit`, `dest`), unifica `CNPJ`/`CPF`/`idEstrangeiro`
|
|
632
|
+
* em um único campo `CNPJCPF`, removendo as chaves originais. Mantém a
|
|
633
|
+
* primeira não-vazia encontrada na ordem `CNPJ → CPF → idEstrangeiro`.
|
|
634
|
+
*/
|
|
635
|
+
unifyDocFields(target) {
|
|
636
|
+
if (!target || typeof target !== 'object')
|
|
637
|
+
return;
|
|
638
|
+
if (target.CNPJCPF)
|
|
639
|
+
return;
|
|
640
|
+
const candidate = target.CNPJ || target.CPF || target.idEstrangeiro;
|
|
641
|
+
if (candidate) {
|
|
642
|
+
target.CNPJCPF = candidate;
|
|
643
|
+
}
|
|
644
|
+
delete target.CNPJ;
|
|
645
|
+
delete target.CPF;
|
|
646
|
+
delete target.idEstrangeiro;
|
|
647
|
+
}
|
|
459
648
|
}
|
|
460
649
|
|
|
461
650
|
class BaseNFE {
|
|
@@ -765,6 +954,7 @@ class GerarConsulta {
|
|
|
765
954
|
const config = this.environment.getConfig();
|
|
766
955
|
// Capturando o schema path
|
|
767
956
|
const { schemaPath } = this.utility.getSchema(metodo);
|
|
957
|
+
logger.info(xmlConsulta);
|
|
768
958
|
// Valida Schema apenas se o schema path estiver definido
|
|
769
959
|
if (schemaPath) {
|
|
770
960
|
if (config.lib?.useForSchemaValidation !== 'validateSchemaJsBased') {
|
|
@@ -1015,6 +1205,7 @@ const getSchema = (metodo) => {
|
|
|
1015
1205
|
RecepcaoEvento: `${pathSchemas}/envEvento_v1.00.xsd`,
|
|
1016
1206
|
NFeDistribuicaoDFe: `${pathSchemas}/distDFeInt_v1.01.xsd`,
|
|
1017
1207
|
NFEAutorizacao: `${pathSchemas}/enviNFe_v4.00.xsd`,
|
|
1208
|
+
NFeAutorizacao: `${pathSchemas}/enviNFe_v4.00.xsd`,
|
|
1018
1209
|
NFEInutilizacao: `${pathSchemas}/inutNFe_v4.00.xsd`,
|
|
1019
1210
|
NFERetAutorizacao: `${pathSchemas}/consReciNFe_v4.00.xsd`,
|
|
1020
1211
|
CTeDistribuicaoDFe: `${pathSchemas}/cte/distDFeInt_v1.00.xsd`,
|
|
@@ -3678,6 +3869,254 @@ const mountICMS = (icms) => {
|
|
|
3678
3869
|
return {};
|
|
3679
3870
|
};
|
|
3680
3871
|
|
|
3872
|
+
/*
|
|
3873
|
+
* This file is part of NFeWizard-io.
|
|
3874
|
+
*
|
|
3875
|
+
* NFeWizard-io is free software: you can redistribute it and/or modify
|
|
3876
|
+
* it under the terms of the GNU General Public License as published by
|
|
3877
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
3878
|
+
* (at your option) any later version.
|
|
3879
|
+
*
|
|
3880
|
+
* NFeWizard-io is distributed in the hope that it will be useful,
|
|
3881
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
3882
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
3883
|
+
* GNU General Public License for more details.
|
|
3884
|
+
*
|
|
3885
|
+
* You should have received a copy of the GNU General Public License
|
|
3886
|
+
* along with NFeWizard-io. If not, see <https://www.gnu.org/licenses/>.
|
|
3887
|
+
*/
|
|
3888
|
+
/** Remove o prefixo `{namespace}` deixando apenas o nome local do elemento. */
|
|
3889
|
+
function stripNamespace(name) {
|
|
3890
|
+
return name.replace(/^\{[^}]*\}/, '');
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Interpreta uma mensagem de erro do libxmljs2 / xsd-schema-validator e retorna
|
|
3894
|
+
* um `SchemaValidationIssue` com versão humanizada + metadados estruturados.
|
|
3895
|
+
*/
|
|
3896
|
+
function humanizeIssue(raw, line, column) {
|
|
3897
|
+
const issue = { raw, humanized: raw, line, column };
|
|
3898
|
+
// 1) Elemento com filhos ausentes
|
|
3899
|
+
// Element '{ns}Nome': Missing child element(s). Expected is one of ( {ns}a, {ns}b ).
|
|
3900
|
+
let m = raw.match(/Element\s+'([^']+)':\s*Missing child element\(s\)\.\s*Expected is one of\s*\(\s*([^)]+)\s*\)\.?/i);
|
|
3901
|
+
if (m) {
|
|
3902
|
+
const element = stripNamespace(m[1]);
|
|
3903
|
+
const expected = m[2].split(',').map((s) => stripNamespace(s.trim())).filter(Boolean);
|
|
3904
|
+
issue.element = element;
|
|
3905
|
+
issue.expected = expected;
|
|
3906
|
+
issue.humanized = `Elemento <${element}> está incompleto. Filho ausente, esperado um de: ${expected.join(', ')}.`;
|
|
3907
|
+
return issue;
|
|
3908
|
+
}
|
|
3909
|
+
// 2) Elemento não esperado
|
|
3910
|
+
// Element '{ns}Nome': This element is not expected. Expected is one of ( {ns}a, {ns}b ).
|
|
3911
|
+
m = raw.match(/Element\s+'([^']+)':\s*This element is not expected\.\s*(?:Expected is one of\s*\(\s*([^)]+)\s*\)\.?)?/i);
|
|
3912
|
+
if (m) {
|
|
3913
|
+
const element = stripNamespace(m[1]);
|
|
3914
|
+
issue.element = element;
|
|
3915
|
+
if (m[2]) {
|
|
3916
|
+
issue.expected = m[2].split(',').map((s) => stripNamespace(s.trim())).filter(Boolean);
|
|
3917
|
+
issue.humanized = `Elemento <${element}> inesperado nesta posição. Esperado um de: ${issue.expected.join(', ')}.`;
|
|
3918
|
+
}
|
|
3919
|
+
else {
|
|
3920
|
+
issue.humanized = `Elemento <${element}> inesperado nesta posição.`;
|
|
3921
|
+
}
|
|
3922
|
+
return issue;
|
|
3923
|
+
}
|
|
3924
|
+
// 3) Atributo inválido / faltando
|
|
3925
|
+
// Element '{ns}Nome', attribute 'attr': ...
|
|
3926
|
+
m = raw.match(/Element\s+'([^']+)',\s*attribute\s+'([^']+)':\s*(.+?)\.?$/i);
|
|
3927
|
+
if (m) {
|
|
3928
|
+
const element = stripNamespace(m[1]);
|
|
3929
|
+
const attribute = m[2];
|
|
3930
|
+
const desc = m[3];
|
|
3931
|
+
issue.element = element;
|
|
3932
|
+
issue.attribute = attribute;
|
|
3933
|
+
issue.humanized = `Elemento <${element}> — atributo "${attribute}": ${desc}.`;
|
|
3934
|
+
return issue;
|
|
3935
|
+
}
|
|
3936
|
+
// 4) Valor inválido para tipo
|
|
3937
|
+
// Element '{ns}Nome': [facet 'pattern'] The value '...' is not accepted by the pattern '...'.
|
|
3938
|
+
// Element '{ns}Nome': '...' is not a valid value of the atomic type 'TString'.
|
|
3939
|
+
m = raw.match(/Element\s+'([^']+)':\s*(?:\[facet[^\]]*\]\s*)?(.+?)\.?$/i);
|
|
3940
|
+
if (m) {
|
|
3941
|
+
const element = stripNamespace(m[1]);
|
|
3942
|
+
issue.element = element;
|
|
3943
|
+
issue.humanized = `Elemento <${element}>: ${m[2]}.`;
|
|
3944
|
+
return issue;
|
|
3945
|
+
}
|
|
3946
|
+
// 5) Sem padrão reconhecido — devolve raw como humanizado
|
|
3947
|
+
return issue;
|
|
3948
|
+
}
|
|
3949
|
+
/** Cria um issue "interno" quando o erro não veio do schema (ex.: XML mal formado). */
|
|
3950
|
+
function internalIssue(raw) {
|
|
3951
|
+
return { raw, humanized: raw };
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Monta o relatório textual estilo SEFAZ-RS pronto para `console.log`.
|
|
3955
|
+
*/
|
|
3956
|
+
function buildReport(metodo, schemaFile, errors) {
|
|
3957
|
+
const linhas = [];
|
|
3958
|
+
linhas.push('=== Resultado da Validação do Schema ===');
|
|
3959
|
+
linhas.push(`Tipo de Mensagem: ${metodo}`);
|
|
3960
|
+
linhas.push(`Schema XML: ${schemaFile}`);
|
|
3961
|
+
if (errors.length === 0) {
|
|
3962
|
+
linhas.push('Parser XML: Nenhum erro encontrado');
|
|
3963
|
+
linhas.push('Schema XML: Nenhum erro encontrado');
|
|
3964
|
+
linhas.push('Status: XML válido.');
|
|
3965
|
+
}
|
|
3966
|
+
else {
|
|
3967
|
+
linhas.push(`Status: ${errors.length} erro(s) de validação`);
|
|
3968
|
+
linhas.push('');
|
|
3969
|
+
errors.forEach((err, idx) => {
|
|
3970
|
+
const pos = err.line ? ` (linha ${err.line}${err.column ? `, col ${err.column}` : ''})` : '';
|
|
3971
|
+
linhas.push(` ${idx + 1}. ${err.humanized}${pos}`);
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
linhas.push('=========================================');
|
|
3975
|
+
return linhas.join('\n');
|
|
3976
|
+
}
|
|
3977
|
+
/** Converte a lista de erros em linhas para `console.table`. */
|
|
3978
|
+
function buildTableRows(errors) {
|
|
3979
|
+
if (errors.length === 0) {
|
|
3980
|
+
return [{ '#': 1, Linha: '', Elemento: '-', Mensagem: 'XML válido.' }];
|
|
3981
|
+
}
|
|
3982
|
+
return errors.map((err, idx) => ({
|
|
3983
|
+
'#': idx + 1,
|
|
3984
|
+
Linha: err.line ?? '',
|
|
3985
|
+
Elemento: err.element ?? '-',
|
|
3986
|
+
Mensagem: err.humanized,
|
|
3987
|
+
}));
|
|
3988
|
+
}
|
|
3989
|
+
function schemaFileName(schemaPath) {
|
|
3990
|
+
if (!schemaPath)
|
|
3991
|
+
return '-';
|
|
3992
|
+
const parts = schemaPath.split(/[\\/]/);
|
|
3993
|
+
return parts[parts.length - 1] || schemaPath;
|
|
3994
|
+
}
|
|
3995
|
+
function makeResult(success, metodo, schemaPath, errors) {
|
|
3996
|
+
const schema = schemaFileName(schemaPath);
|
|
3997
|
+
const report = buildReport(metodo, schema, errors);
|
|
3998
|
+
const message = success
|
|
3999
|
+
? 'XML válido.'
|
|
4000
|
+
: errors[0]?.humanized ?? 'Erro de validação não identificado.';
|
|
4001
|
+
return {
|
|
4002
|
+
success,
|
|
4003
|
+
message,
|
|
4004
|
+
errors,
|
|
4005
|
+
report,
|
|
4006
|
+
tableRows: buildTableRows(errors),
|
|
4007
|
+
metodo,
|
|
4008
|
+
schema,
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
4011
|
+
function runJsBased(xml, metodo) {
|
|
4012
|
+
return new Promise(async (resolve, reject) => {
|
|
4013
|
+
const { basePath, schemaPath } = getSchema(metodo);
|
|
4014
|
+
if (!schemaPath) {
|
|
4015
|
+
reject(makeResult(false, metodo, '', [internalIssue(`Schema XSD não encontrado para o método '${metodo}'. Verifique se o nome do método está correto.`)]));
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
try {
|
|
4019
|
+
const completeXSD = await xsdAssembler.assemble(schemaPath);
|
|
4020
|
+
const xmlDoc = libxmljs.parseXml(xml);
|
|
4021
|
+
const xsdDoc = libxmljs.parseXml(completeXSD, { baseUrl: `${basePath}/` });
|
|
4022
|
+
const isValid = xmlDoc.validate(xsdDoc);
|
|
4023
|
+
if (isValid) {
|
|
4024
|
+
resolve(makeResult(true, metodo, schemaPath, []));
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
const errors = (xmlDoc.validationErrors || []).map((e) => humanizeIssue(String(e.message ?? e).trim(), e.line, e.column));
|
|
4028
|
+
reject(makeResult(false, metodo, schemaPath, errors.length ? errors : [internalIssue('XML inválido (sem detalhes do validador).')]));
|
|
4029
|
+
}
|
|
4030
|
+
catch (error) {
|
|
4031
|
+
reject(makeResult(false, metodo, schemaPath, [internalIssue(String(error?.message ?? error))]));
|
|
4032
|
+
}
|
|
4033
|
+
});
|
|
4034
|
+
}
|
|
4035
|
+
function runJavaBased(xml, metodo) {
|
|
4036
|
+
return new Promise((resolve, reject) => {
|
|
4037
|
+
const { schemaPath } = getSchema(metodo);
|
|
4038
|
+
if (!schemaPath) {
|
|
4039
|
+
reject(makeResult(false, metodo, '', [internalIssue(`Schema XSD não encontrado para o método '${metodo}'. Verifique se o nome do método está correto.`)]));
|
|
4040
|
+
return;
|
|
4041
|
+
}
|
|
4042
|
+
try {
|
|
4043
|
+
xsdValidator.validateXML(xml, schemaPath, (err, validationResult) => {
|
|
4044
|
+
if (err) {
|
|
4045
|
+
reject(makeResult(false, metodo, schemaPath, [humanizeIssue(String(err.message ?? err))]));
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
if (!validationResult?.valid) {
|
|
4049
|
+
const msgs = validationResult?.messages ?? [];
|
|
4050
|
+
const errors = msgs.length
|
|
4051
|
+
? msgs.map((m) => humanizeIssue(String(m)))
|
|
4052
|
+
: [internalIssue('XML inválido (sem detalhes do validador).')];
|
|
4053
|
+
reject(makeResult(false, metodo, schemaPath, errors));
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
resolve(makeResult(true, metodo, schemaPath, []));
|
|
4057
|
+
});
|
|
4058
|
+
}
|
|
4059
|
+
catch (error) {
|
|
4060
|
+
reject(makeResult(false, metodo, schemaPath, [internalIssue(String(error?.message ?? error))]));
|
|
4061
|
+
}
|
|
4062
|
+
});
|
|
4063
|
+
}
|
|
4064
|
+
/**
|
|
4065
|
+
* Valida um XML contra o schema XSD correspondente ao método fiscal informado.
|
|
4066
|
+
*
|
|
4067
|
+
* ### Seleção do validador (ordem de prioridade)
|
|
4068
|
+
* 1. `options.validator` — escolha explícita do caller.
|
|
4069
|
+
* 2. `options.environment.getConfig().lib?.useForSchemaValidation` — valor definido
|
|
4070
|
+
* na inicialização da lib.
|
|
4071
|
+
* 3. `'validateSchemaJsBased'` — padrão (sem JDK).
|
|
4072
|
+
*
|
|
4073
|
+
* @param xml - String XML a ser validada.
|
|
4074
|
+
* @param metodo - Nome do método/operação fiscal (ex.: `'NFeAutorizacao'`).
|
|
4075
|
+
* @param options - Opções opcionais: `validator` e/ou `environment`.
|
|
4076
|
+
*
|
|
4077
|
+
* @example
|
|
4078
|
+
* // Sem opções: usa validateSchemaJsBased
|
|
4079
|
+
* await NFE_SchemaValidate(xml, 'NFeAutorizacao');
|
|
4080
|
+
*
|
|
4081
|
+
* @example
|
|
4082
|
+
* // Passando o environment inicializado pela lib (lê useForSchemaValidation do config)
|
|
4083
|
+
* await NFE_SchemaValidate(xml, 'NFeAutorizacao', { environment });
|
|
4084
|
+
*
|
|
4085
|
+
* @example
|
|
4086
|
+
* // Forçando um validador específico
|
|
4087
|
+
* await NFE_SchemaValidate(xml, 'NFeAutorizacao', { validator: 'validateSchemaJavaBased' });
|
|
4088
|
+
*/
|
|
4089
|
+
function NFE_SchemaValidate(xml, metodo, options) {
|
|
4090
|
+
let chosen = 'validateSchemaJsBased';
|
|
4091
|
+
if (options?.validator) {
|
|
4092
|
+
chosen = options.validator;
|
|
4093
|
+
}
|
|
4094
|
+
else if (options?.environment) {
|
|
4095
|
+
const fromConfig = options.environment.getConfig()?.lib?.useForSchemaValidation;
|
|
4096
|
+
if (fromConfig)
|
|
4097
|
+
chosen = fromConfig;
|
|
4098
|
+
}
|
|
4099
|
+
const xmlNormalizado = wrapEnviNFeIfNeeded(xml, metodo);
|
|
4100
|
+
return chosen === 'validateSchemaJavaBased'
|
|
4101
|
+
? runJavaBased(xmlNormalizado, metodo)
|
|
4102
|
+
: runJsBased(xmlNormalizado, metodo);
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Para os métodos de autorização de NF-e, o schema XSD (`enviNFe_v4.00.xsd`)
|
|
4106
|
+
* exige que o elemento raiz seja `<enviNFe>`. Quando o caller passa apenas a
|
|
4107
|
+
* árvore `<NFe>`, envelopamos automaticamente em `<enviNFe>` com `idLote=1` e
|
|
4108
|
+
* `indSinc=1` para que a validação funcione.
|
|
4109
|
+
*/
|
|
4110
|
+
function wrapEnviNFeIfNeeded(xml, metodo) {
|
|
4111
|
+
if (metodo !== 'NFEAutorizacao' && metodo !== 'NFeAutorizacao')
|
|
4112
|
+
return xml;
|
|
4113
|
+
const semDecl = xml.replace(/^\s*<\?xml[^?]*\?>\s*/, '').trimStart();
|
|
4114
|
+
if (/^<NFe[\s>]/.test(semDecl)) {
|
|
4115
|
+
return `<enviNFe versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe"><idLote>1</idLote><indSinc>1</indSinc>${semDecl}</enviNFe>`;
|
|
4116
|
+
}
|
|
4117
|
+
return xml;
|
|
4118
|
+
}
|
|
4119
|
+
|
|
3681
4120
|
/*
|
|
3682
4121
|
* This file is part of NFeWizard-io.
|
|
3683
4122
|
*
|
|
@@ -3863,12 +4302,35 @@ class LoadCertificate {
|
|
|
3863
4302
|
this.certificate = '';
|
|
3864
4303
|
this.cert_key = '';
|
|
3865
4304
|
}
|
|
3866
|
-
|
|
4305
|
+
/**
|
|
4306
|
+
* Resolve o conteúdo binário do PFX a partir do `pathCertificado` informado em
|
|
4307
|
+
* config. Aceita `string` (caminho), `Buffer` ou `NodeJS.ReadableStream`.
|
|
4308
|
+
*/
|
|
4309
|
+
async resolvePfxBuffer() {
|
|
4310
|
+
const input = this.config.dfe.pathCertificado;
|
|
4311
|
+
if (typeof input === 'string') {
|
|
4312
|
+
return fs.readFileSync(input);
|
|
4313
|
+
}
|
|
4314
|
+
if (Buffer.isBuffer(input)) {
|
|
4315
|
+
return input;
|
|
4316
|
+
}
|
|
4317
|
+
if (input && typeof input.pipe === 'function') {
|
|
4318
|
+
const stream = input;
|
|
4319
|
+
return await new Promise((resolve, reject) => {
|
|
4320
|
+
const chunks = [];
|
|
4321
|
+
stream.on('data', (chunk) => {
|
|
4322
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
4323
|
+
});
|
|
4324
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
4325
|
+
stream.on('error', (err) => reject(err));
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
throw new Error('pathCertificado deve ser uma string (path), Buffer ou Stream legível.');
|
|
4329
|
+
}
|
|
4330
|
+
loadCertificateWithPEM(pfxFile) {
|
|
3867
4331
|
return new Promise((resolve, reject) => {
|
|
3868
4332
|
try {
|
|
3869
|
-
const pfxPath = this.config.dfe.pathCertificado;
|
|
3870
4333
|
const pfxPassword = this.config.dfe.senhaCertificado;
|
|
3871
|
-
const pfxFile = fs.readFileSync(pfxPath);
|
|
3872
4334
|
const certsDir = path.resolve(baseDir, dir);
|
|
3873
4335
|
// Read CA certificates if directory exists and has files
|
|
3874
4336
|
let caCerts = [];
|
|
@@ -3927,13 +4389,10 @@ class LoadCertificate {
|
|
|
3927
4389
|
}
|
|
3928
4390
|
});
|
|
3929
4391
|
}
|
|
3930
|
-
loadCertificateWithNodeForge() {
|
|
4392
|
+
loadCertificateWithNodeForge(pfxFile) {
|
|
3931
4393
|
return new Promise((resolve, reject) => {
|
|
3932
4394
|
try {
|
|
3933
|
-
const pfxPath = this.config.dfe.pathCertificado;
|
|
3934
4395
|
const pfxPassword = this.config.dfe.senhaCertificado;
|
|
3935
|
-
// Lê o arquivo PFX
|
|
3936
|
-
const pfxFile = fs.readFileSync(pfxPath);
|
|
3937
4396
|
// Decodifica o arquivo PFX (PKCS#12)
|
|
3938
4397
|
const p12Asn1 = forge.asn1.fromDer(pfxFile.toString('binary'));
|
|
3939
4398
|
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, pfxPassword);
|
|
@@ -4005,15 +4464,16 @@ class LoadCertificate {
|
|
|
4005
4464
|
logger.info('Validando certificado', {
|
|
4006
4465
|
context: 'LoadCertificate',
|
|
4007
4466
|
});
|
|
4467
|
+
const pfxFile = await this.resolvePfxBuffer();
|
|
4008
4468
|
if (this.config.lib?.useOpenSSL || this.config.lib?.useOpenSSL === undefined) {
|
|
4009
|
-
const { agent } = await this.loadCertificateWithPEM();
|
|
4469
|
+
const { agent } = await this.loadCertificateWithPEM(pfxFile);
|
|
4010
4470
|
return {
|
|
4011
4471
|
certificate: this.certificate,
|
|
4012
4472
|
cert_key: this.cert_key,
|
|
4013
4473
|
agent
|
|
4014
4474
|
};
|
|
4015
4475
|
}
|
|
4016
|
-
const { agent } = await this.loadCertificateWithNodeForge();
|
|
4476
|
+
const { agent } = await this.loadCertificateWithNodeForge(pfxFile);
|
|
4017
4477
|
return {
|
|
4018
4478
|
certificate: this.certificate,
|
|
4019
4479
|
cert_key: this.cert_key,
|
|
@@ -4105,6 +4565,7 @@ exports.GerarConsulta = GerarConsulta;
|
|
|
4105
4565
|
exports.HttpClientBuilder = HttpClientBuilder;
|
|
4106
4566
|
exports.JsonArrayTransport = JsonArrayTransport;
|
|
4107
4567
|
exports.LoadCertificate = LoadCertificate;
|
|
4568
|
+
exports.NFE_SchemaValidate = NFE_SchemaValidate;
|
|
4108
4569
|
exports.SaveFiles = SaveFiles;
|
|
4109
4570
|
exports.Utility = Utility;
|
|
4110
4571
|
exports.ValidaCPFCNPJ = ValidaCPFCNPJ;
|