@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.
Files changed (90) hide show
  1. package/README.md +55 -50
  2. package/dist/build/build.d.ts +23 -23
  3. package/dist/build/build.d.ts.map +1 -1
  4. package/dist/build/destinatario.d.ts +2 -2
  5. package/dist/build/ibscbs.d.ts.map +1 -1
  6. package/dist/build/nfce.d.ts +5 -5
  7. package/dist/build/nfce.d.ts.map +1 -1
  8. package/dist/build/pl.d.ts +4 -4
  9. package/dist/build/pl.d.ts.map +1 -1
  10. package/dist/build/rejeicoes.d.ts +7 -7
  11. package/dist/build/rejeicoes.d.ts.map +1 -1
  12. package/dist/build/textos.d.ts +26 -0
  13. package/dist/build/textos.d.ts.map +1 -0
  14. package/dist/build/values.d.ts +8 -8
  15. package/dist/build/values.d.ts.map +1 -1
  16. package/dist/decimal.d.ts +2 -2
  17. package/dist/decimal.d.ts.map +1 -1
  18. package/dist/format.d.ts +19 -19
  19. package/dist/format.d.ts.map +1 -1
  20. package/dist/ibs-cbs.d.ts +10 -5
  21. package/dist/ibs-cbs.d.ts.map +1 -1
  22. package/dist/ibs-cbs.js +35 -35
  23. package/dist/ibs-cbs.js.map +3 -3
  24. package/dist/index.d.ts +18 -18
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +796 -588
  27. package/dist/index.js.map +27 -26
  28. package/dist/issues.d.ts +10 -10
  29. package/dist/issues.d.ts.map +1 -1
  30. package/dist/model.d.ts +4 -4
  31. package/dist/ports.d.ts +15 -15
  32. package/dist/ports.d.ts.map +1 -1
  33. package/dist/rotulo.d.ts +2 -2
  34. package/dist/rotulo.d.ts.map +1 -1
  35. package/dist/rtc.d.ts +23 -23
  36. package/dist/rtc.d.ts.map +1 -1
  37. package/dist/services/client.d.ts +44 -40
  38. package/dist/services/client.d.ts.map +1 -1
  39. package/dist/services/gzip.d.ts +1 -1
  40. package/dist/services/gzip.d.ts.map +1 -1
  41. package/dist/services/index.d.ts +4 -4
  42. package/dist/services/index.d.ts.map +1 -1
  43. package/dist/services/outcome.d.ts +3 -3
  44. package/dist/services/outcome.d.ts.map +1 -1
  45. package/dist/services/proc.d.ts +6 -6
  46. package/dist/services/proc.d.ts.map +1 -1
  47. package/dist/services/recuperar.d.ts +8 -7
  48. package/dist/services/recuperar.d.ts.map +1 -1
  49. package/dist/services/resolver.d.ts +12 -11
  50. package/dist/services/resolver.d.ts.map +1 -1
  51. package/dist/services/soap.d.ts +8 -8
  52. package/dist/services/soap.d.ts.map +1 -1
  53. package/dist/time.d.ts +4 -4
  54. package/dist/time.d.ts.map +1 -1
  55. package/dist/versao-gerada.d.ts +1 -1
  56. package/package.json +9 -9
  57. package/src/build/build.ts +213 -203
  58. package/src/build/destinatario.ts +8 -8
  59. package/src/build/ibscbs.ts +10 -6
  60. package/src/build/icms.ts +6 -6
  61. package/src/build/nfce.ts +19 -19
  62. package/src/build/pl.ts +12 -12
  63. package/src/build/rejeicoes.ts +13 -13
  64. package/src/build/textos.ts +228 -0
  65. package/src/build/values.ts +14 -14
  66. package/src/data/arredondamento.json +1 -1
  67. package/src/data/cstat.json +6 -6
  68. package/src/data/fusos.json +1 -1
  69. package/src/data/nfce-urls.json +2 -2
  70. package/src/data/produtor-rural.json +1 -1
  71. package/src/data/resp-tec.json +1 -1
  72. package/src/decimal.ts +10 -9
  73. package/src/format.ts +29 -28
  74. package/src/ibs-cbs.ts +79 -74
  75. package/src/index.ts +29 -27
  76. package/src/issues.ts +11 -11
  77. package/src/model.ts +4 -4
  78. package/src/ports.ts +15 -15
  79. package/src/rotulo.ts +4 -4
  80. package/src/rtc.ts +91 -91
  81. package/src/services/client.ts +248 -217
  82. package/src/services/gzip.ts +7 -7
  83. package/src/services/index.ts +12 -10
  84. package/src/services/outcome.ts +9 -9
  85. package/src/services/proc.ts +50 -49
  86. package/src/services/recuperar.ts +29 -25
  87. package/src/services/resolver.ts +25 -23
  88. package/src/services/soap.ts +35 -35
  89. package/src/time.ts +6 -6
  90. 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 { ufBySigla } from '@sinete/core';
