@sinete/nfe 0.1.0 → 0.3.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/README.md +55 -50
- package/dist/build/build.d.ts +23 -23
- package/dist/build/build.d.ts.map +1 -1
- package/dist/build/destinatario.d.ts +2 -2
- package/dist/build/ibscbs.d.ts.map +1 -1
- package/dist/build/nfce.d.ts +5 -5
- package/dist/build/nfce.d.ts.map +1 -1
- package/dist/build/pl.d.ts +4 -4
- package/dist/build/pl.d.ts.map +1 -1
- package/dist/build/rejeicoes.d.ts +7 -7
- package/dist/build/rejeicoes.d.ts.map +1 -1
- package/dist/build/textos.d.ts +26 -0
- package/dist/build/textos.d.ts.map +1 -0
- package/dist/build/values.d.ts +8 -8
- package/dist/build/values.d.ts.map +1 -1
- package/dist/decimal.d.ts +2 -2
- package/dist/decimal.d.ts.map +1 -1
- package/dist/format.d.ts +19 -19
- package/dist/format.d.ts.map +1 -1
- package/dist/ibs-cbs.d.ts +10 -5
- package/dist/ibs-cbs.d.ts.map +1 -1
- package/dist/ibs-cbs.js +35 -35
- package/dist/ibs-cbs.js.map +3 -3
- package/dist/index.d.ts +18 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +796 -588
- package/dist/index.js.map +27 -26
- package/dist/issues.d.ts +10 -10
- package/dist/issues.d.ts.map +1 -1
- package/dist/model.d.ts +4 -4
- package/dist/ports.d.ts +15 -15
- package/dist/ports.d.ts.map +1 -1
- package/dist/rotulo.d.ts +2 -2
- package/dist/rotulo.d.ts.map +1 -1
- package/dist/rtc.d.ts +23 -23
- package/dist/rtc.d.ts.map +1 -1
- package/dist/services/client.d.ts +44 -40
- package/dist/services/client.d.ts.map +1 -1
- package/dist/services/gzip.d.ts +1 -1
- package/dist/services/gzip.d.ts.map +1 -1
- package/dist/services/index.d.ts +4 -4
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/outcome.d.ts +3 -3
- package/dist/services/outcome.d.ts.map +1 -1
- package/dist/services/proc.d.ts +6 -6
- package/dist/services/proc.d.ts.map +1 -1
- package/dist/services/recuperar.d.ts +8 -7
- package/dist/services/recuperar.d.ts.map +1 -1
- package/dist/services/resolver.d.ts +12 -11
- package/dist/services/resolver.d.ts.map +1 -1
- package/dist/services/soap.d.ts +8 -8
- package/dist/services/soap.d.ts.map +1 -1
- package/dist/time.d.ts +4 -4
- package/dist/time.d.ts.map +1 -1
- package/dist/versao-gerada.d.ts +1 -1
- package/package.json +9 -9
- package/src/build/build.ts +213 -203
- package/src/build/destinatario.ts +8 -8
- package/src/build/ibscbs.ts +10 -6
- package/src/build/icms.ts +6 -6
- package/src/build/nfce.ts +19 -19
- package/src/build/pl.ts +12 -12
- package/src/build/rejeicoes.ts +13 -13
- package/src/build/textos.ts +228 -0
- package/src/build/values.ts +14 -14
- package/src/data/arredondamento.json +1 -1
- package/src/data/cstat.json +6 -6
- package/src/data/fusos.json +1 -1
- package/src/data/nfce-urls.json +2 -2
- package/src/data/produtor-rural.json +1 -1
- package/src/data/resp-tec.json +1 -1
- package/src/decimal.ts +10 -9
- package/src/format.ts +29 -28
- package/src/ibs-cbs.ts +79 -74
- package/src/index.ts +29 -27
- package/src/issues.ts +11 -11
- package/src/model.ts +4 -4
- package/src/ports.ts +15 -15
- package/src/rotulo.ts +4 -4
- package/src/rtc.ts +91 -91
- package/src/services/client.ts +248 -217
- package/src/services/gzip.ts +7 -7
- package/src/services/index.ts +12 -10
- package/src/services/outcome.ts +9 -9
- package/src/services/proc.ts +50 -49
- package/src/services/recuperar.ts +29 -25
- package/src/services/resolver.ts +25 -23
- package/src/services/soap.ts +35 -35
- package/src/time.ts +6 -6
- package/src/versao-gerada.ts +1 -1
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* critério da UF (E16a-30 e E16a-35, E14-10 e E14-20, facultativas). As regras só da NFC-e estão em `nfce.ts`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { ufPorSigla } from '@sinete/core';
|
|
9
|
+
import { lerCnpj } from '@sinete/validators';
|
|
10
10
|
import suframa from '../data/suframa.json' with { type: 'json' };
|
|
11
11
|
import type { Issues } from '../issues.ts';
|
|
12
|
-
import type {
|
|
12
|
+
import type { DadosNfe } from '../model.ts';
|
|
13
13
|
|
|
14
14
|
/** Identificação já resolvida pelo montador (com os padrões aplicados). */
|
|
15
15
|
export interface IdeDestinatario {
|
|
@@ -26,7 +26,7 @@ const ID_ESTRANGEIRO = /^[0-9A-Za-z:.+\-/()]*$/;
|
|
|
26
26
|
|
|
27
27
|
const digitos = (s: string): string => s.replace(/\D/g, '');
|
|
28
28
|
|
|
29
|
-
export function conferirDestinatario(input:
|
|
29
|
+
export function conferirDestinatario(input: DadosNfe, ide: IdeDestinatario, issues: Issues): void {
|
|
30
30
|
const d = input.destinatario;
|
|
31
31
|
if (d === undefined) return;
|
|
32
32
|
const nfe = ide.mod === '55';
|
|
@@ -85,7 +85,7 @@ export function conferirDestinatario(input: NfeInput, ide: IdeDestinatario, issu
|
|
|
85
85
|
// E10-20: o município do destinatário é da UF dele (as duas primeiras posições do código são o código da UF).
|
|
86
86
|
if (end !== undefined && !exterior) {
|
|
87
87
|
const e = end as { UF: string; cMun: string };
|
|
88
|
-
const cUF =
|
|
88
|
+
const cUF = ufPorSigla(e.UF)?.cUF;
|
|
89
89
|
if (cUF !== undefined && digitos(e.cMun).slice(0, 2) !== cUF) {
|
|
90
90
|
issues.add(
|
|
91
91
|
'destinatario.endereco.cMun',
|
|
@@ -108,11 +108,11 @@ export function conferirDestinatario(input: NfeInput, ide: IdeDestinatario, issu
|
|
|
108
108
|
// Na saída a entrega vale contra o emitente e a retirada contra o destinatário; na entrada, ao contrário.
|
|
109
109
|
const [ufEntrega, ufRetirada] = saida ? [emitUf, destUf] : [destUf, emitUf];
|
|
110
110
|
if (ide.idDest === '2' && destUf === emitUf) {
|
|
111
|
-
// CNPJ alfanumérico: as letras contam; só a máscara sai (a mesma normalização do
|
|
111
|
+
// CNPJ alfanumérico: as letras contam; só a máscara sai (a mesma normalização do lerCnpj).
|
|
112
112
|
const cnpj = (v: string | undefined): string | undefined => {
|
|
113
113
|
if (v === undefined) return undefined;
|
|
114
|
-
const r =
|
|
115
|
-
return r.ok ? r.
|
|
114
|
+
const r = lerCnpj(v);
|
|
115
|
+
return r.ok ? r.valor : undefined;
|
|
116
116
|
};
|
|
117
117
|
const cnpjDest = 'CNPJ' in d ? cnpj(d.CNPJ) : undefined;
|
|
118
118
|
const mesmoCnpj = cnpjDest !== undefined && cnpjDest === cnpj(input.emitente.CNPJ);
|
package/src/build/ibscbs.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* `IBSCBSTot` (W34 a W59g) somando exatamente os valores dos itens.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { lerXml, primeiroFilho } from '@sinete/core/xml';
|
|
7
7
|
import type { ComplexType } from '@sinete/schemas';
|
|
8
|
-
import {
|
|
8
|
+
import { serializar, validar } from '@sinete/schemas';
|
|
9
9
|
import type { TIBSCBSMonoTot, TTribNFe } from '@sinete/schemas/nfe/PL_010f';
|
|
10
10
|
import { Decimal } from '../decimal.ts';
|
|
11
11
|
import type { Issues } from '../issues.ts';
|
|
@@ -19,12 +19,16 @@ const NFE_NS = 'http://www.portalfiscal.inf.br/nfe';
|
|
|
19
19
|
export function grupoInvalido(ct: ComplexType, nome: string, value: unknown, path: string, issues: Issues): boolean {
|
|
20
20
|
if (value === undefined) return false;
|
|
21
21
|
try {
|
|
22
|
-
const doc =
|
|
23
|
-
const el =
|
|
22
|
+
const doc = lerXml(`<w xmlns="${NFE_NS}">${serializar(ct, nome, value as never, NFE_NS)}</w>`);
|
|
23
|
+
const el = primeiroFilho(doc.raiz, nome, NFE_NS);
|
|
24
24
|
const erros =
|
|
25
|
-
el === undefined ? [{
|
|
25
|
+
el === undefined ? [{ caminho: '', code: 'modelo_de_conteudo', mensagem: 'grupo vazio' }] : validar(ct, el);
|
|
26
26
|
for (const e of erros)
|
|
27
|
-
issues.add(
|
|
27
|
+
issues.add(
|
|
28
|
+
`${path}${e.caminho.replace(/^\/[^/]+/, '').replace(/\//g, '.')}`,
|
|
29
|
+
'schema',
|
|
30
|
+
`${e.code}: ${e.mensagem}`,
|
|
31
|
+
);
|
|
28
32
|
return erros.length > 0;
|
|
29
33
|
} catch (e) {
|
|
30
34
|
issues.add(path, 'schema', `grupo malformado: ${(e as Error).message}`);
|
package/src/build/icms.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { TNFe_infNFe_det_imposto_ICMS } from '@sinete/schemas/nfe/PL_010f';
|
|
7
7
|
import type { DecimalInput } from '../decimal.ts';
|
|
8
8
|
import { Decimal } from '../decimal.ts';
|
|
9
|
-
import type {
|
|
9
|
+
import type { FormatoDecimal } from '../format.ts';
|
|
10
10
|
import { D0302A04, D0302A04_OPC, D1104V, D1302 } from '../format.ts';
|
|
11
11
|
import type {
|
|
12
12
|
Desoneracao,
|
|
@@ -74,9 +74,9 @@ export interface IcmsItemBase {
|
|
|
74
74
|
|
|
75
75
|
type Out = Record<string, string | undefined>;
|
|
76
76
|
|
|
77
|
-
const M:
|
|
78
|
-
const P:
|
|
79
|
-
const PO:
|
|
77
|
+
const M: FormatoDecimal = D1302;
|
|
78
|
+
const P: FormatoDecimal = D0302A04;
|
|
79
|
+
const PO: FormatoDecimal = D0302A04_OPC;
|
|
80
80
|
|
|
81
81
|
function nonNeg(d: Decimal): Decimal {
|
|
82
82
|
return d.isNegative() ? Decimal.ZERO : d;
|
|
@@ -221,7 +221,7 @@ export function buildIcms(
|
|
|
221
221
|
|
|
222
222
|
/** Retido anteriormente (N26 a N26b, N27a a N27c, N34 a N37): informativo, só o FCP retido entra no total. */
|
|
223
223
|
const retido = (i: IcmsStRetido): Out => {
|
|
224
|
-
const o = (k: keyof IcmsStRetido, f:
|
|
224
|
+
const o = (k: keyof IcmsStRetido, f: FormatoDecimal): string | undefined =>
|
|
225
225
|
ctx.so(ctx.opt(i[k], `${path}.${k}`, f), f);
|
|
226
226
|
const vFCPSTRet = ctx.opt(i.vFCPSTRet, `${path}.vFCPSTRet`, M);
|
|
227
227
|
if (vFCPSTRet !== undefined) t.vFCPSTRet = t.vFCPSTRet.plus(vFCPSTRet);
|
|
@@ -259,7 +259,7 @@ export function buildIcms(
|
|
|
259
259
|
vFCPDif?: DecimalInput;
|
|
260
260
|
vFCPEfet?: DecimalInput;
|
|
261
261
|
},
|
|
262
|
-
redFormat:
|
|
262
|
+
redFormat: FormatoDecimal,
|
|
263
263
|
triade: boolean,
|
|
264
264
|
): Out => {
|
|
265
265
|
const pRedBC = ctx.opt(i.pRedBC, `${path}.pRedBC`, redFormat);
|
package/src/build/nfce.ts
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
* UF decide, e recusar localmente bloquearia a venda onde a regra não vale.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { Ambiente,
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
10
|
+
import type { Ambiente, Assinador, Uf } from '@sinete/core';
|
|
11
|
+
import { tpAmbDoAmbiente } from '@sinete/core';
|
|
12
|
+
import { c14n, codificarBase64, lerXml, PREFIXO_DIGEST_INFO_SHA1, primeiroFilho } from '@sinete/core/xml';
|
|
13
|
+
import { lerCnpj } from '@sinete/validators';
|
|
14
14
|
import urls from '../data/nfce-urls.json' with { type: 'json' };
|
|
15
15
|
import { Decimal } from '../decimal.ts';
|
|
16
16
|
import type { Issues } from '../issues.ts';
|
|
17
|
-
import type {
|
|
17
|
+
import type { DadosNfe } from '../model.ts';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Versão do QR Code da NFC-e. A 3 (padrão) dispensa o CSC: na emissão normal leva só chave, versão e ambiente, e na
|
|
@@ -60,7 +60,7 @@ function vigente(lista: readonly Entrada[] | undefined, dia: string): Entrada |
|
|
|
60
60
|
/**
|
|
61
61
|
* Endereços da NFC-e da UF no ambiente e no dia (`data/nfce-urls.json`, das tabelas do Portal Nacional da NFC-e). O
|
|
62
62
|
* `qrCode` é `undefined` onde a tabela não traz o endereço completo (AM e MA publicam sem o protocolo): informe
|
|
63
|
-
* `
|
|
63
|
+
* `MontarNfeOpcoes.urlQrCode`.
|
|
64
64
|
*/
|
|
65
65
|
export function urlsNfce(
|
|
66
66
|
uf: Uf,
|
|
@@ -87,10 +87,10 @@ const hex = (b: Uint8Array): string => Array.from(b, (x) => x.toString(16).padSt
|
|
|
87
87
|
* `797a4759...`).
|
|
88
88
|
*/
|
|
89
89
|
async function digValHex(xml: string): Promise<string> {
|
|
90
|
-
const doc =
|
|
91
|
-
const inf =
|
|
90
|
+
const doc = lerXml(xml);
|
|
91
|
+
const inf = primeiroFilho(doc.raiz, 'infNFe', NFE_NS);
|
|
92
92
|
if (inf === undefined) throw new Error('NF-e montada sem infNFe');
|
|
93
|
-
return hexDoDigestValue(
|
|
93
|
+
return hexDoDigestValue(codificarBase64(await sha1(te.encode(c14n(inf)))));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** O DigestValue em Base64 convertido para hexadecimal, caractere a caractere (Manual 6.0, 4.3.5, passo 1). */
|
|
@@ -128,7 +128,7 @@ export async function parametrosQrCode(
|
|
|
128
128
|
d: DadosQrCode,
|
|
129
129
|
qr: QrCodeNfceOpcoes,
|
|
130
130
|
): Promise<{ readonly parametros: string; readonly assinar: boolean }> {
|
|
131
|
-
const tpAmb =
|
|
131
|
+
const tpAmb = tpAmbDoAmbiente(d.ambiente);
|
|
132
132
|
const offline = d.tpEmis === '9';
|
|
133
133
|
const dia = d.dhEmi.slice(8, 10);
|
|
134
134
|
if (qr.versao === '3') {
|
|
@@ -155,14 +155,14 @@ export async function parametrosQrCode(
|
|
|
155
155
|
* Assinatura dos parâmetros 1 a 7 do QR Code versão 3 off-line: RSA com SHA-1 (PKCS#1 v1.5), em Base64, com o mesmo
|
|
156
156
|
* certificado que assina a NFC-e (Manual 6.0, 4.4.2, parâmetro 8).
|
|
157
157
|
*/
|
|
158
|
-
export async function assinarParametros(parametros: string, signer:
|
|
158
|
+
export async function assinarParametros(parametros: string, signer: Assinador): Promise<string> {
|
|
159
159
|
const bytes = te.encode(parametros);
|
|
160
|
-
if (signer.
|
|
160
|
+
if (signer.tipo === 'dados') return codificarBase64(await signer.assinar(bytes, 'SHA-1'));
|
|
161
161
|
const h = await sha1(bytes);
|
|
162
|
-
const di = new Uint8Array(
|
|
163
|
-
di.set(
|
|
164
|
-
di.set(h,
|
|
165
|
-
return
|
|
162
|
+
const di = new Uint8Array(PREFIXO_DIGEST_INFO_SHA1.length + h.length);
|
|
163
|
+
di.set(PREFIXO_DIGEST_INFO_SHA1);
|
|
164
|
+
di.set(h, PREFIXO_DIGEST_INFO_SHA1.length);
|
|
165
|
+
return codificarBase64(await signer.assinarDigestInfo(di));
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/** CFOP da prestação de serviço tributada pelo ISSQN na NFC-e (MOC 7.0 Anexo I, RV I08-150 a I08-170). */
|
|
@@ -192,7 +192,7 @@ export interface IdeNfce {
|
|
|
192
192
|
* Regras da NFC-e sobre a entrada (origem `entrada`, ADR 0011), todas de aplicação obrigatória. Grupos que a NFC-e
|
|
193
193
|
* não tem saem como `grupo_vedado` no caminho da entrada.
|
|
194
194
|
*/
|
|
195
|
-
export function conferirNfce(input:
|
|
195
|
+
export function conferirNfce(input: DadosNfe, ide: IdeNfce, issues: Issues): void {
|
|
196
196
|
const vedado = (path: string, msg: string): void => issues.add(path, 'grupo_vedado', `NFC-e ${msg}`);
|
|
197
197
|
const invalido = (path: string, msg: string): void => issues.add(path, 'campo_invalido', `NFC-e ${msg}`);
|
|
198
198
|
|
|
@@ -416,8 +416,8 @@ export function pagamentoNfce(pag: PagMontado, vNF: Decimal, issues: Issues): vo
|
|
|
416
416
|
);
|
|
417
417
|
}
|
|
418
418
|
if (c.CNPJ !== undefined) {
|
|
419
|
-
const r =
|
|
420
|
-
if (!r.ok) issues.list.push(r.
|
|
419
|
+
const r = lerCnpj(c.CNPJ, { caminho: `${path}.CNPJ` });
|
|
420
|
+
if (!r.ok) issues.list.push(r.erro);
|
|
421
421
|
}
|
|
422
422
|
});
|
|
423
423
|
}
|
package/src/build/pl.ts
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* `camposForaDoPl` aponta cada campo que sumiria em silêncio.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { Ambiente,
|
|
10
|
-
import {
|
|
11
|
-
import type { ComplexType,
|
|
12
|
-
import {
|
|
9
|
+
import type { Ambiente, Relogio } from '@sinete/core';
|
|
10
|
+
import { ErroNaoSuportado } from '@sinete/core';
|
|
11
|
+
import type { ComplexType, EntradaDeVigencia, Particle } from '@sinete/schemas';
|
|
12
|
+
import { ehComplexType, ehElementParticle, ehWildcard, selecionarPl } from '@sinete/schemas';
|
|
13
13
|
import { TNFe_infNFe as Inf010e } from '@sinete/schemas/nfe/PL_010e';
|
|
14
14
|
import { TNFe_infNFe as Inf010f } from '@sinete/schemas/nfe/PL_010f';
|
|
15
15
|
import type { Issues } from '../issues.ts';
|
|
@@ -21,32 +21,32 @@ const INF_NFE: Readonly<Record<string, ComplexType>> = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export interface PlEscolhido {
|
|
24
|
-
readonly vigencia:
|
|
24
|
+
readonly vigencia: EntradaDeVigencia;
|
|
25
25
|
readonly infNFe: ComplexType;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** PL da NF-e vigente para o ambiente no relógio de emissão (ADR 0002, decisão 6). */
|
|
29
|
-
export function escolherPl(ambiente: Ambiente, emissao:
|
|
29
|
+
export function escolherPl(ambiente: Ambiente, emissao: Relogio): PlEscolhido {
|
|
30
30
|
const vigencia = selecionarPl('nfe', ambiente, emissao);
|
|
31
31
|
const infNFe = INF_NFE[vigencia.modulo];
|
|
32
32
|
if (infNFe === undefined) {
|
|
33
33
|
// A tabela de vigências ganhou um PL que este pacote ainda não conhece: falha explícita, nunca tentativa.
|
|
34
|
-
throw new
|
|
34
|
+
throw new ErroNaoSuportado(`@sinete/nfe não conhece o módulo ${vigencia.modulo}; atualize o pacote`);
|
|
35
35
|
}
|
|
36
36
|
return { vigencia, infNFe };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function memberNames(p: Particle, acc: Set<string>): Set<string> {
|
|
40
|
-
if (
|
|
41
|
-
else if (
|
|
40
|
+
if (ehWildcard(p)) acc.add('$any');
|
|
41
|
+
else if (ehElementParticle(p)) acc.add(p.e);
|
|
42
42
|
else for (const i of p.i) memberNames(i, acc);
|
|
43
43
|
return acc;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function elementTypes(p: Particle, acc: Map<string, ComplexType>): Map<string, ComplexType> {
|
|
47
|
-
if (
|
|
48
|
-
if (
|
|
49
|
-
if (
|
|
47
|
+
if (ehWildcard(p)) return acc;
|
|
48
|
+
if (ehElementParticle(p)) {
|
|
49
|
+
if (ehComplexType(p.t)) acc.set(p.e, p.t);
|
|
50
50
|
} else for (const i of p.i) elementTypes(i, acc);
|
|
51
51
|
return acc;
|
|
52
52
|
}
|
package/src/build/rejeicoes.ts
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* dúvida, a nota vai e a SEFAZ decide, porque recusar localmente uma nota que a SEFAZ aceitaria é pior que a rejeição.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type { Ocorrencia } from '@sinete/core';
|
|
11
11
|
import type { Issues } from '../issues.ts';
|
|
12
|
-
import type {
|
|
12
|
+
import type { DadosNfe } from '../model.ts';
|
|
13
13
|
import type { Instante } from '../time.ts';
|
|
14
|
-
import {
|
|
14
|
+
import { formatarDh } from '../time.ts';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* CFOP em que o CST 50 (suspensão) é aceito com destinatário contribuinte isento: conserto ou reparo e remessa para
|
|
@@ -40,7 +40,7 @@ const CFOP_CST50_ISENTO: ReadonlySet<string> = new Set([
|
|
|
40
40
|
* recusado aqui, seja qual for o documento do destinatário. A exceção 2 (emissão antes de 01/07/2016) não alcança nota
|
|
41
41
|
* montada agora.
|
|
42
42
|
*/
|
|
43
|
-
export function cstComIsento(input:
|
|
43
|
+
export function cstComIsento(input: DadosNfe, idDest: string, issues: Issues): void {
|
|
44
44
|
const d = input.destinatario;
|
|
45
45
|
if (d?.indIEDest !== '2') return;
|
|
46
46
|
input.itens.forEach((it, n) => {
|
|
@@ -60,7 +60,7 @@ export function cstComIsento(input: NfeInput, idDest: string, issues: Issues): v
|
|
|
60
60
|
|
|
61
61
|
/** Data (`AAAA-MM-DD`) do instante no deslocamento dado, em minutos. */
|
|
62
62
|
const dataNoFuso = (instante: Instante, offsetMinutes: number): string =>
|
|
63
|
-
|
|
63
|
+
formatarDh(instante, offsetMinutes).slice(0, 10);
|
|
64
64
|
|
|
65
65
|
/** Brasília (UTC-3), o fuso das SEFAZ, para a comparação de datas que o MOC faz "desconsiderando a hora". */
|
|
66
66
|
const OFFSET_BRASILIA = -180;
|
|
@@ -78,7 +78,7 @@ const OFFSET_BRASILIA = -180;
|
|
|
78
78
|
* "na data da emissão" só vale quando a data local do `dhEmi`, a de Brasília e a de UTC coincidem.
|
|
79
79
|
*/
|
|
80
80
|
export function vencimentos(
|
|
81
|
-
input:
|
|
81
|
+
input: DadosNfe,
|
|
82
82
|
emissao: { readonly dhEmi: string; readonly instante: Instante },
|
|
83
83
|
issues: Issues,
|
|
84
84
|
): void {
|
|
@@ -107,22 +107,22 @@ const soDocumento = (s: string): string => s.replace(/[^0-9A-Za-z]/g, '').toUppe
|
|
|
107
107
|
* - e-CPF: o CPF do emitente é o do certificado, senão rejeição 227 (RV F03A).
|
|
108
108
|
*
|
|
109
109
|
* Só compara documento com documento do mesmo tipo: e-CNPJ assinando nota de emitente CPF (ou o contrário) fica para a
|
|
110
|
-
* SEFAZ. O certificado da SEFAZ que a F03 dispensa só assina a nota avulsa, que o `
|
|
110
|
+
* SEFAZ. O certificado da SEFAZ que a F03 dispensa só assina a nota avulsa, que o `montarNfe` não monta (procEmi 0).
|
|
111
111
|
* Devolve as ocorrências (vazio quando confere), com o caminho da entrada.
|
|
112
112
|
*/
|
|
113
113
|
export function conferirEmitenteDoCertificado(
|
|
114
|
-
nfe:
|
|
114
|
+
nfe: DadosNfe,
|
|
115
115
|
titular: { readonly cnpj?: string | undefined; readonly cpf?: string | undefined },
|
|
116
|
-
): readonly
|
|
116
|
+
): readonly Ocorrencia[] {
|
|
117
117
|
const e = nfe.emitente;
|
|
118
118
|
if (e.CNPJ !== undefined && titular.cnpj !== undefined) {
|
|
119
119
|
const base = soDocumento(e.CNPJ).slice(0, 8);
|
|
120
120
|
if (base !== soDocumento(titular.cnpj).slice(0, 8)) {
|
|
121
121
|
return [
|
|
122
122
|
{
|
|
123
|
-
|
|
123
|
+
caminho: 'emitente.CNPJ',
|
|
124
124
|
code: 'emitente_difere_do_certificado',
|
|
125
|
-
|
|
125
|
+
mensagem: `CNPJ-base ${base} do emitente difere do CNPJ-base do certificado (F03, rejeição 213)`,
|
|
126
126
|
origem: 'entrada',
|
|
127
127
|
},
|
|
128
128
|
];
|
|
@@ -131,9 +131,9 @@ export function conferirEmitenteDoCertificado(
|
|
|
131
131
|
if (e.CPF !== undefined && titular.cpf !== undefined && soDocumento(e.CPF) !== soDocumento(titular.cpf)) {
|
|
132
132
|
return [
|
|
133
133
|
{
|
|
134
|
-
|
|
134
|
+
caminho: 'emitente.CPF',
|
|
135
135
|
code: 'emitente_difere_do_certificado',
|
|
136
|
-
|
|
136
|
+
mensagem: 'CPF do emitente difere do CPF do certificado (F03A, rejeição 227)',
|
|
137
137
|
origem: 'entrada',
|
|
138
138
|
},
|
|
139
139
|
];
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Texto e tamanho dos campos de texto da NF-e conferidos na entrada (ADR 0011, revisão de 01/10/2026).
|
|
3
|
+
*
|
|
4
|
+
* Antes, o caractere que o XML não representa e o texto fora do tipo do leiaute (tamanho, espaço nas pontas,
|
|
5
|
+
* caractere fora do `TString`) só apareciam na montagem, com o caminho do XML (`infNFe.det[0].prod.xProd` ou
|
|
6
|
+
* `/infNFe/det[1]/prod/xProd`). Conferidos aqui, saem como `campo_invalido`, com `origem: 'entrada'`, o caminho da
|
|
7
|
+
* entrada (`itens[0].produto.xProd`) e uma mensagem para quem preenche o campo (`no máximo 60 caracteres`), sem nome
|
|
8
|
+
* de validador nem de tipo do XSD.
|
|
9
|
+
*
|
|
10
|
+
* O tipo de cada campo vem do schema do PL da montagem (o `TString` com o `maxLength` do elemento), nunca de um número
|
|
11
|
+
* escrito aqui: a tabela abaixo só diz qual elemento do `infNFe` recebe cada campo da entrada.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ComplexType, OcorrenciaSchema, Particle, SimpleType } from '@sinete/schemas';
|
|
15
|
+
import { conferirTipoSimples, ehComplexType, ehElementParticle, ehWildcard } from '@sinete/schemas';
|
|
16
|
+
import type { Issues } from '../issues.ts';
|
|
17
|
+
import type { DadosNfe } from '../model.ts';
|
|
18
|
+
|
|
19
|
+
/** Produção `Char` do XML 1.0: tab, LF, CR, U+0020 a U+D7FF, U+E000 a U+FFFD e U+10000 a U+10FFFF. */
|
|
20
|
+
export function textoXmlValido(texto: string): boolean {
|
|
21
|
+
for (const ch of texto) {
|
|
22
|
+
const c = ch.codePointAt(0) ?? 0;
|
|
23
|
+
const ok =
|
|
24
|
+
c === 0x9 ||
|
|
25
|
+
c === 0xa ||
|
|
26
|
+
c === 0xd ||
|
|
27
|
+
(c >= 0x20 && c <= 0xd7ff) ||
|
|
28
|
+
(c >= 0xe000 && c <= 0xfffd) ||
|
|
29
|
+
(c >= 0x10000 && c <= 0x10ffff);
|
|
30
|
+
if (!ok) return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Campo de texto da entrada (com `[]` onde a entrada é uma lista) e o elemento do `infNFe` que o recebe como veio. Só
|
|
37
|
+
* os campos copiados sem transformação: o que a montagem calcula ou formata é conferido por ela.
|
|
38
|
+
*/
|
|
39
|
+
const CAMPOS: readonly (readonly [entrada: string, xml: string])[] = [
|
|
40
|
+
['natOp', 'ide.natOp'],
|
|
41
|
+
['emitente.xNome', 'emit.xNome'],
|
|
42
|
+
['emitente.xFant', 'emit.xFant'],
|
|
43
|
+
['emitente.endereco.xLgr', 'emit.enderEmit.xLgr'],
|
|
44
|
+
['emitente.endereco.nro', 'emit.enderEmit.nro'],
|
|
45
|
+
['emitente.endereco.xCpl', 'emit.enderEmit.xCpl'],
|
|
46
|
+
['emitente.endereco.xBairro', 'emit.enderEmit.xBairro'],
|
|
47
|
+
['emitente.endereco.xMun', 'emit.enderEmit.xMun'],
|
|
48
|
+
['destinatario.xNome', 'dest.xNome'],
|
|
49
|
+
['destinatario.email', 'dest.email'],
|
|
50
|
+
['destinatario.endereco.xLgr', 'dest.enderDest.xLgr'],
|
|
51
|
+
['destinatario.endereco.nro', 'dest.enderDest.nro'],
|
|
52
|
+
['destinatario.endereco.xCpl', 'dest.enderDest.xCpl'],
|
|
53
|
+
['destinatario.endereco.xBairro', 'dest.enderDest.xBairro'],
|
|
54
|
+
['destinatario.endereco.xMun', 'dest.enderDest.xMun'],
|
|
55
|
+
['destinatario.endereco.xPais', 'dest.enderDest.xPais'],
|
|
56
|
+
...(['retirada', 'entrega'] as const).flatMap((g) =>
|
|
57
|
+
(['xNome', 'xLgr', 'nro', 'xCpl', 'xBairro', 'xMun', 'email'] as const).map(
|
|
58
|
+
(c) => [`${g}.${c}`, `${g}.${c}`] as const,
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
['itens[].produto.cProd', 'det.prod.cProd'],
|
|
62
|
+
['itens[].produto.xProd', 'det.prod.xProd'],
|
|
63
|
+
['itens[].produto.uCom', 'det.prod.uCom'],
|
|
64
|
+
['itens[].produto.uTrib', 'det.prod.uTrib'],
|
|
65
|
+
['itens[].produto.xPed', 'det.prod.xPed'],
|
|
66
|
+
['itens[].infAdProd', 'det.infAdProd'],
|
|
67
|
+
['transporte.transportador.xNome', 'transp.transporta.xNome'],
|
|
68
|
+
['transporte.transportador.xEnder', 'transp.transporta.xEnder'],
|
|
69
|
+
['transporte.transportador.xMun', 'transp.transporta.xMun'],
|
|
70
|
+
['transporte.volumes[].esp', 'transp.vol.esp'],
|
|
71
|
+
['transporte.volumes[].marca', 'transp.vol.marca'],
|
|
72
|
+
['transporte.volumes[].nVol', 'transp.vol.nVol'],
|
|
73
|
+
['cobranca.fatura.nFat', 'cobr.fat.nFat'],
|
|
74
|
+
['cobranca.duplicatas[].nDup', 'cobr.dup.nDup'],
|
|
75
|
+
['pagamento.detPag[].xPag', 'pag.detPag.xPag'],
|
|
76
|
+
['informacoesAdicionais.infAdFisco', 'infAdic.infAdFisco'],
|
|
77
|
+
['informacoesAdicionais.infCpl', 'infAdic.infCpl'],
|
|
78
|
+
['informacoesAdicionais.obsCont[].xTexto', 'infAdic.obsCont.xTexto'],
|
|
79
|
+
['informacoesAdicionais.obsFisco[].xTexto', 'infAdic.obsFisco.xTexto'],
|
|
80
|
+
['compra.xNEmp', 'compra.xNEmp'],
|
|
81
|
+
['compra.xPed', 'compra.xPed'],
|
|
82
|
+
['compra.xCont', 'compra.xCont'],
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/** O tipo do elemento `nome` entre as partículas (sequências e escolhas aninhadas). */
|
|
86
|
+
function elemento(p: Particle | undefined, nome: string): ComplexType | SimpleType | undefined {
|
|
87
|
+
if (p === undefined || ehWildcard(p)) return undefined;
|
|
88
|
+
if (ehElementParticle(p)) return p.e === nome ? p.t : undefined;
|
|
89
|
+
for (const i of p.i) {
|
|
90
|
+
const t = elemento(i, nome);
|
|
91
|
+
if (t !== undefined) return t;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** O tipo simples do elemento em `caminho` (pontos) a partir do `infNFe`; `undefined` se o PL não o tem. */
|
|
97
|
+
function tipoEm(infNFe: ComplexType, caminho: string): SimpleType | undefined {
|
|
98
|
+
let t: ComplexType | SimpleType | undefined = infNFe;
|
|
99
|
+
for (const nome of caminho.split('.')) {
|
|
100
|
+
if (t === undefined || !ehComplexType(t)) return undefined;
|
|
101
|
+
t = elemento(t.c, nome);
|
|
102
|
+
}
|
|
103
|
+
return t === undefined || ehComplexType(t) ? undefined : t;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const tiposPorPl = new WeakMap<ComplexType, ReadonlyMap<string, SimpleType>>();
|
|
107
|
+
|
|
108
|
+
/** Tipo de cada campo da tabela no PL, calculado uma vez por PL. */
|
|
109
|
+
function tiposDoPl(infNFe: ComplexType): ReadonlyMap<string, SimpleType> {
|
|
110
|
+
let m = tiposPorPl.get(infNFe);
|
|
111
|
+
if (m === undefined) {
|
|
112
|
+
const novo = new Map<string, SimpleType>();
|
|
113
|
+
for (const [entrada, xml] of CAMPOS) {
|
|
114
|
+
const t = tipoEm(infNFe, xml);
|
|
115
|
+
if (t !== undefined) novo.set(entrada, t);
|
|
116
|
+
}
|
|
117
|
+
m = novo;
|
|
118
|
+
tiposPorPl.set(infNFe, m);
|
|
119
|
+
}
|
|
120
|
+
return m;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Os valores de `caminho` (com `[]` nas listas) na entrada, com o caminho concreto de cada um. */
|
|
124
|
+
function valoresEm(raiz: unknown, caminho: string): [string, unknown][] {
|
|
125
|
+
let atuais: [string, unknown][] = [['', raiz]];
|
|
126
|
+
for (const parte of caminho.split('.')) {
|
|
127
|
+
const lista = parte.endsWith('[]');
|
|
128
|
+
const chave = lista ? parte.slice(0, -2) : parte;
|
|
129
|
+
const proximos: [string, unknown][] = [];
|
|
130
|
+
for (const [c, v] of atuais) {
|
|
131
|
+
if (typeof v !== 'object' || v === null) continue;
|
|
132
|
+
const filho = (v as Record<string, unknown>)[chave];
|
|
133
|
+
const base = c === '' ? chave : `${c}.${chave}`;
|
|
134
|
+
if (!lista) proximos.push([base, filho]);
|
|
135
|
+
else if (Array.isArray(filho)) for (const [n, f] of filho.entries()) proximos.push([`${base}[${n}]`, f]);
|
|
136
|
+
}
|
|
137
|
+
atuais = proximos;
|
|
138
|
+
}
|
|
139
|
+
return atuais;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Caminhos de todos os textos da entrada com caractere que o XML não representa. */
|
|
143
|
+
function textosForaDoXml(valor: unknown, caminho: string, saida: string[]): void {
|
|
144
|
+
if (typeof valor === 'string') {
|
|
145
|
+
if (!textoXmlValido(valor)) saida.push(caminho);
|
|
146
|
+
} else if (Array.isArray(valor)) {
|
|
147
|
+
for (const [n, v] of valor.entries()) textosForaDoXml(v, `${caminho}[${n}]`, saida);
|
|
148
|
+
} else if (typeof valor === 'object' && valor !== null) {
|
|
149
|
+
for (const [k, v] of Object.entries(valor)) textosForaDoXml(v, caminho === '' ? k : `${caminho}.${k}`, saida);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Mensagem do caractere que não vai ao documento e que a pessoa não consegue ver (controle, invisível). */
|
|
154
|
+
const CARACTERE_INVISIVEL = 'caractere não aceito (símbolo ou caractere de controle)';
|
|
155
|
+
|
|
156
|
+
/** Caractere que a pessoa enxerga: letra, marca, número, pontuação ou símbolo. */
|
|
157
|
+
const VISIVEL = /^[\p{L}\p{M}\p{N}\p{P}\p{S}]$/u;
|
|
158
|
+
|
|
159
|
+
/** O primeiro caractere de `valor` que o tipo não aceita no meio de um texto, ou `undefined` se todos passam. */
|
|
160
|
+
function caractereRecusado(tipo: SimpleType, valor: string): string | undefined {
|
|
161
|
+
for (const ch of new Set(valor)) {
|
|
162
|
+
const saida: OcorrenciaSchema[] = [];
|
|
163
|
+
conferirTipoSimples(tipo, `A${ch}A`, '', saida);
|
|
164
|
+
if (saida.some((o) => o.code === 'padrao')) return ch;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* As regras do tipo que `valor` viola, em texto para quem preenche o campo. O código do validador decide a regra; a
|
|
171
|
+
* mensagem diz o limite (do próprio tipo) e, no `padrao`, se o problema é o espaço nas pontas ou um caractere.
|
|
172
|
+
*/
|
|
173
|
+
function mensagensDoTexto(tipo: SimpleType, valor: string, violadas: readonly OcorrenciaSchema[]): string[] {
|
|
174
|
+
if (violadas.length === 0) return [];
|
|
175
|
+
if (valor.trim() === '') return ['não pode ficar em branco'];
|
|
176
|
+
const tamanho = [...valor].length;
|
|
177
|
+
const mensagens = new Set<string>();
|
|
178
|
+
for (const o of violadas) {
|
|
179
|
+
if (o.code === 'tamanho_maximo' && tipo.mx !== undefined)
|
|
180
|
+
mensagens.add(`no máximo ${tipo.mx} caracteres (tem ${tamanho})`);
|
|
181
|
+
else if (o.code === 'tamanho_minimo' && tipo.mn !== undefined)
|
|
182
|
+
mensagens.add(`no mínimo ${tipo.mn} caracteres (tem ${tamanho})`);
|
|
183
|
+
else if (o.code === 'tamanho' && tipo.l !== undefined)
|
|
184
|
+
mensagens.add(`exatamente ${tipo.l} caracteres (tem ${tamanho})`);
|
|
185
|
+
else if (o.code === 'padrao') {
|
|
186
|
+
const espaco = valor !== valor.trim();
|
|
187
|
+
const ch = caractereRecusado(tipo, valor);
|
|
188
|
+
if (espaco) mensagens.add('sem espaço no começo nem no fim');
|
|
189
|
+
if (ch !== undefined) mensagens.add(VISIVEL.test(ch) ? `caractere não aceito: “${ch}”` : CARACTERE_INVISIVEL);
|
|
190
|
+
if (!espaco && ch === undefined) mensagens.add('formato não aceito');
|
|
191
|
+
} else mensagens.add('valor não aceito neste campo');
|
|
192
|
+
}
|
|
193
|
+
return [...mensagens];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Confere os textos da entrada: em qualquer campo, o caractere que o XML não representa; nos campos da tabela, o tipo
|
|
198
|
+
* do elemento no PL (tamanho, espaço nas pontas, caractere fora do conjunto aceito). Tudo sai como `campo_invalido`,
|
|
199
|
+
* com o caminho da entrada, `origem: 'entrada'` e uma ocorrência por regra violada. O campo que já tem ocorrência (de
|
|
200
|
+
* outra conferência da entrada) não ganha outra. `substituidos` são os campos que a montagem troca por um texto fixo
|
|
201
|
+
* (o nome do destinatário em homologação): o tipo deles não é conferido, porque o texto informado não vai ao XML.
|
|
202
|
+
*/
|
|
203
|
+
export function conferirTextosDaEntrada(
|
|
204
|
+
entrada: DadosNfe,
|
|
205
|
+
infNFe: ComplexType,
|
|
206
|
+
issues: Issues,
|
|
207
|
+
substituidos: ReadonlySet<string> = new Set(),
|
|
208
|
+
): void {
|
|
209
|
+
const jaRecusados = new Set(issues.list.map((i) => i.caminho));
|
|
210
|
+
const foraDoXml: string[] = [];
|
|
211
|
+
textosForaDoXml(entrada, '', foraDoXml);
|
|
212
|
+
for (const c of foraDoXml) {
|
|
213
|
+
if (jaRecusados.has(c) || substituidos.has(c)) continue;
|
|
214
|
+
issues.add(c, 'campo_invalido', CARACTERE_INVISIVEL);
|
|
215
|
+
jaRecusados.add(c);
|
|
216
|
+
}
|
|
217
|
+
const tipos = tiposDoPl(infNFe);
|
|
218
|
+
for (const [campo] of CAMPOS) {
|
|
219
|
+
const tipo = tipos.get(campo);
|
|
220
|
+
if (tipo === undefined) continue;
|
|
221
|
+
for (const [caminho, valor] of valoresEm(entrada, campo)) {
|
|
222
|
+
if (typeof valor !== 'string' || jaRecusados.has(caminho) || substituidos.has(caminho)) continue;
|
|
223
|
+
const saida: OcorrenciaSchema[] = [];
|
|
224
|
+
conferirTipoSimples(tipo, valor, caminho, saida);
|
|
225
|
+
for (const m of mensagensDoTexto(tipo, valor, saida)) issues.add(caminho, 'campo_invalido', m);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|