@sinete/nfe 0.0.0 → 0.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/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +147 -1
- package/dist/build/build.d.ts +127 -0
- package/dist/build/build.d.ts.map +1 -0
- package/dist/build/destinatario.d.ts +19 -0
- package/dist/build/destinatario.d.ts.map +1 -0
- package/dist/build/ibscbs.d.ts +27 -0
- package/dist/build/ibscbs.d.ts.map +1 -0
- package/dist/build/icms.d.ts +42 -0
- package/dist/build/icms.d.ts.map +1 -0
- package/dist/build/nfce.d.ts +122 -0
- package/dist/build/nfce.d.ts.map +1 -0
- package/dist/build/pl.d.ts +19 -0
- package/dist/build/pl.d.ts.map +1 -0
- package/dist/build/rejeicoes.d.ts +52 -0
- package/dist/build/rejeicoes.d.ts.map +1 -0
- package/dist/build/tributos.d.ts +45 -0
- package/dist/build/tributos.d.ts.map +1 -0
- package/dist/build/values.d.ts +48 -0
- package/dist/build/values.d.ts.map +1 -0
- package/dist/decimal.d.ts +66 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/format.d.ts +48 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/ibs-cbs.d.ts +14 -0
- package/dist/ibs-cbs.d.ts.map +1 -0
- package/dist/ibs-cbs.js +43 -0
- package/dist/ibs-cbs.js.map +10 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4370 -0
- package/dist/index.js.map +33 -0
- package/dist/issues.d.ts +24 -0
- package/dist/issues.d.ts.map +1 -0
- package/dist/model.d.ts +861 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/ports.d.ts +109 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/rotulo.d.ts +12 -0
- package/dist/rotulo.d.ts.map +1 -0
- package/dist/rtc.d.ts +65 -0
- package/dist/rtc.d.ts.map +1 -0
- package/dist/services/client.d.ts +273 -0
- package/dist/services/client.d.ts.map +1 -0
- package/dist/services/gzip.d.ts +7 -0
- package/dist/services/gzip.d.ts.map +1 -0
- package/dist/services/index.d.ts +14 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/outcome.d.ts +15 -0
- package/dist/services/outcome.d.ts.map +1 -0
- package/dist/services/proc.d.ts +41 -0
- package/dist/services/proc.d.ts.map +1 -0
- package/dist/services/recuperar.d.ts +28 -0
- package/dist/services/recuperar.d.ts.map +1 -0
- package/dist/services/resolver.d.ts +88 -0
- package/dist/services/resolver.d.ts.map +1 -0
- package/dist/services/soap.d.ts +43 -0
- package/dist/services/soap.d.ts.map +1 -0
- package/dist/time.d.ts +12 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/versao-gerada.d.ts +3 -0
- package/dist/versao-gerada.d.ts.map +1 -0
- package/package.json +58 -3
- package/src/build/build.ts +1486 -0
- package/src/build/destinatario.ts +212 -0
- package/src/build/ibscbs.ts +131 -0
- package/src/build/icms.ts +645 -0
- package/src/build/nfce.ts +425 -0
- package/src/build/pl.ts +73 -0
- package/src/build/rejeicoes.ts +142 -0
- package/src/build/tributos.ts +217 -0
- package/src/build/values.ts +126 -0
- package/src/data/arredondamento.json +30 -0
- package/src/data/cstat.json +36 -0
- package/src/data/fusos.json +17 -0
- package/src/data/nfce-urls.json +170 -0
- package/src/data/produtor-rural.json +17 -0
- package/src/data/reforma.json +6 -0
- package/src/data/resp-tec.json +13 -0
- package/src/data/servicos.json +44 -0
- package/src/data/suframa.json +7 -0
- package/src/decimal.ts +231 -0
- package/src/format.ts +64 -0
- package/src/ibs-cbs.ts +89 -0
- package/src/index.ts +153 -0
- package/src/issues.ts +80 -0
- package/src/model.ts +1033 -0
- package/src/ports.ts +96 -0
- package/src/rotulo.ts +137 -0
- package/src/rtc.ts +305 -0
- package/src/services/client.ts +1085 -0
- package/src/services/gzip.ts +26 -0
- package/src/services/index.ts +43 -0
- package/src/services/outcome.ts +50 -0
- package/src/services/proc.ts +164 -0
- package/src/services/recuperar.ts +91 -0
- package/src/services/resolver.ts +127 -0
- package/src/services/soap.ts +133 -0
- package/src/time.ts +23 -0
- package/src/versao-gerada.ts +3 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Descompressão do `docZip` da Distribuição DF-e (gzip em base64, NT 2014.002). Usa o `DecompressionStream` da
|
|
3
|
+
* plataforma (Node 18+, Bun, Deno e browsers), sem dependência.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ProtocolError, UnsupportedError } from '@sinete/core';
|
|
7
|
+
import { base64Decode } from '@sinete/core/xml';
|
|
8
|
+
|
|
9
|
+
/** Base64 de um gzip para o texto UTF-8 de dentro. */
|
|
10
|
+
export async function gunzipBase64(b64: string): Promise<string> {
|
|
11
|
+
const Ds = (globalThis as { DecompressionStream?: typeof DecompressionStream }).DecompressionStream;
|
|
12
|
+
if (Ds === undefined)
|
|
13
|
+
throw new UnsupportedError('DecompressionStream indisponível nesta runtime; docZip não pode ser lido');
|
|
14
|
+
let bytes: Uint8Array<ArrayBuffer>;
|
|
15
|
+
try {
|
|
16
|
+
bytes = base64Decode(b64.trim());
|
|
17
|
+
} catch (cause) {
|
|
18
|
+
throw new ProtocolError('docZip com base64 inválido', { cause });
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new Ds('gzip'));
|
|
22
|
+
return await new Response(stream).text();
|
|
23
|
+
} catch (cause) {
|
|
24
|
+
throw new ProtocolError('docZip não é um gzip válido', { cause });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serviços da NF-e (webservices 4.00 da SEFAZ e do Ambiente Nacional). Veja `client.ts` para o contrato de cada
|
|
3
|
+
* operação e `resolver.ts` para a regra de idempotência da autorização.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
AutorDocumento,
|
|
8
|
+
AutorizacaoOutcome,
|
|
9
|
+
AutorizarOpcoes,
|
|
10
|
+
Cadastro,
|
|
11
|
+
CadastroPedido,
|
|
12
|
+
CancelamentoPedido,
|
|
13
|
+
CancelamentoSubstituicaoPedido,
|
|
14
|
+
CartaCorrecaoPedido,
|
|
15
|
+
ConsultaNfe,
|
|
16
|
+
ConsultaOutcome,
|
|
17
|
+
ConsultaReciboOpcoes,
|
|
18
|
+
Distribuicao,
|
|
19
|
+
DistribuicaoConsulta,
|
|
20
|
+
DistribuicaoOpcoes,
|
|
21
|
+
DocumentoDistribuido,
|
|
22
|
+
EventoOutcome,
|
|
23
|
+
EventoRegistrado,
|
|
24
|
+
Inutilizacao,
|
|
25
|
+
InutilizacaoOutcome,
|
|
26
|
+
InutilizacaoPedido,
|
|
27
|
+
ManifestacaoPedido,
|
|
28
|
+
ManifestacaoTipo,
|
|
29
|
+
NfeClient,
|
|
30
|
+
NfeClientOptions,
|
|
31
|
+
PoliticaRecibo,
|
|
32
|
+
ProtocoloNfe,
|
|
33
|
+
Sleep,
|
|
34
|
+
StatusServico,
|
|
35
|
+
} from './client.ts';
|
|
36
|
+
export { autorizadorContingencia, createNfeClient } from './client.ts';
|
|
37
|
+
export { gunzipBase64 } from './gzip.ts';
|
|
38
|
+
export type { DocumentoAssinado } from './proc.ts';
|
|
39
|
+
export { documentoAssinado, nfeAssinadaDoProc, sliceElement } from './proc.ts';
|
|
40
|
+
export type { RecuperacaoEvento } from './recuperar.ts';
|
|
41
|
+
export { recuperarEventoRegistrado } from './recuperar.ts';
|
|
42
|
+
export type { ConteudoDoProtocolo, ResolucaoEnvio } from './resolver.ts';
|
|
43
|
+
export { chaveDaDuplicidade, resolverEnvioSemResposta } from './resolver.ts';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desfechos da SEFAZ a partir da tabela versionada de `cStat` (`data/cstat.json`, MOC 7.0 Anexo I, tabela 4.4.1).
|
|
3
|
+
* Nenhuma lista de códigos fica no código: quem decide se um `cStat` é autorização, denegação ou pendência é o dado.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Rejected, RejectionHint, SefazStatus } from '@sinete/core';
|
|
7
|
+
import { rejected } from '@sinete/core';
|
|
8
|
+
import { enrichRejected } from '@sinete/rejeicoes';
|
|
9
|
+
import table from '../data/cstat.json' with { type: 'json' };
|
|
10
|
+
|
|
11
|
+
/** Classes de `cStat` que a tabela descreve. */
|
|
12
|
+
export type CStatClasse =
|
|
13
|
+
| 'autorizada'
|
|
14
|
+
| 'denegada'
|
|
15
|
+
| 'cancelada'
|
|
16
|
+
| 'loteRecebido'
|
|
17
|
+
| 'loteProcessado'
|
|
18
|
+
| 'loteEmProcessamento'
|
|
19
|
+
| 'loteNaoLocalizado'
|
|
20
|
+
| 'servicoEmOperacao'
|
|
21
|
+
| 'inutilizacaoHomologada'
|
|
22
|
+
| 'eventoRegistrado'
|
|
23
|
+
| 'loteEventoProcessado'
|
|
24
|
+
| 'cadastroEncontrado'
|
|
25
|
+
| 'distribuicaoNenhumDocumento'
|
|
26
|
+
| 'distribuicaoDocumentos'
|
|
27
|
+
| 'duplicidade'
|
|
28
|
+
| 'duplicidadeChaveDiferente'
|
|
29
|
+
| 'naoConsta'
|
|
30
|
+
| 'outraNfeNoNumero'
|
|
31
|
+
| 'consumoIndevido';
|
|
32
|
+
|
|
33
|
+
const CLASSES: Readonly<Record<CStatClasse, readonly string[]>> = table;
|
|
34
|
+
const DICAS: Readonly<Record<string, RejectionHint>> = table.dicas;
|
|
35
|
+
|
|
36
|
+
/** O `cStat` pertence à classe da tabela. */
|
|
37
|
+
export function cstatEm(cStat: string, classe: CStatClasse): boolean {
|
|
38
|
+
return CLASSES[classe].includes(cStat);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Desfecho `rejected` enriquecido pelo `@sinete/rejeicoes`; sem curadoria lá, usa a dica própria do `@sinete/nfe`
|
|
43
|
+
* (`dicas` em `data/cstat.json`) quando houver.
|
|
44
|
+
*/
|
|
45
|
+
export function rejeitado(status: SefazStatus): Rejected {
|
|
46
|
+
const r = enrichRejected(rejected(status));
|
|
47
|
+
if (r.hint !== undefined) return r;
|
|
48
|
+
const dica = Object.hasOwn(DICAS, status.cStat) ? DICAS[status.cStat] : undefined;
|
|
49
|
+
return dica === undefined ? r : rejected(status, dica);
|
|
50
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Montagem dos documentos processados (`nfeProc`, `procEventoNFe`, `ProcInutNFe`) por splice de texto. O documento
|
|
3
|
+
* assinado entra byte a byte como foi assinado e o protocolo entra como fatia da resposta recebida: nada é reparseado
|
|
4
|
+
* para gerar saída nem reserializado (invariante do repositório, ADR 0003).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ConfigError, ProtocolError } from '@sinete/core';
|
|
8
|
+
import type { XmlDocument, XmlElement } from '@sinete/core/xml';
|
|
9
|
+
import {
|
|
10
|
+
attributeOf,
|
|
11
|
+
descendants,
|
|
12
|
+
firstChild,
|
|
13
|
+
inScopeNamespaces,
|
|
14
|
+
parseXml,
|
|
15
|
+
textOf,
|
|
16
|
+
XMLDSIG_NS,
|
|
17
|
+
} from '@sinete/core/xml';
|
|
18
|
+
|
|
19
|
+
/** Namespace dos documentos da NF-e. */
|
|
20
|
+
export const NFE_NS = 'http://www.portalfiscal.inf.br/nfe';
|
|
21
|
+
|
|
22
|
+
const XML_DECL = /^?<\?xml[^?]*\?>\s*/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recorta o elemento da fonte, acrescentando na tag de abertura só as declarações de namespace que ele usa e que
|
|
26
|
+
* estão em ancestrais fora do recorte. O default entra apenas quando difere de `parentDefaultNs` (o default do
|
|
27
|
+
* envelope onde a fatia vai morar).
|
|
28
|
+
*/
|
|
29
|
+
export function sliceElement(doc: XmlDocument, el: XmlElement, parentDefaultNs: string = NFE_NS): string {
|
|
30
|
+
const slice = doc.source.slice(el.start, el.end);
|
|
31
|
+
// Só os prefixos usados por algum elemento ou atributo cuja declaração está fora do recorte: um prefixo redeclarado
|
|
32
|
+
// dentro dele não pode ganhar outra declaração na raiz, que mudaria o C14N inclusivo de um irmão assinado.
|
|
33
|
+
//
|
|
34
|
+
// Declaração herdada e não usada (xmlns:soap, xmlns:xsi, xmlns:xsd do envelope da resposta) fica de fora de
|
|
35
|
+
// propósito. O que a fatia leva é o contexto em que o autorizador montou o documento, não o do transporte: o
|
|
36
|
+
// protocolo vai para o proc como o leiaute pede, e é assim que o proc confere depois. Copiar para a fatia todo
|
|
37
|
+
// namespace em escopo mudaria o C14N inclusivo do documento assinado pelo emitente, que nunca viu o envelope.
|
|
38
|
+
const used = new Set<string>();
|
|
39
|
+
const cobertoDentro = (d: XmlElement, p: string): boolean => {
|
|
40
|
+
for (let e: XmlElement | null = d; e; e = e === el ? null : e.parent) if (e.namespaces.has(p)) return true;
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
for (const d of descendants(el)) {
|
|
44
|
+
const prefixos = [d.prefix, ...d.attributes.map((a) => a.prefix)].filter((p) => p !== '' && p !== 'xml');
|
|
45
|
+
for (const p of prefixos) if (!cobertoDentro(d, p)) used.add(p);
|
|
46
|
+
}
|
|
47
|
+
const extra: string[] = [];
|
|
48
|
+
const inScope = new Map<string, string>();
|
|
49
|
+
for (let e: XmlElement | null = el.parent; e; e = e.parent) {
|
|
50
|
+
for (const [p, u] of e.namespaces) if (!inScope.has(p)) inScope.set(p, u);
|
|
51
|
+
}
|
|
52
|
+
// O default herdado de fora entra quando algum elemento sem prefixo do recorte (a raiz ou um descendente, inclusive
|
|
53
|
+
// sob uma raiz prefixada) depende dele e ele difere do default do envelope de destino.
|
|
54
|
+
const herdado = inScope.get('') ?? '';
|
|
55
|
+
if (herdado !== parentDefaultNs && usaDefaultHerdado(el)) extra.push(` xmlns="${herdado}"`);
|
|
56
|
+
for (const p of [...used].sort()) {
|
|
57
|
+
const uri = inScope.get(p);
|
|
58
|
+
if (uri === undefined) throw new ProtocolError(`prefixo ${p} sem declaração no recorte de ${el.name}`);
|
|
59
|
+
extra.push(` xmlns:${p}="${uri}"`);
|
|
60
|
+
}
|
|
61
|
+
if (extra.length === 0) return slice;
|
|
62
|
+
const at = 1 + el.name.length;
|
|
63
|
+
return slice.slice(0, at) + extra.join('') + slice.slice(at);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Algum elemento sem prefixo do recorte resolve o default por uma declaração de fora do recorte. */
|
|
67
|
+
function usaDefaultHerdado(raiz: XmlElement): boolean {
|
|
68
|
+
for (const d of descendants(raiz)) {
|
|
69
|
+
if (d.prefix !== '') continue;
|
|
70
|
+
let coberto = false;
|
|
71
|
+
for (let e: XmlElement | null = d; e; e = e === raiz ? null : e.parent) {
|
|
72
|
+
if (e.namespaces.has('')) {
|
|
73
|
+
coberto = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!coberto) return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Documento assinado já conferido: a string como veio (sem a declaração XML) e o que se lê dela. */
|
|
83
|
+
export interface DocumentoAssinado {
|
|
84
|
+
/** O texto que vai no envelope, byte a byte o assinado (só a declaração XML inicial removida). */
|
|
85
|
+
readonly xml: string;
|
|
86
|
+
/** `Id` do elemento assinado (`NFe3526...`, `ID110111...`). */
|
|
87
|
+
readonly id: string;
|
|
88
|
+
/** DigestValue da assinatura (base64). */
|
|
89
|
+
readonly digestValue: string;
|
|
90
|
+
readonly doc: XmlDocument;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Confere que `xml` é um documento `raiz` assinado (no namespace da NF-e, com `Signature` referenciando o filho
|
|
95
|
+
* `elemento`) e devolve a string sem a declaração XML. Lança `ConfigError` para qualquer outra coisa: o serviço nunca
|
|
96
|
+
* "conserta" o documento de quem chama.
|
|
97
|
+
*/
|
|
98
|
+
export function documentoAssinado(xml: string, raiz: string, elemento: string): DocumentoAssinado {
|
|
99
|
+
const text = xml.replace(XML_DECL, '');
|
|
100
|
+
let doc: XmlDocument;
|
|
101
|
+
try {
|
|
102
|
+
doc = parseXml(text);
|
|
103
|
+
} catch (cause) {
|
|
104
|
+
throw new ConfigError(`${raiz} assinado malformado`, { cause });
|
|
105
|
+
}
|
|
106
|
+
if (doc.root.local !== raiz || doc.root.ns !== NFE_NS) {
|
|
107
|
+
throw new ConfigError(`esperado <${raiz}> no namespace da NF-e, veio <${doc.root.name}>`);
|
|
108
|
+
}
|
|
109
|
+
// O envelope (nfeProc, procEventoNFe) declara o default da NF-e; o C14N inclusivo herda os namespaces dos
|
|
110
|
+
// ancestrais, então a raiz assinada precisa declarar ela mesma esse default, ou a assinatura deixa de conferir
|
|
111
|
+
// dentro do envelope. Não se conserta o documento de quem chama: recusa.
|
|
112
|
+
if (doc.root.namespaces.get('') !== NFE_NS) {
|
|
113
|
+
throw new ConfigError(`<${raiz}> assinado precisa declarar xmlns="${NFE_NS}" na própria raiz`);
|
|
114
|
+
}
|
|
115
|
+
const alvo = firstChild(doc.root, elemento, NFE_NS);
|
|
116
|
+
const id = alvo ? attributeOf(alvo, 'Id') : undefined;
|
|
117
|
+
const sig = firstChild(doc.root, 'Signature', XMLDSIG_NS);
|
|
118
|
+
const digest = sig && descendantText(sig, 'DigestValue');
|
|
119
|
+
if (!id || digest === undefined) throw new ConfigError(`${raiz} sem ${elemento} identificado ou sem assinatura`);
|
|
120
|
+
return { xml: text, id, digestValue: digest, doc };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function descendantText(el: XmlElement, local: string): string | undefined {
|
|
124
|
+
for (const d of descendants(el)) if (d.local === local && d.ns === XMLDSIG_NS) return textOf(d).trim();
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* NF-e assinada de dentro de um `nfeProc` (ou a própria NF-e assinada), como fatia do texto e sem a declaração XML,
|
|
130
|
+
* pronta para `consultar`, `resolverEnvioSemResposta` e a retomada, que recusam raiz sem `xmlns` próprio. O `NFe` dentro
|
|
131
|
+
* do proc herda os namespaces do envelope; eles são declarados na raiz da fatia. O C14N inclusivo do `infNFe` já os
|
|
132
|
+
* enxergava em escopo, então o digest e a assinatura são os mesmos dentro e fora do proc. Nada mais muda nos bytes.
|
|
133
|
+
* Lança `ConfigError` quando não há NF-e assinada (proc de outro documento, NFe sem assinatura, XML malformado).
|
|
134
|
+
*/
|
|
135
|
+
export function nfeAssinadaDoProc(xml: string): string {
|
|
136
|
+
const text = xml.replace(XML_DECL, '');
|
|
137
|
+
let doc: XmlDocument;
|
|
138
|
+
try {
|
|
139
|
+
doc = parseXml(text);
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
throw new ConfigError('nfeProc malformado', { cause });
|
|
142
|
+
}
|
|
143
|
+
const noProc =
|
|
144
|
+
doc.root.local === 'nfeProc' && doc.root.ns === NFE_NS ? firstChild(doc.root, 'NFe', NFE_NS) : undefined;
|
|
145
|
+
const el = noProc ?? (doc.root.local === 'NFe' && doc.root.ns === NFE_NS ? doc.root : undefined);
|
|
146
|
+
if (el === undefined) throw new ConfigError(`esperado <nfeProc> ou <NFe> assinada, veio <${doc.root.name}>`);
|
|
147
|
+
const fatia = doc.source.slice(el.start, el.end);
|
|
148
|
+
const herdados = [...inScopeNamespaces(el)].filter(([p]) => p !== 'xml' && !el.namespaces.has(p));
|
|
149
|
+
const decl = herdados
|
|
150
|
+
.map(
|
|
151
|
+
([p, uri]) =>
|
|
152
|
+
` ${p === '' ? 'xmlns' : `xmlns:${p}`}="${uri.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"')}"`,
|
|
153
|
+
)
|
|
154
|
+
.join('');
|
|
155
|
+
const at = 1 + el.name.length;
|
|
156
|
+
const assinado = decl === '' ? fatia : fatia.slice(0, at) + decl + fatia.slice(at);
|
|
157
|
+
// Confere raiz, Id e assinatura como a retomada vai conferir: o que sai daqui serve direto nela.
|
|
158
|
+
return documentoAssinado(assinado, 'NFe', 'infNFe').xml;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** `<raiz xmlns versao>` + partes + `</raiz>`, sem tocar nas partes. */
|
|
162
|
+
export function envelope(raiz: string, versao: string, partes: readonly string[]): string {
|
|
163
|
+
return `<${raiz} xmlns="${NFE_NS}" versao="${versao}">${partes.join('')}</${raiz}>`;
|
|
164
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recuperação de um evento que a SEFAZ registrou e cuja resposta se perdeu.
|
|
3
|
+
*
|
|
4
|
+
* Um pedido de evento sem resposta (timeout, conexão caída), ou respondido com 573 (duplicidade de evento) ou 580
|
|
5
|
+
* (evento já registrado no contexto do cancelamento), pode ter sido registrado. O `cStat` sozinho não prova: 573 e
|
|
6
|
+
* 580 dizem que algo foi registrado antes, não que foi este evento, com este conteúdo e para esta chave. A prova é a
|
|
7
|
+
* consulta da chave, que devolve os `procEventoNFe` que a SEFAZ tem (MOC 7.0, Visão Geral, consulta protocolo). Esta
|
|
8
|
+
* função lê o evento de lá e só o dá como registrado quando o retorno dele diz 135, 136 ou 155 para a mesma chave e o
|
|
9
|
+
* mesmo tipo.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { XmlDocument, XmlElement } from '@sinete/core/xml';
|
|
13
|
+
import { firstChild, parseXml, textOf } from '@sinete/core/xml';
|
|
14
|
+
import type { ConsultaOutcome, EventoRegistrado, NfeClient } from './client.ts';
|
|
15
|
+
import { cstatEm } from './outcome.ts';
|
|
16
|
+
import { NFE_NS, sliceElement } from './proc.ts';
|
|
17
|
+
|
|
18
|
+
/** Resultado da recuperação: o evento registrado, com a consulta que o prova, ou só a consulta. */
|
|
19
|
+
export type RecuperacaoEvento =
|
|
20
|
+
| { readonly registrado: true; readonly evento: EventoRegistrado; readonly consulta: ConsultaOutcome }
|
|
21
|
+
| { readonly registrado: false; readonly consulta: ConsultaOutcome };
|
|
22
|
+
|
|
23
|
+
/** Texto do filho `local` no namespace da NF-e, se houver. */
|
|
24
|
+
function campo(el: XmlElement, local: string): string | undefined {
|
|
25
|
+
const c = firstChild(el, local, NFE_NS);
|
|
26
|
+
return c === undefined ? undefined : textOf(c).trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Lê um `procEventoNFe` da consulta; `undefined` para o que não é um evento com retorno legível. */
|
|
30
|
+
function lerProcEvento(xml: string): EventoRegistrado | undefined {
|
|
31
|
+
let doc: XmlDocument;
|
|
32
|
+
try {
|
|
33
|
+
doc = parseXml(xml);
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const evento = firstChild(doc.root, 'evento', NFE_NS);
|
|
38
|
+
const pedido = evento === undefined ? undefined : firstChild(evento, 'infEvento', NFE_NS);
|
|
39
|
+
const retEl = firstChild(doc.root, 'retEvento', NFE_NS);
|
|
40
|
+
const ret = retEl === undefined ? undefined : firstChild(retEl, 'infEvento', NFE_NS);
|
|
41
|
+
if (doc.root.local !== 'procEventoNFe' || pedido === undefined || retEl === undefined || ret === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const cStat = campo(ret, 'cStat');
|
|
45
|
+
const chNFe = campo(ret, 'chNFe');
|
|
46
|
+
const tpEvento = campo(ret, 'tpEvento');
|
|
47
|
+
const nSeq = campo(ret, 'nSeqEvento');
|
|
48
|
+
const dhRegEvento = campo(ret, 'dhRegEvento');
|
|
49
|
+
if (cStat === undefined || !cstatEm(cStat, 'eventoRegistrado')) return undefined;
|
|
50
|
+
// O pedido e o retorno têm de falar do mesmo evento.
|
|
51
|
+
// Chave, tipo e sequência têm de vir no retorno e ser iguais aos do pedido: um retorno registrado de outro evento, ou
|
|
52
|
+
// sem dizer de qual, não prova nada.
|
|
53
|
+
if (chNFe === undefined || tpEvento === undefined || nSeq === undefined) return undefined;
|
|
54
|
+
const nSeqPedido = campo(pedido, 'nSeqEvento');
|
|
55
|
+
if (campo(pedido, 'chNFe') !== chNFe || campo(pedido, 'tpEvento') !== tpEvento) return undefined;
|
|
56
|
+
if (nSeqPedido === undefined || Number(nSeqPedido) !== Number(nSeq)) return undefined;
|
|
57
|
+
if (chNFe === undefined || tpEvento === undefined || nSeq === undefined || dhRegEvento === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
const nProt = campo(ret, 'nProt');
|
|
60
|
+
return {
|
|
61
|
+
chNFe,
|
|
62
|
+
tpEvento,
|
|
63
|
+
nSeqEvento: String(Number(nSeq)),
|
|
64
|
+
dhRegEvento,
|
|
65
|
+
retEvento: sliceElement(doc, retEl, ''),
|
|
66
|
+
procEventoNFe: xml,
|
|
67
|
+
...(nProt === undefined ? {} : { nProt }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Consulta a chave e devolve o evento `tpEvento` que a SEFAZ registrou para ela (o de maior `nSeqEvento`, quando há
|
|
73
|
+
* vários, como na CC-e). Serve depois de um pedido de evento sem resposta ou respondido com 573 ou 580: nunca conclua
|
|
74
|
+
* que o evento existe só pelo `cStat` do pedido. `registrado: false` quer dizer que a consulta não mostrou o evento
|
|
75
|
+
* (ou não decidiu: veja `consulta`); não quer dizer que o evento não existe.
|
|
76
|
+
*/
|
|
77
|
+
export async function recuperarEventoRegistrado(
|
|
78
|
+
client: NfeClient,
|
|
79
|
+
chave: string,
|
|
80
|
+
tpEvento: string,
|
|
81
|
+
): Promise<RecuperacaoEvento> {
|
|
82
|
+
const consulta = await client.consultar(chave);
|
|
83
|
+
if (consulta.status !== 'authorized' && consulta.status !== 'denied') return { registrado: false, consulta };
|
|
84
|
+
let achado: EventoRegistrado | undefined;
|
|
85
|
+
for (const xml of consulta.value.eventos) {
|
|
86
|
+
const e = lerProcEvento(xml);
|
|
87
|
+
if (e === undefined || e.chNFe !== consulta.value.chNFe || e.tpEvento !== tpEvento) continue;
|
|
88
|
+
if (achado === undefined || Number(e.nSeqEvento) > Number(achado.nSeqEvento)) achado = e;
|
|
89
|
+
}
|
|
90
|
+
return achado === undefined ? { registrado: false, consulta } : { registrado: true, evento: achado, consulta };
|
|
91
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Envio sem resposta e duplicidade na autorização.
|
|
3
|
+
*
|
|
4
|
+
* Idempotência: a chave de acesso contém o `cNF` e o número da nota, e o `digVal` do protocolo é o DigestValue do
|
|
5
|
+
* conteúdo autorizado. Enquanto o mesmo XML assinado (os mesmos bytes) for reenviado, a SEFAZ ou autoriza, ou devolve
|
|
6
|
+
* duplicidade (204) com o protocolo existente. Gerar outro `cNF`, outro `dhEmi` ou reassinar depois de um envio sem
|
|
7
|
+
* resposta cria uma segunda nota para o mesmo número: a primeira pode ter sido autorizada, e a segunda cai em 539
|
|
8
|
+
* (duplicidade com diferença na chave) ou, pior, é autorizada com outra chave se o número mudar. Por isso:
|
|
9
|
+
*
|
|
10
|
+
* 1. grave o XML assinado antes de enviar;
|
|
11
|
+
* 2. envio sem resposta (timeout, conexão caída) ou 204: consulte a chave (`resolverEnvioSemResposta`);
|
|
12
|
+
* 3. `reenviar` (cStat 217, a NF-e não consta): reenvie exatamente os bytes gravados;
|
|
13
|
+
* 4. `concluida`: o `nfeProc` vem montado com o XML gravado, sem novo envio.
|
|
14
|
+
*
|
|
15
|
+
* O `digVal` é opcional no protocolo (`TProtNFe/infProt/digVal`, minOccurs 0 no `leiauteNFe_v4.00.xsd` do PL_010), e
|
|
16
|
+
* há autorizador que o omite. Sem ele, nada prova que o conteúdo registrado é o destes bytes, e as duas decisões se
|
|
17
|
+
* separam:
|
|
18
|
+
*
|
|
19
|
+
* - denegação (110, 301, 302, 303; MOC 7.0 Anexo I, tabela 4.4.3 e regras 1C17-40, 5E17-40 e 5E17-60) é decisão
|
|
20
|
+
* sobre a chave: a NF-e fica registrada como denegada e o número não pode ser reaproveitado nem inutilizado, qualquer
|
|
21
|
+
* que seja o conteúdo. A resolução é `concluida` com o protocolo, e `conteudo` diz se o `digVal` confere, falta ou
|
|
22
|
+
* difere; o `nfeProc` só vem quando confere;
|
|
23
|
+
* - autorização sem `digVal` não prova que a NF-e autorizada é esta: `sem-prova`, e quem chama decide com um humano.
|
|
24
|
+
*
|
|
25
|
+
* Fontes: MOC 7.0 Anexo I (RV 204 "Duplicidade de NF-e", RV 539 "Duplicidade de NF-e com diferença na Chave de
|
|
26
|
+
* Acesso [chNFe: ...]") e serviço de consulta protocolo (cStat 217; 561, 562 e 613 quando a numeração tem outra NF-e).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { Rejected } from '@sinete/core';
|
|
30
|
+
import { authorized, denied } from '@sinete/core';
|
|
31
|
+
import type { AutorizacaoOutcome, ConsultaOutcome, NfeClient, ProtocoloNfe } from './client.ts';
|
|
32
|
+
import { cstatEm } from './outcome.ts';
|
|
33
|
+
import { documentoAssinado } from './proc.ts';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* O que o `digVal` do protocolo diz dos bytes assinados: `confere` (é o DigestValue deles), `sem-digval` (o protocolo
|
|
37
|
+
* não o traz) ou `difere` (a SEFAZ registrou outro conteúdo com a mesma chave).
|
|
38
|
+
*/
|
|
39
|
+
export type ConteudoDoProtocolo = 'confere' | 'sem-digval' | 'difere';
|
|
40
|
+
|
|
41
|
+
/** O que fazer com uma NF-e cujo envio ficou sem resposta ou voltou como duplicidade. */
|
|
42
|
+
export type ResolucaoEnvio =
|
|
43
|
+
/**
|
|
44
|
+
* A chave está decidida: autorizada (ou cancelada) com o mesmo conteúdo, e `outcome` traz o protocolo e o `nfeProc`;
|
|
45
|
+
* ou denegada, com qualquer conteúdo (a denegação é da chave), e `outcome` traz o protocolo, com o `nfeProc` só
|
|
46
|
+
* quando `conteudo` é `confere`.
|
|
47
|
+
*/
|
|
48
|
+
| {
|
|
49
|
+
readonly acao: 'concluida';
|
|
50
|
+
readonly situacao: 'autorizada' | 'cancelada' | 'denegada';
|
|
51
|
+
readonly conteudo: ConteudoDoProtocolo;
|
|
52
|
+
readonly outcome: AutorizacaoOutcome;
|
|
53
|
+
}
|
|
54
|
+
/** A NF-e não consta na SEFAZ (217): reenvie `nfeAssinada`, os mesmos bytes. */
|
|
55
|
+
| { readonly acao: 'reenviar'; readonly nfeAssinada: string }
|
|
56
|
+
/**
|
|
57
|
+
* Existe outra NF-e para o mesmo número: a mesma chave com outro conteúdo (`digVal` diferente) ou outra chave (539 no
|
|
58
|
+
* envio, ou 561, 562 e 613 na consulta; `chNFe` extraída do `xMotivo` quando vier). Não reenvie: recupere a nota registrada e descarte a local.
|
|
59
|
+
*/
|
|
60
|
+
| {
|
|
61
|
+
readonly acao: 'divergente';
|
|
62
|
+
readonly chNFe?: string;
|
|
63
|
+
readonly consulta?: ConsultaOutcome;
|
|
64
|
+
readonly motivo: Rejected | ConsultaOutcome;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A chave está autorizada (ou cancelada), mas o protocolo não traz `digVal`: nada prova que o conteúdo autorizado é o
|
|
68
|
+
* destes bytes. Não reenvie nem descarte a nota local, e não a guarde como autorizada sem conferir o XML registrado
|
|
69
|
+
* na SEFAZ (a Distribuição DF-e ou o portal devolvem a NF-e autorizada).
|
|
70
|
+
*/
|
|
71
|
+
| { readonly acao: 'sem-prova'; readonly situacao: 'autorizada' | 'cancelada'; readonly consulta: ConsultaOutcome }
|
|
72
|
+
/**
|
|
73
|
+
* A consulta não decidiu (serviço paralisado, consumo indevido, rejeição de schema, situação sem protocolo): tente de
|
|
74
|
+
* novo mais tarde, sem descartar a nota local.
|
|
75
|
+
*/
|
|
76
|
+
| { readonly acao: 'indefinida'; readonly outcome: ConsultaOutcome };
|
|
77
|
+
|
|
78
|
+
/** Chave com CNPJ alfanumérico (NT 2025.001): 6 dígitos, 12 alfanuméricos, 26 dígitos. */
|
|
79
|
+
const CHAVE_NO_MOTIVO = /\[\s*chNFe\s*:\s*([0-9]{6}[0-9A-Z]{12}[0-9]{26})\s*\]/i;
|
|
80
|
+
|
|
81
|
+
/** Chave de acesso que a SEFAZ informa no `xMotivo` da rejeição 539 (`[chNFe:...]`), se houver. */
|
|
82
|
+
export function chaveDaDuplicidade(xMotivo: string): string | undefined {
|
|
83
|
+
return CHAVE_NO_MOTIVO.exec(xMotivo)?.[1]?.toUpperCase();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve um envio de autorização sem resposta, ou cuja resposta foi 204 ou 539, consultando a chave da NF-e
|
|
88
|
+
* assinada. `anterior` é o desfecho do envio, quando houve um.
|
|
89
|
+
*/
|
|
90
|
+
export async function resolverEnvioSemResposta(
|
|
91
|
+
client: NfeClient,
|
|
92
|
+
nfeAssinada: string,
|
|
93
|
+
anterior?: AutorizacaoOutcome,
|
|
94
|
+
): Promise<ResolucaoEnvio> {
|
|
95
|
+
const a = documentoAssinado(nfeAssinada, 'NFe', 'infNFe');
|
|
96
|
+
const chave = a.id.slice(3);
|
|
97
|
+
if (anterior && anterior.status === 'rejected' && cstatEm(anterior.cStat, 'duplicidadeChaveDiferente')) {
|
|
98
|
+
const outra = chaveDaDuplicidade(anterior.xMotivo);
|
|
99
|
+
return { acao: 'divergente', motivo: anterior, ...(outra === undefined ? {} : { chNFe: outra }) };
|
|
100
|
+
}
|
|
101
|
+
const consulta = await client.consultar(chave, nfeAssinada);
|
|
102
|
+
if (consulta.status === 'rejected') {
|
|
103
|
+
if (cstatEm(consulta.cStat, 'naoConsta')) return { acao: 'reenviar', nfeAssinada: a.xml };
|
|
104
|
+
// 561, 562 e 613 na consulta: a chave local não consta, mas a numeração dela tem outra NF-e (outro mês, outro cNF
|
|
105
|
+
// ou outra chave). É a mesma situação da 539 no envio; o 562 traz a chave registrada no xMotivo.
|
|
106
|
+
if (cstatEm(consulta.cStat, 'outraNfeNoNumero')) {
|
|
107
|
+
const outra = chaveDaDuplicidade(consulta.xMotivo);
|
|
108
|
+
return { acao: 'divergente', motivo: consulta, ...(outra === undefined ? {} : { chNFe: outra }) };
|
|
109
|
+
}
|
|
110
|
+
return { acao: 'indefinida', outcome: consulta };
|
|
111
|
+
}
|
|
112
|
+
if (consulta.status === 'pending') return { acao: 'indefinida', outcome: consulta };
|
|
113
|
+
const v = consulta.value;
|
|
114
|
+
const p: ProtocoloNfe | undefined = v.protocolo;
|
|
115
|
+
if (!p) return { acao: 'indefinida', outcome: consulta };
|
|
116
|
+
const conteudo: ConteudoDoProtocolo =
|
|
117
|
+
p.digVal === undefined ? 'sem-digval' : v.digValConfere === true ? 'confere' : 'difere';
|
|
118
|
+
const protStatus = { cStat: p.cStat, xMotivo: p.xMotivo };
|
|
119
|
+
// A denegação é da chave: o número está denegado com qualquer conteúdo, e o `conteudo` diz se é o destes bytes.
|
|
120
|
+
if (v.situacao === 'denegada')
|
|
121
|
+
return { acao: 'concluida', situacao: 'denegada', conteudo, outcome: denied(protStatus, p) };
|
|
122
|
+
// Só um digVal presente e diferente prova outro conteúdo para a chave.
|
|
123
|
+
if (conteudo === 'difere') return { acao: 'divergente', chNFe: chave, consulta, motivo: consulta };
|
|
124
|
+
if (conteudo === 'sem-digval') return { acao: 'sem-prova', situacao: v.situacao, consulta };
|
|
125
|
+
if (p.nfeProc === undefined) return { acao: 'indefinida', outcome: consulta };
|
|
126
|
+
return { acao: 'concluida', situacao: v.situacao, conteudo, outcome: authorized(protStatus, p) };
|
|
127
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chamada SOAP 1.2 a um web service da NF-e 4.00: monta o corpo com o namespace do WSDL (dado em `data/servicos.json`),
|
|
3
|
+
* envia pelo `Transport` injetado e devolve o elemento de retorno como fatia da resposta, com a árvore para leitura.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from '@sinete/core';
|
|
7
|
+
import { ProtocolError } from '@sinete/core';
|
|
8
|
+
import type { XmlDocument, XmlElement } from '@sinete/core/xml';
|
|
9
|
+
import { childElements, descendants, parseXml } from '@sinete/core/xml';
|
|
10
|
+
import type { EndpointRef, NfeServico, Transport } from '@sinete/transport';
|
|
11
|
+
import { soap12ContentType, soap12Envelope, soapFault } from '@sinete/transport';
|
|
12
|
+
import servicos from '../data/servicos.json' with { type: 'json' };
|
|
13
|
+
import { NFE_NS } from './proc.ts';
|
|
14
|
+
|
|
15
|
+
interface ServicoInfo {
|
|
16
|
+
readonly namespace: string;
|
|
17
|
+
readonly operacao: string;
|
|
18
|
+
readonly envelope?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Autorizadores que pedem a mensagem dentro do elemento da operação do WSDL (`<operacao><nfeDadosMsg>`), como a
|
|
21
|
+
* Distribuição DF-e faz para todos. Valor `operacao`; a fonte fica em `fonteEnvelope`.
|
|
22
|
+
*/
|
|
23
|
+
readonly envelopeNoAutorizador?: Readonly<Record<string, string>>;
|
|
24
|
+
readonly fonteEnvelope?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SERVICOS: Readonly<Record<string, ServicoInfo>> = servicos.servicos;
|
|
28
|
+
|
|
29
|
+
/** Namespace do WSDL e operação do serviço. */
|
|
30
|
+
export function servicoInfo(servico: NfeServico): ServicoInfo {
|
|
31
|
+
const s = SERVICOS[servico];
|
|
32
|
+
if (!s) throw new ProtocolError(`serviço sem descrição em data/servicos.json: ${servico}`);
|
|
33
|
+
return s;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Corpo SOAP do serviço: a mensagem entra como texto, sem reparse. */
|
|
37
|
+
export function soapBodyFor(servico: NfeServico, mensagem: string, autorizador?: string): string {
|
|
38
|
+
const s = servicoInfo(servico);
|
|
39
|
+
if (s.envelope === 'distDFeInteresse' || (autorizador && s.envelopeNoAutorizador?.[autorizador] === 'operacao')) {
|
|
40
|
+
return `<${s.operacao} xmlns="${s.namespace}"><nfeDadosMsg>${mensagem}</nfeDadosMsg></${s.operacao}>`;
|
|
41
|
+
}
|
|
42
|
+
return `<nfeDadosMsg xmlns="${s.namespace}">${mensagem}</nfeDadosMsg>`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Resposta de um serviço: o documento inteiro (para fatiar) e o elemento de retorno. */
|
|
46
|
+
export interface RespostaSoap {
|
|
47
|
+
readonly doc: XmlDocument;
|
|
48
|
+
readonly ret: XmlElement;
|
|
49
|
+
readonly status: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ChamadaSoap {
|
|
53
|
+
readonly transport: Transport;
|
|
54
|
+
readonly endpoint: EndpointRef;
|
|
55
|
+
readonly servico: NfeServico;
|
|
56
|
+
readonly mensagem: string;
|
|
57
|
+
/** Nome local do elemento de retorno (`retEnviNFe`, `retConsSitNFe`...). */
|
|
58
|
+
readonly retorno: string;
|
|
59
|
+
readonly timeoutMs?: number;
|
|
60
|
+
readonly signal?: AbortSignal;
|
|
61
|
+
readonly logger: Logger;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Envia e devolve o elemento de retorno. Fault SOAP, retorno ausente ou XML malformado viram `ProtocolError`. */
|
|
65
|
+
export async function chamar(c: ChamadaSoap): Promise<RespostaSoap> {
|
|
66
|
+
const s = servicoInfo(c.servico);
|
|
67
|
+
const started = { servico: c.servico, autorizador: c.endpoint.autorizador, host: c.endpoint.host };
|
|
68
|
+
const res = await c.transport.send({
|
|
69
|
+
url: c.endpoint.url,
|
|
70
|
+
endpoint: c.endpoint,
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'content-type': soap12ContentType(`${s.namespace}/${s.operacao}`) },
|
|
73
|
+
body: soap12Envelope(soapBodyFor(c.servico, c.mensagem, c.endpoint.autorizador)),
|
|
74
|
+
...(c.timeoutMs === undefined ? {} : { timeoutMs: c.timeoutMs }),
|
|
75
|
+
...(c.signal === undefined ? {} : { signal: c.signal }),
|
|
76
|
+
});
|
|
77
|
+
const text = res.text();
|
|
78
|
+
const fault = soapFault(text);
|
|
79
|
+
if (fault) {
|
|
80
|
+
c.logger.warn('nfe.soap.fault', { ...started, status: res.status, code: fault.code });
|
|
81
|
+
throw new ProtocolError(`SOAP fault de ${c.endpoint.host}: ${fault.reason ?? fault.code ?? 'sem motivo'}`, {
|
|
82
|
+
details: { status: res.status, code: fault.code, reason: fault.reason, servico: c.servico },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
let doc: XmlDocument;
|
|
86
|
+
try {
|
|
87
|
+
doc = parseXml(text.replace(/^/, ''));
|
|
88
|
+
} catch (cause) {
|
|
89
|
+
throw new ProtocolError(`resposta de ${c.endpoint.host} não é XML (HTTP ${res.status})`, {
|
|
90
|
+
cause,
|
|
91
|
+
details: { status: res.status, servico: c.servico },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
let ret: XmlElement | undefined;
|
|
95
|
+
for (const el of descendants(doc.root)) {
|
|
96
|
+
if (el.local === c.retorno && el.ns === NFE_NS) {
|
|
97
|
+
ret = el;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!ret) ret = retornoForaDoNamespace(doc.root, c.retorno);
|
|
102
|
+
if (ret && ret.ns !== NFE_NS) {
|
|
103
|
+
// A SEFAZ-MG devolve o <retConsCad> sem o namespace da NF-e: ele herda o default do WSDL do nfeResultMsg, e só
|
|
104
|
+
// os filhos declaram o da NF-e (visto em homologação e em produção em 28/09/2026). O conteúdo é o do leiaute.
|
|
105
|
+
c.logger.warn('nfe.soap.retorno_fora_do_namespace', { ...started, retorno: c.retorno, ns: ret.ns });
|
|
106
|
+
}
|
|
107
|
+
if (!ret) {
|
|
108
|
+
throw new ProtocolError(`resposta de ${c.endpoint.host} sem <${c.retorno}> (HTTP ${res.status})`, {
|
|
109
|
+
details: { status: res.status, servico: c.servico },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (!Array.from(descendants(ret)).some((el) => el.local === 'cStat' && el.ns === NFE_NS)) {
|
|
113
|
+
throw new ProtocolError(`<${c.retorno}> de ${c.endpoint.host} sem cStat (HTTP ${res.status})`, {
|
|
114
|
+
details: { status: res.status, servico: c.servico },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
c.logger.debug('nfe.soap.resposta', { ...started, status: res.status });
|
|
118
|
+
return { doc, ret, status: res.status };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* O elemento de retorno com o nome certo em outro namespace, aceito só quando os filhos dele estão no namespace da
|
|
123
|
+
* NF-e: é o caso do autorizador que esquece o `xmlns` no elemento de retorno e o declara nos filhos. Com os filhos
|
|
124
|
+
* fora do namespace, a resposta continua recusada.
|
|
125
|
+
*/
|
|
126
|
+
function retornoForaDoNamespace(root: XmlElement, retorno: string): XmlElement | undefined {
|
|
127
|
+
for (const el of descendants(root)) {
|
|
128
|
+
if (el.local !== retorno) continue;
|
|
129
|
+
const filhos = childElements(el);
|
|
130
|
+
if (filhos.length > 0 && filhos.every((f) => f.ns === NFE_NS)) return el;
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
package/src/time.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fuso do emitente e formatação dos instantes do leiaute (`TDateTimeUTC`, `AAAA-MM-DDThh:mm:ss±hh:mm`). O fuso vem da
|
|
3
|
+
* tabela `data/fusos.json` pela UF, ou do chamador; nunca do fuso da máquina.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Clock, Uf } from '@sinete/core';
|
|
7
|
+
import { formatDateTimeOffset } from '@sinete/core';
|
|
8
|
+
import fusos from './data/fusos.json' with { type: 'json' };
|
|
9
|
+
|
|
10
|
+
/** Instante no tempo, como os relógios do `@sinete/core` o devolvem (o tipo `Date`, sem tocar no global). */
|
|
11
|
+
export type Instante = ReturnType<Clock['now']>;
|
|
12
|
+
|
|
13
|
+
const UF_OFFSETS: Readonly<Record<string, number>> = fusos.ufs;
|
|
14
|
+
|
|
15
|
+
/** Deslocamento do horário legal da UF em minutos (`-180` para Brasília). */
|
|
16
|
+
export function offsetDaUf(uf: Uf): number {
|
|
17
|
+
return Object.hasOwn(UF_OFFSETS, uf) ? (UF_OFFSETS[uf] as number) : fusos.padrao;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `TDateTimeUTC` do instante no deslocamento dado. */
|
|
21
|
+
export function formatDh(date: Instante, offsetMinutes: number): string {
|
|
22
|
+
return formatDateTimeOffset(date, offsetMinutes);
|
|
23
|
+
}
|