9
- import { parseCnpj } from '@sinete/validators';
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 { NfeInput } from '../model.ts';
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: NfeInput, ide: IdeDestinatario, issues: Issues): void {
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 = ufBySigla(e.UF)?.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 parseCnpj).
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 = parseCnpj(v);
115
- return r.ok ? r.value : undefined;
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);
@@ -3,9 +3,9 @@
3
3
  * `IBSCBSTot` (W34 a W59g) somando exatamente os valores dos itens.
4
4
  */
5
5
 
6
- import { firstChild, parseXml } from '@sinete/core/xml';
6
+ import { lerXml, primeiroFilho } from '@sinete/core/xml';
7
7
  import type { ComplexType } from '@sinete/schemas';
8
- import { serialize, validate } from '@sinete/schemas';
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 = parseXml(`<w xmlns="${NFE_NS}">${serialize(ct, nome, value as never, NFE_NS)}</w>`);
23
- const el = firstChild(doc.root, nome, NFE_NS);
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 ? [{ path: '', code: 'modelo_de_conteudo', message: 'grupo vazio' }] : validate(ct, el);
25
+ el === undefined ? [{ caminho: '', code: 'modelo_de_conteudo', mensagem: 'grupo vazio' }] : validar(ct, el);
26
26
  for (const e of erros)
27
- issues.add(`${path}${e.path.replace(/^\/[^/]+/, '').replace(/\//g, '.')}`, 'schema', `${e.code}: ${e.message}`);
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 { DecimalFormat } from '../format.ts';
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: DecimalFormat = D1302;
78
- const P: DecimalFormat = D0302A04;
79
- const PO: DecimalFormat = D0302A04_OPC;
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: DecimalFormat): string | undefined =>
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: DecimalFormat,
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, Signer, Uf } from '@sinete/core';
11
- import { tpAmbOf } from '@sinete/core';
12
- import { base64Encode, c14n, firstChild, parseXml, SHA1_DIGEST_INFO_PREFIX } from '@sinete/core/xml';
13
- import { parseCnpj } from '@sinete/validators';
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 { NfeInput } from '../model.ts';
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
- * `BuildNfeOptions.urlQrCode`.
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 = parseXml(xml);
91
- const inf = firstChild(doc.root, 'infNFe', NFE_NS);
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(base64Encode(await sha1(te.encode(c14n(inf)))));
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 = tpAmbOf(d.ambiente);
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: Signer): Promise<string> {
158
+ export async function assinarParametros(parametros: string, signer: Assinador): Promise<string> {
159
159
  const bytes = te.encode(parametros);
160
- if (signer.kind === 'data') return base64Encode(await signer.sign(bytes, 'SHA-1'));
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(SHA1_DIGEST_INFO_PREFIX.length + h.length);
163
- di.set(SHA1_DIGEST_INFO_PREFIX);
164
- di.set(h, SHA1_DIGEST_INFO_PREFIX.length);
165
- return base64Encode(await signer.signDigestInfo(di));
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: NfeInput, ide: IdeNfce, issues: Issues): void {
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 = parseCnpj(c.CNPJ, { path: `${path}.CNPJ` });
420
- if (!r.ok) issues.list.push(r.error);
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, Clock } from '@sinete/core';
10
- import { UnsupportedError } from '@sinete/core';
11
- import type { ComplexType, Particle, VigenciaEntry } from '@sinete/schemas';
12
- import { isComplexType, isElementParticle, isWildcard, selecionarPl } from '@sinete/schemas';
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: VigenciaEntry;
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: Clock): PlEscolhido {
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 UnsupportedError(`@sinete/nfe não conhece o módulo ${vigencia.modulo}; atualize o pacote`);
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 (isWildcard(p)) acc.add('$any');
41
- else if (isElementParticle(p)) acc.add(p.e);
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 (isWildcard(p)) return acc;
48
- if (isElementParticle(p)) {
49
- if (isComplexType(p.t)) acc.set(p.e, p.t);
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
  }
@@ -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 { ValidationIssue } from '@sinete/core';
10
+ import type { Ocorrencia } from '@sinete/core';
11
11
  import type { Issues } from '../issues.ts';
12
- import type { NfeInput } from '../model.ts';
12
+ import type { DadosNfe } from '../model.ts';
13
13
  import type { Instante } from '../time.ts';
14
- import { formatDh } from '../time.ts';
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: NfeInput, idDest: string, issues: Issues): void {
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
- formatDh(instante, offsetMinutes).slice(0, 10);
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: NfeInput,
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 `buildNfe` não monta (procEmi 0).
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: NfeInput,
114
+ nfe: DadosNfe,
115
115
  titular: { readonly cnpj?: string | undefined; readonly cpf?: string | undefined },
116
- ): readonly ValidationIssue[] {
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
- path: 'emitente.CNPJ',
123
+ caminho: 'emitente.CNPJ',
124
124
  code: 'emitente_difere_do_certificado',
125
- message: `CNPJ-base ${base} do emitente difere do CNPJ-base do certificado (F03, rejeição 213)`,
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
- path: 'emitente.CPF',
134
+ caminho: 'emitente.CPF',
135
135
  code: 'emitente_difere_do_certificado',
136
- message: 'CPF do emitente difere do CPF do certificado (F03A, rejeição 227)',
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
+ }