@sinete/nfe 0.0.0 → 0.2.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 (101) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +148 -1
  4. package/dist/build/build.d.ts +127 -0
  5. package/dist/build/build.d.ts.map +1 -0
  6. package/dist/build/destinatario.d.ts +19 -0
  7. package/dist/build/destinatario.d.ts.map +1 -0
  8. package/dist/build/ibscbs.d.ts +27 -0
  9. package/dist/build/ibscbs.d.ts.map +1 -0
  10. package/dist/build/icms.d.ts +42 -0
  11. package/dist/build/icms.d.ts.map +1 -0
  12. package/dist/build/nfce.d.ts +122 -0
  13. package/dist/build/nfce.d.ts.map +1 -0
  14. package/dist/build/pl.d.ts +19 -0
  15. package/dist/build/pl.d.ts.map +1 -0
  16. package/dist/build/rejeicoes.d.ts +52 -0
  17. package/dist/build/rejeicoes.d.ts.map +1 -0
  18. package/dist/build/tributos.d.ts +45 -0
  19. package/dist/build/tributos.d.ts.map +1 -0
  20. package/dist/build/values.d.ts +48 -0
  21. package/dist/build/values.d.ts.map +1 -0
  22. package/dist/decimal.d.ts +66 -0
  23. package/dist/decimal.d.ts.map +1 -0
  24. package/dist/format.d.ts +48 -0
  25. package/dist/format.d.ts.map +1 -0
  26. package/dist/ibs-cbs.d.ts +14 -0
  27. package/dist/ibs-cbs.d.ts.map +1 -0
  28. package/dist/ibs-cbs.js +43 -0
  29. package/dist/ibs-cbs.js.map +10 -0
  30. package/dist/index.d.ts +37 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +4399 -0
  33. package/dist/index.js.map +33 -0
  34. package/dist/issues.d.ts +24 -0
  35. package/dist/issues.d.ts.map +1 -0
  36. package/dist/model.d.ts +861 -0
  37. package/dist/model.d.ts.map +1 -0
  38. package/dist/ports.d.ts +109 -0
  39. package/dist/ports.d.ts.map +1 -0
  40. package/dist/rotulo.d.ts +12 -0
  41. package/dist/rotulo.d.ts.map +1 -0
  42. package/dist/rtc.d.ts +65 -0
  43. package/dist/rtc.d.ts.map +1 -0
  44. package/dist/services/client.d.ts +277 -0
  45. package/dist/services/client.d.ts.map +1 -0
  46. package/dist/services/gzip.d.ts +7 -0
  47. package/dist/services/gzip.d.ts.map +1 -0
  48. package/dist/services/index.d.ts +14 -0
  49. package/dist/services/index.d.ts.map +1 -0
  50. package/dist/services/outcome.d.ts +15 -0
  51. package/dist/services/outcome.d.ts.map +1 -0
  52. package/dist/services/proc.d.ts +41 -0
  53. package/dist/services/proc.d.ts.map +1 -0
  54. package/dist/services/recuperar.d.ts +28 -0
  55. package/dist/services/recuperar.d.ts.map +1 -0
  56. package/dist/services/resolver.d.ts +88 -0
  57. package/dist/services/resolver.d.ts.map +1 -0
  58. package/dist/services/soap.d.ts +43 -0
  59. package/dist/services/soap.d.ts.map +1 -0
  60. package/dist/time.d.ts +12 -0
  61. package/dist/time.d.ts.map +1 -0
  62. package/dist/versao-gerada.d.ts +3 -0
  63. package/dist/versao-gerada.d.ts.map +1 -0
  64. package/package.json +58 -3
  65. package/src/build/build.ts +1503 -0
  66. package/src/build/destinatario.ts +212 -0
  67. package/src/build/ibscbs.ts +135 -0
  68. package/src/build/icms.ts +645 -0
  69. package/src/build/nfce.ts +425 -0
  70. package/src/build/pl.ts +73 -0
  71. package/src/build/rejeicoes.ts +142 -0
  72. package/src/build/tributos.ts +217 -0
  73. package/src/build/values.ts +126 -0
  74. package/src/data/arredondamento.json +30 -0
  75. package/src/data/cstat.json +36 -0
  76. package/src/data/fusos.json +17 -0
  77. package/src/data/nfce-urls.json +170 -0
  78. package/src/data/produtor-rural.json +17 -0
  79. package/src/data/reforma.json +6 -0
  80. package/src/data/resp-tec.json +13 -0
  81. package/src/data/servicos.json +44 -0
  82. package/src/data/suframa.json +7 -0
  83. package/src/decimal.ts +232 -0
  84. package/src/format.ts +65 -0
  85. package/src/ibs-cbs.ts +89 -0
  86. package/src/index.ts +155 -0
  87. package/src/issues.ts +80 -0
  88. package/src/model.ts +1033 -0
  89. package/src/ports.ts +96 -0
  90. package/src/rotulo.ts +137 -0
  91. package/src/rtc.ts +305 -0
  92. package/src/services/client.ts +1116 -0
  93. package/src/services/gzip.ts +26 -0
  94. package/src/services/index.ts +45 -0
  95. package/src/services/outcome.ts +50 -0
  96. package/src/services/proc.ts +165 -0
  97. package/src/services/recuperar.ts +93 -0
  98. package/src/services/resolver.ts +127 -0
  99. package/src/services/soap.ts +133 -0
  100. package/src/time.ts +23 -0
  101. package/src/versao-gerada.ts +3 -0
@@ -0,0 +1,1503 @@
1
+ /**
2
+ * Montagem da NF-e: entrada do domínio (`DadosNfe`) para o objeto tipado do `@sinete/schemas` no PL vigente, com os
3
+ * derivados e os totais em decimal exato, a chave de acesso e as regras conferidas antes de qualquer serialização. A
4
+ * saída é a string canônica de `<NFe>` ainda sem assinatura, já validada contra o schema; `assinarNfe` insere a
5
+ * `Signature` por splice (ADR 0003) e nada mais toca nela.
6
+ */
7
+
8
+ import type { Ambiente, Assinador, ContextoDeTempo, Ocorrencia, Uf } from '@sinete/core';
9
+ import {
10
+ ErroDeConfiguracao,
11
+ ErroDeValidacao,
12
+ ehUf,
13
+ formatarVerProc,
14
+ relogioFixo,
15
+ tpAmbDoAmbiente,
16
+ ufPorSigla,
17
+ } from '@sinete/core';
18
+ import { assinarXml, codificarBase64, lerXml, primeiroFilho } from '@sinete/core/xml';
19
+ import type { ComplexType, EntradaDeVigencia } from '@sinete/schemas';
20
+ import { ErroSerializacao, serializar, validar } from '@sinete/schemas';
21
+ import type {
22
+ TNFe_infNFe,
23
+ TNFe_infNFe_det,
24
+ TNFe_infNFe_det_imposto,
25
+ TNFe_infNFe_det_prod,
26
+ TNFe_infNFe_ide_NFref,
27
+ TNFe_infNFe_total_ICMSTot,
28
+ TNFe_infNFe_total_ISSQNtot,
29
+ TTribNFe,
30
+ } from '@sinete/schemas/nfe/PL_010f';
31
+ import { TIS as TISCt, TNFe_infNFeSupl, TTribNFe as TTribNFeCt } from '@sinete/schemas/nfe/PL_010f';
32
+ import { lerChaveAcesso, lerCnpj, lerCpf, lerIe, montarChaveAcesso } from '@sinete/validators';
33
+ import arredondamento from '../data/arredondamento.json' with { type: 'json' };
34
+ import produtorRural from '../data/produtor-rural.json' with { type: 'json' };
35
+ import reforma from '../data/reforma.json' with { type: 'json' };
36
+ import respTecData from '../data/resp-tec.json' with { type: 'json' };
37
+ import type { RoundingMode } from '../decimal.ts';
38
+ import { Decimal } from '../decimal.ts';
39
+ import { D0302, D0302A04, D1104V, D1110V, D1203, D1302, D1302_OPC } from '../format.ts';
40
+ import { Issues } from '../issues.ts';
41
+ import type {
42
+ DadosNfe,
43
+ Destinatario,
44
+ DocumentoPessoa,
45
+ Emitente,
46
+ Endereco,
47
+ EnderecoExterior,
48
+ Item,
49
+ Local,
50
+ Referenciada,
51
+ ResponsavelTecnico,
52
+ } from '../model.ts';
53
+ import type { CalculadoraIbsCbs, PedidoIbsCbsItem } from '../ports.ts';
54
+ import { calculadoraIbsCbs } from '../rtc.ts';
55
+ import type { Instante } from '../time.ts';
56
+ import { deslocamentoDaUf, formatarDh } from '../time.ts';
57
+ import { VERSAO_PACOTE } from '../versao-gerada.ts';
58
+ import { conferirDestinatario } from './destinatario.ts';
59
+ import { decimaisInvalidos, grupoInvalido, ibsCbsDoItem, totalIbsCbs } from './ibscbs.ts';
60
+ import type { IcmsTotais } from './icms.ts';
61
+ import { buildIcms, zeroIcmsTotais } from './icms.ts';
62
+ import type { NfceSupl, PagMontado, QrCodeNfceOpcoes } from './nfce.ts';
63
+ import {
64
+ assinarParametros,
65
+ conferirNfce,
66
+ NFCE_LIMITE_SEM_DESTINATARIO,
67
+ pagamentoNfce,
68
+ parametrosQrCode,
69
+ urlsNfce,
70
+ } from './nfce.ts';
71
+ import { camposForaDoPl, escolherPl } from './pl.ts';
72
+ import { cstComIsento, vencimentos } from './rejeicoes.ts';
73
+ import { buildIcmsUfDest, buildIi, buildIpi, buildIssqn, buildPisCofins, buildPisCofinsSt } from './tributos.ts';
74
+ import type { Familia } from './values.ts';
75
+ import { Ctx, clean, digits, TOLERANCIA } from './values.ts';
76
+
77
+ let padrao: CalculadoraIbsCbs | undefined;
78
+
79
+ /** A calculadora de IBS/CBS usada quando `opcoes.ibsCbs` não vem: o motor do sinete, criada uma vez por processo. */
80
+ function calculadoraPadrao(): CalculadoraIbsCbs {
81
+ padrao ??= calculadoraIbsCbs();
82
+ return padrao;
83
+ }
84
+
85
+ export const NFE_NS = 'http://www.portalfiscal.inf.br/nfe';
86
+
87
+ /** Literal do nome do destinatário em homologação (MOC 7.0 Anexo I, RV E04-20, rejeição 598). */
88
+ export const XNOME_HOMOLOGACAO = 'NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL';
89
+
90
+ /** Literal da descrição do primeiro item da NFC-e em homologação (MOC 7.0 Anexo I, RV I04-10, rejeição 373). */
91
+ export const XPROD_HOMOLOGACAO_NFCE = 'NOTA FISCAL EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL';
92
+
93
+ export type ExigenciaRespTec = 'obrigatorio' | 'opcional';
94
+
95
+ export interface MontarNfeOpcoes {
96
+ readonly ambiente: Ambiente;
97
+ /** Relógio de emissão (dhEmi, PL vigente, CSRT) e de fato gerador (IBS/CBS). */
98
+ readonly tempo: ContextoDeTempo;
99
+ /**
100
+ * Calculadora de IBS/CBS dos itens com `ibsCbs.classificacao`. Padrão: `calculadoraIbsCbs()`, o motor do sinete com o
101
+ * dataset embarcado (importado na primeira nota que precisar dele) e as alíquotas oficiais. Informe outra para trocar
102
+ * dataset, alíquotas, base ou regras, ou para calcular fora do sinete.
103
+ */
104
+ readonly ibsCbs?: CalculadoraIbsCbs;
105
+ /** Fuso do emitente em minutos; padrão pela UF (`data/fusos.json`). */
106
+ readonly deslocamentoMin?: number;
107
+ /** Versão do aplicativo emissor (`verProc`); padrão `sinete <versão do @sinete/nfe>` (`formatarVerProc`). */
108
+ readonly verProc?: string;
109
+ /** Sobrepõe o modo de arredondamento de uma família (`data/arredondamento.json`). */
110
+ readonly arredondamento?: Partial<Record<Familia, RoundingMode>>;
111
+ /** Responsável técnico padrão (a software house), usado quando a nota não traz `respTec`. */
112
+ readonly respTec?: ResponsavelTecnico;
113
+ /** Sobrepõe a exigência de infRespTec e CSRT da tabela por UF (`data/resp-tec.json`). */
114
+ readonly exigencias?: { readonly infRespTec?: ExigenciaRespTec; readonly csrt?: ExigenciaRespTec };
115
+ /** Fonte de aleatoriedade para o cNF; padrão `crypto.getRandomValues`. */
116
+ readonly aleatorio?: (bytes: Uint8Array) => Uint8Array;
117
+ /**
118
+ * O `vPag` do único `detPag` passa a ser o `vNF` calculado na mesma montagem, e o valor informado nele é ignorado.
119
+ * Serve a quem recebe à vista o total da nota e não quer calcular o `vNF` antes de montar. Mais de um `detPag`,
120
+ * nenhum, ou `tPag` 90 (sem pagamento) dão ocorrência `pagamento_igual_total`: não há como saber como repartir o
121
+ * total, e a nota sem pagamento tem `vPag` zero.
122
+ */
123
+ readonly pagamentoIgualTotal?: boolean;
124
+ /**
125
+ * Só NFC-e (modelo 65): versão do QR Code. Padrão `{ versao: '3' }`, que dispensa o CSC (NT 2025.001). Com
126
+ * `{ versao: '2', idCSC, CSC }`, o hash leva o CSC fornecido pela SEFAZ da UF, que nunca vai para o XML.
127
+ */
128
+ readonly qrCode?: QrCodeNfceOpcoes;
129
+ /**
130
+ * Só NFC-e: endereço da consulta via QR Code (sem o `?p=`). Padrão: o da UF do emitente no ambiente e na data de
131
+ * emissão (`data/nfce-urls.json`, do Portal Nacional da NFC-e). AM e MA publicam o endereço sem o protocolo: lá, é
132
+ * obrigatório.
133
+ */
134
+ readonly urlQrCode?: string;
135
+ /** Só NFC-e: URL da consulta por chave de acesso (`urlChave`). Padrão: a da UF (`data/nfce-urls.json`). */
136
+ readonly urlChave?: string;
137
+ }
138
+
139
+ export interface NfeMontada {
140
+ /** Chave de acesso (44 posições). */
141
+ readonly chave: string;
142
+ /** `Id` do `infNFe` (`NFe` + chave): é o que a assinatura referencia. */
143
+ readonly id: string;
144
+ readonly cNF: string;
145
+ readonly cDV: string;
146
+ /** 55 (NF-e) ou 65 (NFC-e). */
147
+ readonly mod: '55' | '65';
148
+ readonly tpEmis: string;
149
+ readonly dhEmi: string;
150
+ /**
151
+ * Só NFC-e: o `infNFeSupl` a acrescentar (`comQrCode`, `assinarNfe`). Na versão 3 em contingência off-line, falta a
152
+ * assinatura dos parâmetros, que só o certificado faz (`assinaturaQrCode`).
153
+ */
154
+ readonly nfce?: NfceSupl;
155
+ /** Pacote de liberação usado (escolhido pela vigência). */
156
+ readonly pl: EntradaDeVigencia;
157
+ /** O objeto tipado, na forma do PL_010f (o mais novo que o pacote conhece). */
158
+ readonly infNFe: TNFe_infNFe;
159
+ /** `<NFe xmlns="...">` com o `infNFe` canônico, sem assinatura, já validado contra o schema. */
160
+ readonly xml: string;
161
+ }
162
+
163
+ export type ResultadoMontagemNfe =
164
+ | { readonly ok: true; readonly valor: NfeMontada }
165
+ | { readonly ok: false; readonly ocorrencias: readonly Ocorrencia[] };
166
+
167
+ const MODES: Record<Familia, RoundingMode> = Object.fromEntries(
168
+ Object.entries(arredondamento.familias).map(([k, v]) => [k, v.modo as RoundingMode]),
169
+ ) as Record<Familia, RoundingMode>;
170
+
171
+ const SEM_GTIN = 'SEM GTIN';
172
+ const BRASILIA = -180;
173
+
174
+ function diaBrasilia(d: Instante): string {
175
+ return formatarDh(d, BRASILIA).slice(0, 10);
176
+ }
177
+
178
+ /** Exigência de infRespTec ou CSRT para a UF no ambiente e na data (tabela `data/resp-tec.json`). */
179
+ export function exigenciaRespTec(
180
+ uf: Uf,
181
+ ambiente: Ambiente,
182
+ data: Instante,
183
+ ): { readonly infRespTec: ExigenciaRespTec; readonly csrt: ExigenciaRespTec } {
184
+ const dia = diaBrasilia(data);
185
+ const ufs = respTecData.ufs as Readonly<
186
+ Record<string, { readonly csrt?: Record<string, string>; readonly infRespTec?: Record<string, string> }>
187
+ >;
188
+ const regra = Object.hasOwn(ufs, uf) ? ufs[uf] : undefined;
189
+ const vale = (r: Record<string, string> | undefined, padrao: string): ExigenciaRespTec => {
190
+ const desde = r?.[ambiente];
191
+ if (desde !== undefined) return dia >= desde ? 'obrigatorio' : (padrao as ExigenciaRespTec);
192
+ return padrao as ExigenciaRespTec;
193
+ };
194
+ return {
195
+ infRespTec: vale(regra?.infRespTec, respTecData.padrao.infRespTec),
196
+ csrt: vale(regra?.csrt, respTecData.padrao.csrt),
197
+ };
198
+ }
199
+
200
+ /** `hashCSRT`: Base64(SHA-1(CSRT + chave de acesso)) (NT 2018.005, campo ZD09). */
201
+ export async function hashCsrt(csrt: string, chave: string): Promise<string> {
202
+ const d = await globalThis.crypto.subtle.digest('SHA-1', new TextEncoder().encode(csrt + chave));
203
+ return codificarBase64(new Uint8Array(d));
204
+ }
205
+
206
+ /** Normaliza CNPJ ou CPF, apontando a ocorrência do validador. */
207
+ function documento(doc: DocumentoPessoa, path: string, issues: Issues): { CNPJ: string } | { CPF: string } {
208
+ if (doc.CNPJ !== undefined) {
209
+ const r = lerCnpj(doc.CNPJ, { caminho: `${path}.CNPJ` });
210
+ if (!r.ok) issues.list.push(r.erro);
211
+ return { CNPJ: r.ok ? r.valor : doc.CNPJ };
212
+ }
213
+ const r = lerCpf(doc.CPF ?? '', { caminho: `${path}.CPF` });
214
+ if (!r.ok) issues.list.push(r.erro);
215
+ return { CPF: r.ok ? r.valor : (doc.CPF ?? '') };
216
+ }
217
+
218
+ function ie(value: string | undefined, uf: Uf, path: string, issues: Issues, allowIsento: boolean): string | undefined {
219
+ if (value === undefined) return undefined;
220
+ const r = lerIe(value, uf, { caminho: path, aceitarIsento: allowIsento });
221
+ if (!r.ok) {
222
+ issues.list.push(r.erro);
223
+ return value;
224
+ }
225
+ return r.valor.valor;
226
+ }
227
+
228
+ function endereco(e: Endereco): Record<string, string | undefined> {
229
+ return {
230
+ xLgr: e.xLgr,
231
+ nro: e.nro,
232
+ xCpl: e.xCpl,
233
+ xBairro: e.xBairro,
234
+ cMun: e.cMun,
235
+ xMun: e.xMun,
236
+ UF: e.UF,
237
+ CEP: e.CEP === undefined ? undefined : digits(e.CEP),
238
+ fone: e.fone === undefined ? undefined : digits(e.fone),
239
+ };
240
+ }
241
+
242
+ function isExterior(e: Endereco | EnderecoExterior | undefined): e is EnderecoExterior {
243
+ return e !== undefined && 'exterior' in e && e.exterior === true;
244
+ }
245
+
246
+ /** Gera um cNF de 8 dígitos que passa nas regras de emissão da chave (RV B03-10: fora das sequências e diferente do nNF). */
247
+ function gerarChave(
248
+ parts: { cUF: string; aamm: string; emitente: string; mod: string; serie: string; nNF: string; tpEmis: string },
249
+ cNFInformado: string | undefined,
250
+ random: (b: Uint8Array) => Uint8Array,
251
+ issues: Issues,
252
+ ): { chave: string; cNF: string } | undefined {
253
+ const tentar = (cNF: string): { chave: string; ok: boolean; issue?: Ocorrencia } => {
254
+ const chave = montarChaveAcesso({ ...parts, cNF });
255
+ const r = lerChaveAcesso(chave, { emissao: true, caminho: 'chave' });
256
+ return r.ok ? { chave, ok: true } : { chave, ok: false, issue: r.erro };
257
+ };
258
+ if (cNFInformado !== undefined) {
259
+ if (!/^\d{8}$/.test(cNFInformado)) {
260
+ issues.add('cNF', 'campo_invalido', 'cNF tem 8 algarismos');
261
+ return undefined;
262
+ }
263
+ const r = tentar(cNFInformado);
264
+ if (!r.ok && r.issue) {
265
+ // O cNF informado recusado pela regra da chave é da entrada, no campo dela; outra parte da chave recusada aponta
266
+ // a chave montada (a causa, se for da entrada, já apareceu no campo dela).
267
+ const doCnf = r.issue.code === 'chave_cnf_invalido';
268
+ issues.list.push({
269
+ ...r.issue,
270
+ caminho: doCnf ? 'cNF' : r.issue.caminho,
271
+ code: 'chave_invalida',
272
+ mensagem: `${r.issue.mensagem} (${r.issue.code})`,
273
+ origem: doCnf ? 'entrada' : 'montagem',
274
+ });
275
+ }
276
+ return r.ok ? { chave: r.chave, cNF: cNFInformado } : undefined;
277
+ }
278
+ for (let n = 0; n < 32; n++) {
279
+ const b = random(new Uint8Array(4));
280
+ const v = ((b[0] ?? 0) * 2 ** 24 + (b[1] ?? 0) * 2 ** 16 + (b[2] ?? 0) * 2 ** 8 + (b[3] ?? 0)) % 100_000_000;
281
+ const cNF = String(v).padStart(8, '0');
282
+ const r = tentar(cNF);
283
+ if (r.ok) return { chave: r.chave, cNF };
284
+ if (r.issue && r.issue.code !== 'chave_cnf_invalido') {
285
+ // A chave com o cNF gerado aqui foi recusada por outra parte (UF, emitente, série): a ocorrência aponta a chave
286
+ // montada; a causa, se for da entrada, já apareceu no campo dela.
287
+ issues.list.push({
288
+ ...r.issue,
289
+ code: 'chave_invalida',
290
+ mensagem: `${r.issue.mensagem} (${r.issue.code})`,
291
+ origem: 'montagem',
292
+ });
293
+ return undefined;
294
+ }
295
+ }
296
+ issues.montagem('cNF', 'chave_invalida', 'não foi possível gerar um cNF válido');
297
+ return undefined;
298
+ }
299
+
300
+ function referencia(ref: Referenciada, path: string, issues: Issues): TNFe_infNFe_ide_NFref {
301
+ const chave = (c: string, p: string): string => {
302
+ const r = lerChaveAcesso(c, { caminho: p });
303
+ if (!r.ok) issues.list.push(r.erro);
304
+ return r.ok ? r.valor.chave : c;
305
+ };
306
+ if ('refNFe' in ref) return { refNFe: chave(ref.refNFe, `${path}.refNFe`) };
307
+ if ('refNFeSig' in ref) {
308
+ // Chave com o código numérico zerado para sigilo (NT 2022.003, BA02a): o DV é o da chave original, então a
309
+ // conferência do DV não se aplica; confere-se a forma, o modelo 55 e o cNF zerado.
310
+ const c = ref.refNFeSig.replace(/\s/g, '').toUpperCase();
311
+ if (!/^[0-9]{6}[0-9A-Z]{12}[0-9]{2}55[0-9]{22}$/.test(c) || c.slice(35, 43) !== '00000000') {
312
+ issues.add(`${path}.refNFeSig`, 'chave_invalida', 'chave de NF-e (modelo 55) com o código numérico (cNF) zerado');
313
+ }
314
+ return { refNFeSig: c };
315
+ }
316
+ if ('refCTe' in ref) return { refCTe: chave(ref.refCTe, `${path}.refCTe`) };
317
+ if ('refNF' in ref) return { refNF: ref.refNF };
318
+ if ('refECF' in ref) return { refECF: ref.refECF };
319
+ const nfp = ref.refNFP;
320
+ // Nota de produtor em papel (modelo 04) extinta: só referência a nota emitida antes do fim na UF dela.
321
+ if (nfp.mod === '04') {
322
+ const porUf = produtorRural.refNFP.ufs as Readonly<Record<string, { readonly vedadaDesde: string }>>;
323
+ const cUfSigla = Object.keys(porUf).find((k) => ufPorSigla(k)?.cUF === nfp.cUF);
324
+ const vedadaDesde = cUfSigla ? (porUf[cUfSigla]?.vedadaDesde ?? '') : produtorRural.refNFP.modelo04.vedadaDesde;
325
+ if (nfp.AAMM >= vedadaDesde) {
326
+ issues.add(
327
+ `${path}.refNFP.AAMM`,
328
+ 'referencia_vedada',
329
+ `nota de produtor modelo 04 emitida em ${nfp.AAMM}, depois do fim do modelo (${vedadaDesde}; ${produtorRural.refNFP.modelo04.fonte})`,
330
+ );
331
+ }
332
+ }
333
+ const doc = documento(nfp as DocumentoPessoa, `${path}.refNFP`, issues);
334
+ return {
335
+ refNFP: {
336
+ cUF: nfp.cUF,
337
+ AAMM: nfp.AAMM,
338
+ ...doc,
339
+ IE: nfp.IE,
340
+ mod: nfp.mod,
341
+ serie: String(Number(nfp.serie)),
342
+ nNF: String(Number(nfp.nNF)),
343
+ },
344
+ } as TNFe_infNFe_ide_NFref;
345
+ }
346
+
347
+ /** 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. */
348
+ function textoXmlValido(texto: string): boolean {
349
+ for (const ch of texto) {
350
+ const c = ch.codePointAt(0) ?? 0;
351
+ const ok =
352
+ c === 0x9 ||
353
+ c === 0xa ||
354
+ c === 0xd ||
355
+ (c >= 0x20 && c <= 0xd7ff) ||
356
+ (c >= 0xe000 && c <= 0xfffd) ||
357
+ (c >= 0x10000 && c <= 0x10ffff);
358
+ if (!ok) return false;
359
+ }
360
+ return true;
361
+ }
362
+
363
+ /**
364
+ * Texto com caractere que o XML não representa (vindo, por exemplo, de uma descrição importada) vira ocorrência com o
365
+ * caminho do campo, em vez de falhar no parse do documento montado.
366
+ */
367
+ function textosForaDoXml(value: unknown, path: string, issues: Issues): void {
368
+ if (typeof value === 'string') {
369
+ if (!textoXmlValido(value)) issues.montagem(path, 'campo_invalido', 'texto com caractere não permitido em XML');
370
+ } else if (Array.isArray(value)) {
371
+ for (const [n, v] of value.entries()) textosForaDoXml(v, `${path}[${n}]`, issues);
372
+ } else if (typeof value === 'object' && value !== null) {
373
+ for (const [k, v] of Object.entries(value)) textosForaDoXml(v, `${path}.${k}`, issues);
374
+ }
375
+ }
376
+
377
+ interface ItemMontado {
378
+ readonly det: TNFe_infNFe_det;
379
+ readonly icms: IcmsTotais;
380
+ readonly indTot: boolean;
381
+ readonly issqn: boolean;
382
+ readonly faturamentoDireto: boolean;
383
+ readonly v: Record<
384
+ | 'vProd'
385
+ | 'vFrete'
386
+ | 'vSeg'
387
+ | 'vDesc'
388
+ | 'vOutro'
389
+ | 'vII'
390
+ | 'vIPI'
391
+ | 'vIPIDevol'
392
+ | 'vPIS'
393
+ | 'vCOFINS'
394
+ | 'vPISST'
395
+ | 'vCOFINSST'
396
+ | 'vFCPUFDest'
397
+ | 'vICMSUFDest'
398
+ | 'vICMSUFRemet'
399
+ | 'vTotTrib'
400
+ | 'vISSQN'
401
+ | 'vBCISS',
402
+ Decimal
403
+ >;
404
+ readonly temUfDest: boolean;
405
+ readonly temTotTrib: boolean;
406
+ readonly issqnOpcionais: Record<string, Decimal>;
407
+ }
408
+
409
+ function montarItem(ctx: Ctx, item: Item, n: number, normal: boolean): ItemMontado {
410
+ const path = `itens[${n}]`;
411
+ const p = item.produto;
412
+ const pp = `${path}.produto`;
413
+ const qCom = ctx.req(p.qCom, `${pp}.qCom`, D1104V);
414
+ const vUnCom = ctx.req(p.vUnCom, `${pp}.vUnCom`, D1110V);
415
+ // RV I11 (rejeição 629) só vale para a NF-e normal (finNFe 1); nas demais o vProd informado prevalece.
416
+ const vProd = (normal ? ctx.calc : ctx.base).call(ctx, p.vProd, qCom.times(vUnCom), `${pp}.vProd`, D1302, 'produto');
417
+ const uTrib = p.uTrib ?? p.uCom;
418
+ const qTrib = p.qTrib === undefined ? qCom : ctx.req(p.qTrib, `${pp}.qTrib`, D1104V);
419
+ let vUnTrib: Decimal;
420
+ if (p.vUnTrib !== undefined) vUnTrib = ctx.req(p.vUnTrib, `${pp}.vUnTrib`, D1110V);
421
+ else if (uTrib === p.uCom && qTrib.eq(qCom)) vUnTrib = vUnCom;
422
+ else if (qTrib.isZero()) vUnTrib = Decimal.ZERO;
423
+ else vUnTrib = vProd.dividedBy(qTrib, 10, ctx.modes.produto);
424
+ // RV I11 (rejeição 630), só na NF-e normal: vProd confere também com qTrib × vUnTrib, com a tolerância da nota (*4).
425
+ if (normal) {
426
+ const trib = ctx.round(qTrib.times(vUnTrib), D1302, 'produto');
427
+ if (trib.minus(vProd).abs().gt(TOLERANCIA)) {
428
+ ctx.issues.add(
429
+ `${pp}.vUnTrib`,
430
+ 'valor_divergente',
431
+ `qTrib × vUnTrib (${trib.toFixed(2)}) difere de vProd (${vProd.toFixed(2)}) em mais de ${TOLERANCIA.toString()}`,
432
+ );
433
+ }
434
+ }
435
+ // Campos TDec_1302Opc: zero informado é aceito e omitido do XML (o pattern não aceita zero).
436
+ const opt = (v: typeof p.vFrete, k: string): Decimal => ctx.opt(v, `${pp}.${k}`, D1302) ?? Decimal.ZERO;
437
+ const vFrete = opt(p.vFrete, 'vFrete');
438
+ const vSeg = opt(p.vSeg, 'vSeg');
439
+ const vDesc = opt(p.vDesc, 'vDesc');
440
+ const vOutro = opt(p.vOutro, 'vOutro');
441
+ const vOp = vProd.plus(vFrete).plus(vSeg).plus(vOutro).minus(vDesc);
442
+ const cEAN = p.cEAN ?? SEM_GTIN;
443
+ const especifico = p.especifico ?? {};
444
+ const prod = clean({
445
+ cProd: p.cProd,
446
+ cEAN,
447
+ cBarra: p.cBarra,
448
+ xProd: p.xProd,
449
+ NCM: digits(p.NCM),
450
+ NVE: p.NVE as string[] | undefined,
451
+ CEST: p.CEST === undefined ? undefined : digits(p.CEST),
452
+ indEscala: p.indEscala,
453
+ CNPJFab: p.CNPJFab,
454
+ cBenef: p.cBenef,
455
+ gCred: p.gCred,
456
+ tpCredPresIBSZFM: p.tpCredPresIBSZFM,
457
+ EXTIPI: p.EXTIPI,
458
+ CFOP: digits(p.CFOP),
459
+ uCom: p.uCom,
460
+ qCom: ctx.s(qCom, D1104V),
461
+ vUnCom: ctx.s(vUnCom, D1110V),
462
+ vProd: ctx.s(vProd, D1302),
463
+ cEANTrib: p.cEANTrib ?? cEAN,
464
+ cBarraTrib: p.cBarraTrib,
465
+ uTrib,
466
+ qTrib: ctx.s(qTrib, D1104V),
467
+ vUnTrib: ctx.s(vUnTrib, D1110V),
468
+ vFrete: ctx.so(vFrete, D1302_OPC),
469
+ vSeg: ctx.so(vSeg, D1302_OPC),
470
+ vDesc: ctx.so(vDesc, D1302_OPC),
471
+ vOutro: ctx.so(vOutro, D1302_OPC),
472
+ indTot: p.indTot ?? '1',
473
+ indBemMovelUsado: p.indBemMovelUsado,
474
+ DI: p.DI,
475
+ detExport: p.detExport,
476
+ xPed: p.xPed,
477
+ nItemPed: p.nItemPed,
478
+ nFCI: p.nFCI,
479
+ rastro: p.rastro,
480
+ infProdNFF: p.infProdNFF,
481
+ infProdEmb: p.infProdEmb,
482
+ ...especifico,
483
+ }) as unknown as TNFe_infNFe_det_prod;
484
+
485
+ const imp = item.impostos;
486
+ const ip = `${path}.impostos`;
487
+ const imposto: Record<string, unknown> = {};
488
+ const z = Decimal.ZERO;
489
+ const v: ItemMontado['v'] = {
490
+ vProd,
491
+ vFrete,
492
+ vSeg,
493
+ vDesc,
494
+ vOutro,
495
+ vII: z,
496
+ vIPI: z,
497
+ vIPIDevol: z,
498
+ vPIS: z,
499
+ vCOFINS: z,
500
+ vPISST: z,
501
+ vCOFINSST: z,
502
+ vFCPUFDest: z,
503
+ vICMSUFDest: z,
504
+ vICMSUFRemet: z,
505
+ vTotTrib: z,
506
+ vISSQN: z,
507
+ vBCISS: z,
508
+ };
509
+ let issqnOpcionais: Record<string, Decimal> = {};
510
+ const vTotTrib = ctx.opt(imp.vTotTrib, `${ip}.vTotTrib`, D1302);
511
+ if (vTotTrib !== undefined) {
512
+ imposto.vTotTrib = ctx.s(vTotTrib, D1302);
513
+ v.vTotTrib = vTotTrib;
514
+ }
515
+ if (imp.icms !== undefined && imp.issqn !== undefined) {
516
+ ctx.issues.add(ip, 'combinacao_invalida', 'o item tem ICMS ou ISSQN, nunca os dois');
517
+ }
518
+ if (imp.ipi !== undefined) {
519
+ const r = buildIpi(ctx, imp.ipi, vOp, `${ip}.ipi`);
520
+ imposto.IPI = r.grupo;
521
+ v.vIPI = r.vIPI;
522
+ }
523
+ let icms = zeroIcmsTotais();
524
+ if (imp.icms !== undefined) {
525
+ const r = buildIcms(ctx, imp.icms, { vOp, vIPI: v.vIPI }, `${ip}.icms`);
526
+ imposto.ICMS = r.grupo;
527
+ icms = r.totais;
528
+ } else if (imp.issqn === undefined && imp.ibsCbs === undefined) {
529
+ ctx.issues.add(`${ip}.icms`, 'campo_obrigatorio', 'o item precisa de ICMS (ou ISSQN, na NF-e conjugada)');
530
+ }
531
+ if (imp.ii !== undefined) {
532
+ const r = buildIi(ctx, imp.ii, `${ip}.ii`);
533
+ imposto.II = r.grupo;
534
+ v.vII = r.vII;
535
+ }
536
+ if (imp.issqn !== undefined) {
537
+ const r = buildIssqn(ctx, imp.issqn, `${ip}.issqn`);
538
+ imposto.ISSQN = r.grupo;
539
+ v.vISSQN = r.vISSQN;
540
+ v.vBCISS = r.vBC;
541
+ issqnOpcionais = r.opcionais;
542
+ }
543
+ if (imp.pis !== undefined) {
544
+ const r = buildPisCofins(ctx, imp.pis, 'PIS', `${ip}.pis`);
545
+ imposto.PIS = r.grupo;
546
+ v.vPIS = r.valor;
547
+ }
548
+ if (imp.pisSt !== undefined) {
549
+ const r = buildPisCofinsSt(ctx, imp.pisSt, 'PIS', `${ip}.pisSt`);
550
+ imposto.PISST = r.grupo;
551
+ if (r.soma) v.vPISST = r.valor;
552
+ }
553
+ if (imp.cofins !== undefined) {
554
+ const r = buildPisCofins(ctx, imp.cofins, 'COFINS', `${ip}.cofins`);
555
+ imposto.COFINS = r.grupo;
556
+ v.vCOFINS = r.valor;
557
+ }
558
+ if (imp.cofinsSt !== undefined) {
559
+ const r = buildPisCofinsSt(ctx, imp.cofinsSt, 'COFINS', `${ip}.cofinsSt`);
560
+ imposto.COFINSST = r.grupo;
561
+ if (r.soma) v.vCOFINSST = r.valor;
562
+ }
563
+ if (imp.icmsUfDest !== undefined) {
564
+ const r = buildIcmsUfDest(ctx, imp.icmsUfDest, `${ip}.icmsUfDest`);
565
+ imposto.ICMSUFDest = r.grupo;
566
+ v.vFCPUFDest = r.vFCPUFDest;
567
+ v.vICMSUFDest = r.vICMSUFDest;
568
+ v.vICMSUFRemet = r.vICMSUFRemet;
569
+ }
570
+ if (imp.is !== undefined) imposto.IS = imp.is;
571
+ if (imp.ibsCbs?.grupo !== undefined) imposto.IBSCBS = imp.ibsCbs.grupo;
572
+
573
+ const det: Record<string, unknown> = {
574
+ nItem: String(n + 1),
575
+ prod,
576
+ imposto: imposto as unknown as TNFe_infNFe_det_imposto,
577
+ };
578
+ if (item.impostoDevol !== undefined) {
579
+ const pDevol = ctx.req(item.impostoDevol.pDevol, `${path}.impostoDevol.pDevol`, D0302);
580
+ const vIPIDevol = ctx.req(item.impostoDevol.vIPIDevol, `${path}.impostoDevol.vIPIDevol`, D1302);
581
+ det.impostoDevol = { pDevol: ctx.s(pDevol, D0302), IPI: { vIPIDevol: ctx.s(vIPIDevol, D1302) } };
582
+ v.vIPIDevol = vIPIDevol;
583
+ }
584
+ if (item.infAdProd !== undefined) det.infAdProd = item.infAdProd;
585
+ if (item.obsItem !== undefined) det.obsItem = item.obsItem;
586
+ if (item.DFeReferenciado !== undefined) {
587
+ const r = lerChaveAcesso(item.DFeReferenciado.chaveAcesso, { caminho: `${path}.DFeReferenciado.chaveAcesso` });
588
+ if (!r.ok) ctx.issues.list.push(r.erro);
589
+ det.DFeReferenciado = clean({
590
+ chaveAcesso: r.ok ? r.valor.chave : item.DFeReferenciado.chaveAcesso,
591
+ nItem: item.DFeReferenciado.nItem === undefined ? undefined : String(item.DFeReferenciado.nItem),
592
+ });
593
+ }
594
+ return {
595
+ det: det as unknown as TNFe_infNFe_det,
596
+ icms,
597
+ indTot: (p.indTot ?? '1') === '1',
598
+ issqn: imp.issqn !== undefined,
599
+ faturamentoDireto: (especifico as { veicProd?: { tpOp?: string } }).veicProd?.tpOp === '2',
600
+ v,
601
+ temUfDest: imp.icmsUfDest !== undefined,
602
+ temTotTrib: vTotTrib !== undefined,
603
+ issqnOpcionais,
604
+ };
605
+ }
606
+
607
+ /** Parte do vItem/vNF comum aos dois (MOC W16-10 com NT 2020.005 e NT 2023.004; NT 2025.002 VB01-10 e VB01-20). */
608
+ function composicao(m: ItemMontado, incluirProd: boolean): Decimal {
609
+ const v = m.v;
610
+ let total = (incluirProd ? v.vProd : Decimal.ZERO)
611
+ .minus(v.vDesc)
612
+ .minus(m.icms.vICMSDesonDeduz)
613
+ .plus(v.vFrete)
614
+ .plus(v.vSeg)
615
+ .plus(v.vOutro)
616
+ .plus(v.vII)
617
+ .plus(v.vIPI)
618
+ .plus(v.vPISST)
619
+ .plus(v.vCOFINSST);
620
+ if (!m.faturamentoDireto) {
621
+ total = total.plus(m.icms.vST).plus(m.icms.vICMSMonoReten).plus(m.icms.vFCPST).plus(v.vIPIDevol);
622
+ }
623
+ return total;
624
+ }
625
+
626
+ /** Monta, calcula e valida. Nunca lança por dado do chamador: devolve as ocorrências. */
627
+ export async function montarNfe(entrada: DadosNfe, opcoes: MontarNfeOpcoes): Promise<ResultadoMontagemNfe> {
628
+ const issues = new Issues();
629
+ const finNFeIn = entrada.finNFe ?? '1';
630
+ const mod = entrada.modelo ?? '55';
631
+ if (mod !== '55' && mod !== '65') {
632
+ issues.add('modelo', 'modelo_nao_suportado', `modelo ${String(mod)}: o builder monta NF-e (55) e NFC-e (65)`);
633
+ return { ok: false, ocorrencias: issues.classificadas };
634
+ }
635
+ const nfce = mod === '65';
636
+ // Padrões da identificação resolvidos uma vez: o mesmo valor vai para o ide e para a calculadora de IBS/CBS. A NFC-e
637
+ // é presencial e para consumidor final (MOC 7.0 Anexo I, B25a-10 e B25b-20).
638
+ const indPres = entrada.indPres ?? (nfce ? '1' : finNFeIn === '2' || finNFeIn === '3' ? '0' : '9');
639
+ const indFinal = entrada.indFinal ?? (nfce || entrada.destinatario?.indIEDest === '9' ? '1' : '0');
640
+ const ctx = new Ctx(issues, { ...MODES, ...opcoes.arredondamento }, finNFeIn !== '2' && finNFeIn !== '3');
641
+ const pRedutorGov = entrada.gCompraGov && ctx.req(entrada.gCompraGov.pRedutor, 'gCompraGov.pRedutor', D0302A04);
642
+ const emitUf = entrada.emitente.endereco.UF;
643
+ if (!ehUf(emitUf)) {
644
+ issues.add('emitente.endereco.UF', 'campo_invalido', 'UF do emitente inválida');
645
+ return { ok: false, ocorrencias: issues.classificadas };
646
+ }
647
+ const cUF = ufPorSigla(emitUf)?.cUF ?? '';
648
+ const offset = opcoes.deslocamentoMin ?? deslocamentoDaUf(emitUf);
649
+ const agora = opcoes.tempo.emissao.agora();
650
+ // Um instante de fato gerador por montagem: o mesmo vai para a calculadora e para a regra de composição do vItem.
651
+ const fatoGerador = opcoes.tempo.fatoGerador.agora();
652
+ const dhEmi = formatarDh(agora, offset);
653
+ const aamm = dhEmi.slice(2, 4) + dhEmi.slice(5, 7);
654
+ const tpAmb = tpAmbDoAmbiente(opcoes.ambiente);
655
+
656
+ // PL vigente (ErroVigencia do schemas propaga: data fora de toda vigência é erro de configuração, não de dado).
657
+ // O PL sai do mesmo instante do dhEmi: reler o relógio numa virada de vigência escolheria outro PL.
658
+ const pl = escolherPl(opcoes.ambiente, relogioFixo(agora));
659
+
660
+ // Emitente (grupo C)
661
+ const e: Emitente = entrada.emitente;
662
+ const emitDoc = documento(e, 'emitente', issues);
663
+ const serie = Number(entrada.serie);
664
+ const nNF = Number(entrada.nNF);
665
+ if (!Number.isInteger(serie) || serie < 0 || serie > 999) issues.add('serie', 'serie_invalida', 'série de 0 a 999');
666
+ if (!Number.isInteger(nNF) || nNF < 1 || nNF > 999_999_999)
667
+ issues.add('nNF', 'campo_invalido', 'nNF de 1 a 999999999');
668
+ const [cpfIni, cpfFim] = produtorRural.series.cpf as [number, number];
669
+ const [cnpjIni, cnpjFim] = produtorRural.series.cnpj as [number, number];
670
+ if ('CPF' in emitDoc && (serie < cpfIni || serie > cpfFim)) {
671
+ issues.add(
672
+ 'serie',
673
+ 'serie_invalida',
674
+ `emitente pessoa física usa as séries ${cpfIni} a ${cpfFim} (${produtorRural.series.fonte})`,
675
+ );
676
+ } else if ('CNPJ' in emitDoc && Number.isInteger(serie) && serie >= 0 && (serie < cnpjIni || serie > cnpjFim)) {
677
+ issues.add(
678
+ 'serie',
679
+ 'serie_invalida',
680
+ `emitente CNPJ usa as séries ${cnpjIni} a ${cnpjFim} (${produtorRural.series.fonteCnpj})`,
681
+ );
682
+ }
683
+ const emit = clean({
684
+ ...emitDoc,
685
+ xNome: e.xNome,
686
+ xFant: e.xFant,
687
+ enderEmit: clean({ ...endereco(e.endereco), cPais: '1058', xPais: 'BRASIL' }),
688
+ IE: ie(e.IE, emitUf, 'emitente.IE', issues, true),
689
+ IEST: e.IEST,
690
+ IM: e.IM,
691
+ CNAE: e.CNAE,
692
+ CRT: e.CRT,
693
+ ISUFEmit: e.ISUFEmit,
694
+ });
695
+
696
+ // Destinatário (grupo E): obrigatório na NF-e (RV E01-10); na NFC-e, só na entrega a domicílio (E01-20, conferida
697
+ // com as demais regras da NFC-e).
698
+ const d: Destinatario | undefined = entrada.destinatario;
699
+ let dest: Record<string, unknown> | undefined;
700
+ let destUf: Uf | 'EX' | undefined;
701
+ if (d === undefined) {
702
+ if (!nfce) issues.add('destinatario', 'campo_obrigatorio', 'a NF-e exige o destinatário (E01-10, rejeição 719)');
703
+ } else {
704
+ const docDest =
705
+ 'idEstrangeiro' in d && d.idEstrangeiro !== undefined
706
+ ? { idEstrangeiro: d.idEstrangeiro }
707
+ : documento(d as DocumentoPessoa, 'destinatario', issues);
708
+ let enderDest: Record<string, string | undefined> | undefined;
709
+ if (isExterior(d.endereco)) {
710
+ destUf = 'EX';
711
+ enderDest = clean({
712
+ xLgr: d.endereco.xLgr,
713
+ nro: d.endereco.nro,
714
+ xCpl: d.endereco.xCpl,
715
+ xBairro: d.endereco.xBairro,
716
+ cMun: '9999999',
717
+ xMun: 'EXTERIOR',
718
+ UF: 'EX',
719
+ cPais: d.endereco.cPais,
720
+ xPais: d.endereco.xPais,
721
+ fone: d.endereco.fone === undefined ? undefined : digits(d.endereco.fone),
722
+ });
723
+ } else if (d.endereco !== undefined) {
724
+ destUf = d.endereco.UF;
725
+ enderDest = clean({ ...endereco(d.endereco), cPais: '1058', xPais: 'BRASIL' });
726
+ }
727
+ // IE sem UF (endereço ausente) ou no exterior não é conferida aqui: a E05-10 e a E17-40 recusam a combinação.
728
+ const destIe =
729
+ d.IE !== undefined && destUf !== undefined && destUf !== 'EX'
730
+ ? ie(d.IE, destUf, 'destinatario.IE', issues, false)
731
+ : d.IE;
732
+ dest = clean({
733
+ ...docDest,
734
+ xNome: opcoes.ambiente === 'homologacao' ? XNOME_HOMOLOGACAO : d.xNome,
735
+ enderDest,
736
+ indIEDest: d.indIEDest,
737
+ IE: destIe,
738
+ ISUF: d.ISUF,
739
+ IM: d.IM,
740
+ email: d.email,
741
+ });
742
+ }
743
+
744
+ // Identificação: contingência e forma de emissão
745
+ const cont = entrada.contingencia;
746
+ const tpEmis = cont?.tpEmis ?? '1';
747
+ if (cont !== undefined) {
748
+ if (cont.xJust.length < 15 || cont.xJust.length > 256) {
749
+ issues.add(
750
+ 'contingencia.xJust',
751
+ 'contingencia_invalida',
752
+ 'justificativa da contingência com 15 a 256 caracteres',
753
+ );
754
+ }
755
+ if (cont.dhCont.getTime() > agora.getTime()) {
756
+ issues.add('contingencia.dhCont', 'contingencia_invalida', 'entrada em contingência posterior à emissão');
757
+ }
758
+ if (!nfce && tpEmis === '9') {
759
+ issues.add(
760
+ 'contingencia.tpEmis',
761
+ 'contingencia_invalida',
762
+ 'contingência off-line (tpEmis 9) é só da NFC-e (B22-10, rejeição 711)',
763
+ );
764
+ }
765
+ if (nfce && (tpEmis === '6' || tpEmis === '7')) {
766
+ issues.add(
767
+ 'contingencia.tpEmis',
768
+ 'contingencia_invalida',
769
+ 'a NFC-e não é autorizada pela SVC; a contingência dela é off-line, tpEmis 9 (B22-70, rejeição 783)',
770
+ );
771
+ }
772
+ }
773
+
774
+ // Identificação que as regras do modelo conferem antes de montar.
775
+ // Na NFC-e a operação é sempre interna (B11a-10, rejeição 707): o endereço do consumidor não a torna interestadual.
776
+ const idDest =
777
+ entrada.idDest ?? (nfce ? '1' : destUf === 'EX' ? '3' : destUf !== undefined && destUf !== emitUf ? '2' : '1');
778
+ const tpImp = entrada.tpImp ?? (nfce ? '4' : '1');
779
+ conferirDestinatario(
780
+ entrada,
781
+ {
782
+ mod,
783
+ tpNF: entrada.tpNF,
784
+ idDest,
785
+ idDestInformado: entrada.idDest !== undefined,
786
+ indFinal,
787
+ producao: opcoes.ambiente === 'producao',
788
+ },
789
+ issues,
790
+ );
791
+ if (nfce) {
792
+ conferirNfce(
793
+ entrada,
794
+ {
795
+ tpNF: entrada.tpNF,
796
+ idDest,
797
+ tpImp,
798
+ finNFe: finNFeIn,
799
+ indFinal,
800
+ indPres,
801
+ },
802
+ issues,
803
+ );
804
+ } else if (tpImp === '4' || tpImp === '5') {
805
+ issues.add('tpImp', 'campo_invalido', 'tpImp 4 e 5 (DANFC-e) são da NFC-e (B21-20, rejeição 710)');
806
+ }
807
+ const qr: QrCodeNfceOpcoes = opcoes.qrCode ?? { versao: '3' };
808
+ if (nfce && qr.versao === '2') {
809
+ // O CSC e o idCSC são opções do montador, não da nota: as ocorrências são da montagem (ADR 0011).
810
+ if (!/^\d{1,6}$/.test(qr.idCSC) || Number(qr.idCSC) === 0) {
811
+ issues.montagem(
812
+ 'qrCode.idCSC',
813
+ 'qrcode_invalido',
814
+ 'idCSC tem de 1 a 6 algarismos, diferente de zero (Manual do DANFE NFC-e 6.0, 4.6)',
815
+ );
816
+ }
817
+ if (qr.CSC.length < 16 || qr.CSC.length > 36) {
818
+ issues.montagem(
819
+ 'qrCode.CSC',
820
+ 'qrcode_invalido',
821
+ 'o CSC tem de 16 a 36 caracteres (Manual do DANFE NFC-e 6.0, 4.6)',
822
+ );
823
+ }
824
+ if ('CPF' in emitDoc) {
825
+ issues.montagem(
826
+ 'qrCode.versao',
827
+ 'qrcode_invalido',
828
+ 'emitente pessoa física usa o QR Code versão 3 (NT 2025.001, ZX02-222, rejeição 444)',
829
+ );
830
+ }
831
+ }
832
+
833
+ // Chave de acesso e cNF
834
+ const random =
835
+ opcoes.aleatorio ??
836
+ ((b: Uint8Array): Uint8Array => globalThis.crypto.getRandomValues(b as Uint8Array<ArrayBuffer>));
837
+ const emitente14 = 'CNPJ' in emitDoc ? emitDoc.CNPJ : emitDoc.CPF;
838
+ const gerada = issues.empty
839
+ ? gerarChave(
840
+ { cUF, aamm, emitente: emitente14, mod, serie: String(serie), nNF: String(nNF), tpEmis },
841
+ entrada.cNF,
842
+ random,
843
+ issues,
844
+ )
845
+ : undefined;
846
+ const chave = gerada?.chave ?? '0'.repeat(44);
847
+ const cNF = gerada?.cNF ?? '00000000';
848
+ const cDV = chave.slice(43);
849
+
850
+ // Itens
851
+ if (entrada.itens.length === 0 || entrada.itens.length > 990) {
852
+ issues.add('itens', 'itens_limite', 'a NF-e tem de 1 a 990 itens');
853
+ }
854
+ const itens = entrada.itens.map((it, n) => montarItem(ctx, it, n, finNFeIn === '1'));
855
+ // Na NFC-e em homologação, a descrição do primeiro item é a literal da RV I04-10, como o nome do destinatário da
856
+ // E04-20: o builder a põe, e a descrição informada fica fora do XML de teste.
857
+ const primeiro = itens[0];
858
+ if (nfce && opcoes.ambiente === 'homologacao' && primeiro !== undefined) {
859
+ (primeiro.det.prod as unknown as Record<string, unknown>).xProd = XPROD_HOMOLOGACAO_NFCE;
860
+ }
861
+
862
+ // IBS/CBS pela calculadora das opções, ou pelo motor do sinete
863
+ const classificados = entrada.itens
864
+ .map((it, n) => ({ it, n }))
865
+ .filter(({ it }) => it.impostos.ibsCbs?.classificacao !== undefined);
866
+ if (classificados.length > 0) {
867
+ const calculadora = opcoes.ibsCbs ?? calculadoraPadrao();
868
+ const reqs: PedidoIbsCbsItem[] = classificados.map(({ it, n }) => {
869
+ const c = it.impostos.ibsCbs?.classificacao as NonNullable<
870
+ NonNullable<typeof it.impostos.ibsCbs>['classificacao']
871
+ >;
872
+ const m = itens[n] as ItemMontado;
873
+ const vBC = ctx.opt(c.vBC, `itens[${n}].impostos.ibsCbs.classificacao.vBC`, D1302);
874
+ const qTrib = Decimal.of((m.det.prod as { qTrib: string }).qTrib);
875
+ return {
876
+ nItem: n + 1,
877
+ CST: c.CST,
878
+ cClassTrib: c.cClassTrib,
879
+ ...(c.indDoacao ? { indDoacao: c.indDoacao } : {}),
880
+ ...(c.cCredPres ? { cCredPres: c.cCredPres } : {}),
881
+ ...(c.gTribRegular ? { gTribRegular: c.gTribRegular } : {}),
882
+ ...(vBC ? { vBC } : {}),
883
+ NCM: (m.det.prod as { NCM: string }).NCM,
884
+ CFOP: (m.det.prod as { CFOP: string }).CFOP,
885
+ uTrib: (m.det.prod as { uTrib: string }).uTrib,
886
+ qTrib,
887
+ vProd: m.v.vProd,
888
+ vDesc: m.v.vDesc,
889
+ vFrete: m.v.vFrete,
890
+ vSeg: m.v.vSeg,
891
+ vOutro: m.v.vOutro,
892
+ vICMS: m.icms.vICMS,
893
+ vICMSST: m.icms.vST,
894
+ vFCP: m.icms.vFCP,
895
+ vFCPST: m.icms.vFCPST,
896
+ vIPI: m.v.vIPI,
897
+ vPIS: m.v.vPIS,
898
+ vCOFINS: m.v.vCOFINS,
899
+ vII: m.v.vII,
900
+ vISSQN: m.v.vISSQN,
901
+ vICMSUFDest: m.v.vICMSUFDest,
902
+ vFCPUFDest: m.v.vFCPUFDest,
903
+ };
904
+ });
905
+ const destino =
906
+ entrada.entrega !== undefined
907
+ ? { UF: entrada.entrega.UF, cMun: entrada.entrega.cMun }
908
+ : d?.endereco !== undefined
909
+ ? isExterior(d.endereco)
910
+ ? { UF: 'EX' as const, cMun: '9999999' }
911
+ : { UF: d.endereco.UF, cMun: d.endereco.cMun }
912
+ : undefined;
913
+ const compra = entrada.gCompraGov;
914
+ const resp = await calculadora.calcular({
915
+ nota: {
916
+ fatoGerador,
917
+ emissao: agora,
918
+ ambiente: opcoes.ambiente,
919
+ mod,
920
+ tpNF: entrada.tpNF,
921
+ finNFe: finNFeIn,
922
+ ...(entrada.tpNFDebito ? { tpNFDebito: entrada.tpNFDebito } : {}),
923
+ ...(entrada.tpNFCredito ? { tpNFCredito: entrada.tpNFCredito } : {}),
924
+ indFinal,
925
+ indPres,
926
+ emitente: { UF: emitUf, cMun: e.endereco.cMun, CRT: e.CRT },
927
+ ...(destino ? { destino } : {}),
928
+ ...(entrada.cMunFGIBS ? { cMunFGIBS: entrada.cMunFGIBS } : {}),
929
+ ...(compra && pRedutorGov
930
+ ? {
931
+ compraGov: {
932
+ tpEnteGov: compra.tpEnteGov,
933
+ pRedutor: pRedutorGov,
934
+ tpOperGov: compra.tpOperGov,
935
+ },
936
+ }
937
+ : {}),
938
+ },
939
+ itens: reqs,
940
+ });
941
+ issues.addAll(resp.ocorrencias ?? [], 'montagem');
942
+ for (const { n } of classificados) {
943
+ const g = resp.itens.find((x) => x.nItem === n + 1);
944
+ if (g === undefined) {
945
+ issues.montagem(
946
+ `itens[${n}].impostos.ibsCbs`,
947
+ 'ibscbs_calculo',
948
+ 'a calculadora não devolveu o grupo IBSCBS do item',
949
+ );
950
+ continue;
951
+ }
952
+ ((itens[n] as ItemMontado).det.imposto as Record<string, unknown>).IBSCBS = g.IBSCBS;
953
+ }
954
+ }
955
+
956
+ // Grupos repassados (IBSCBS pronto ou da calculadora, IS) chegam como texto: número malformado vira ocorrência antes
957
+ // de qualquer soma, nunca exceção.
958
+ itens.forEach((m, n) => {
959
+ const imp = m.det.imposto as { IBSCBS?: unknown; IS?: unknown };
960
+ const ib = `itens[${n}].impostos.ibsCbs`;
961
+ const is = `itens[${n}].impostos.is`;
962
+ // O IBSCBS pronto (ibsCbs.grupo) é da entrada; o que a calculadora devolveu é da montagem (ADR 0011).
963
+ const ibsDaCalculadora = entrada.itens[n]?.impostos.ibsCbs?.grupo === undefined;
964
+ const antes = issues.list.length;
965
+ decimaisInvalidos(imp.IBSCBS, ib, issues);
966
+ if (ibsDaCalculadora) issues.reclassificarDesde(antes, 'montagem');
967
+ decimaisInvalidos(imp.IS, is, issues);
968
+ if (issues.list.length === antes) {
969
+ grupoInvalido(TTribNFeCt as ComplexType, 'IBSCBS', imp.IBSCBS, ib, issues);
970
+ if (ibsDaCalculadora) issues.reclassificarDesde(antes, 'montagem');
971
+ grupoInvalido(TISCt as ComplexType, 'IS', imp.IS, is, issues);
972
+ }
973
+ });
974
+ if (!issues.empty) return { ok: false, ocorrencias: issues.classificadas };
975
+
976
+ // Totais (grupo W)
977
+ const gruposIbsCbs = itens
978
+ .map((m) => (m.det.imposto as { IBSCBS?: TTribNFe }).IBSCBS)
979
+ .filter((g): g is TTribNFe => g !== undefined);
980
+ const somaTributosNoItem = diaBrasilia(fatoGerador) >= reforma.vItemSomaTributosDesde;
981
+ let vNFTot = Decimal.ZERO;
982
+ // vItem e vNFTot existem quando a nota tem tributo da reforma: IBS/CBS ou IS (NT 2025.002, VB01 e W60).
983
+ const temReforma = gruposIbsCbs.length > 0 || itens.some((m) => (m.det.imposto as { IS?: unknown }).IS !== undefined);
984
+ if (temReforma) {
985
+ for (const m of itens) {
986
+ let vItem = composicao(m, m.indTot);
987
+ const g = (m.det.imposto as { IBSCBS?: TTribNFe }).IBSCBS;
988
+ if (somaTributosNoItem) {
989
+ // VB01-10: o vIS soma no item por si, com ou sem grupo IBSCBS no mesmo item.
990
+ const is = (m.det.imposto as { IS?: { vIS?: string } }).IS?.vIS;
991
+ if (is !== undefined) vItem = vItem.plus(Decimal.of(is));
992
+ }
993
+ if (g !== undefined && somaTributosNoItem) {
994
+ const t = ibsCbsDoItem(g);
995
+ vItem = vItem.plus(t.vIBS).plus(t.vCBS);
996
+ if (!m.faturamentoDireto) vItem = vItem.plus(t.vTotIBSMonoItem).plus(t.vTotCBSMonoItem);
997
+ }
998
+ (m.det as Record<string, unknown>).vItem = ctx.s(vItem, D1302);
999
+ vNFTot = vNFTot.plus(vItem);
1000
+ }
1001
+ }
1002
+ const soma = (f: (m: ItemMontado) => Decimal, filtro: (m: ItemMontado) => boolean = () => true): Decimal =>
1003
+ itens.filter(filtro).reduce((acc, m) => acc.plus(f(m)), Decimal.ZERO);
1004
+ const icmsItens = (m: ItemMontado): boolean => !m.issqn;
1005
+ const icmsSoma = (k: keyof IcmsTotais): Decimal => soma((m) => m.icms[k] as Decimal);
1006
+ const vProdTot = soma(
1007
+ (m) => m.v.vProd,
1008
+ (m) => m.indTot && !m.issqn,
1009
+ );
1010
+ const vServ = soma(
1011
+ (m) => m.v.vProd,
1012
+ (m) => m.indTot && m.issqn,
1013
+ );
1014
+ const vNF = itens.reduce((acc, m) => acc.plus(composicao(m, m.indTot && !m.issqn)), Decimal.ZERO).plus(vServ);
1015
+ const S = (x: Decimal): string => ctx.s(x, D1302);
1016
+ const mono = itens.some((m) => m.icms.mono);
1017
+ const ufDest = itens.some((m) => m.temUfDest);
1018
+ const ICMSTot = clean({
1019
+ vBC: S(icmsSoma('vBC')),
1020
+ vICMS: S(icmsSoma('vICMS')),
1021
+ vICMSDeson: S(icmsSoma('vICMSDeson')),
1022
+ vFCPUFDest: ufDest ? S(soma((m) => m.v.vFCPUFDest)) : undefined,
1023
+ vICMSUFDest: ufDest ? S(soma((m) => m.v.vICMSUFDest)) : undefined,
1024
+ vICMSUFRemet: ufDest ? S(soma((m) => m.v.vICMSUFRemet)) : undefined,
1025
+ vFCP: S(icmsSoma('vFCP')),
1026
+ vBCST: S(icmsSoma('vBCST')),
1027
+ vST: S(icmsSoma('vST')),
1028
+ vFCPST: S(icmsSoma('vFCPST')),
1029
+ vFCPSTRet: S(icmsSoma('vFCPSTRet')),
1030
+ qBCMono: mono ? S(ctx.round(icmsSoma('qBCMono'), D1302, 'icms')) : undefined,
1031
+ vICMSMono: mono ? S(icmsSoma('vICMSMono')) : undefined,
1032
+ qBCMonoReten: mono ? S(ctx.round(icmsSoma('qBCMonoReten'), D1302, 'icms')) : undefined,
1033
+ vICMSMonoReten: mono ? S(icmsSoma('vICMSMonoReten')) : undefined,
1034
+ qBCMonoRet: mono ? S(ctx.round(icmsSoma('qBCMonoRet'), D1302, 'icms')) : undefined,
1035
+ vICMSMonoRet: mono ? S(icmsSoma('vICMSMonoRet')) : undefined,
1036
+ vProd: S(vProdTot),
1037
+ vFrete: S(soma((m) => m.v.vFrete)),
1038
+ vSeg: S(soma((m) => m.v.vSeg)),
1039
+ vDesc: S(soma((m) => m.v.vDesc)),
1040
+ vII: S(soma((m) => m.v.vII)),
1041
+ vIPI: S(soma((m) => m.v.vIPI)),
1042
+ vIPIDevol: S(soma((m) => m.v.vIPIDevol)),
1043
+ vPIS: S(soma((m) => m.v.vPIS, icmsItens)),
1044
+ vCOFINS: S(soma((m) => m.v.vCOFINS, icmsItens)),
1045
+ vOutro: S(soma((m) => m.v.vOutro)),
1046
+ vNF: S(vNF),
1047
+ vTotTrib: itens.some((m) => m.temTotTrib) ? S(soma((m) => m.v.vTotTrib)) : undefined,
1048
+ }) as TNFe_infNFe_total_ICMSTot;
1049
+ const total: Record<string, unknown> = { ICMSTot };
1050
+ if (itens.some((m) => m.issqn)) {
1051
+ const iss = itens.filter((m) => m.issqn);
1052
+ const so = (x: Decimal): string | undefined => ctx.so(x, D1302_OPC);
1053
+ const opc = (k: string): string | undefined =>
1054
+ so(iss.reduce((a, m) => a.plus(m.issqnOpcionais[k] ?? Decimal.ZERO), Decimal.ZERO));
1055
+ total.ISSQNtot = clean({
1056
+ vServ: so(vServ),
1057
+ vBC: so(
1058
+ soma(
1059
+ (m) => m.v.vBCISS,
1060
+ (m) => m.issqn,
1061
+ ),
1062
+ ),
1063
+ vISS: so(
1064
+ soma(
1065
+ (m) => m.v.vISSQN,
1066
+ (m) => m.issqn,
1067
+ ),
1068
+ ),
1069
+ vPIS: so(
1070
+ soma(
1071
+ (m) => m.v.vPIS,
1072
+ (m) => m.issqn,
1073
+ ),
1074
+ ),
1075
+ vCOFINS: so(
1076
+ soma(
1077
+ (m) => m.v.vCOFINS,
1078
+ (m) => m.issqn,
1079
+ ),
1080
+ ),
1081
+ dCompet: dhEmi.slice(0, 10),
1082
+ vDeducao: opc('vDeducao'),
1083
+ vOutro: opc('vOutro'),
1084
+ vDescIncond: opc('vDescIncond'),
1085
+ vDescCond: opc('vDescCond'),
1086
+ vISSRet: opc('vISSRet'),
1087
+ }) as TNFe_infNFe_total_ISSQNtot;
1088
+ }
1089
+ const comIs = itens.filter((m) => (m.det.imposto as { IS?: unknown }).IS !== undefined);
1090
+ if (comIs.length > 0) {
1091
+ total.ISTot = {
1092
+ vIS: S(
1093
+ comIs.reduce(
1094
+ (a, m) => a.plus(Decimal.of((m.det.imposto as { IS: { vIS?: string } }).IS.vIS ?? '0')),
1095
+ Decimal.ZERO,
1096
+ ),
1097
+ ),
1098
+ };
1099
+ }
1100
+ if (gruposIbsCbs.length > 0) total.IBSCBSTot = totalIbsCbs(gruposIbsCbs);
1101
+ if (temReforma) total.vNFTot = S(vNFTot);
1102
+
1103
+ // Identificação (grupo B)
1104
+ const finNFe = finNFeIn;
1105
+ const ide = clean({
1106
+ cUF,
1107
+ cNF,
1108
+ natOp: entrada.natOp,
1109
+ mod,
1110
+ serie: String(serie),
1111
+ nNF: String(nNF),
1112
+ dhEmi,
1113
+ dhSaiEnt: entrada.dhSaiEnt === undefined ? undefined : formatarDh(entrada.dhSaiEnt, offset),
1114
+ dPrevEntrega: entrada.dPrevEntrega,
1115
+ tpNF: entrada.tpNF,
1116
+ idDest,
1117
+ cMunFG: entrada.cMunFG ?? e.endereco.cMun,
1118
+ cMunFGIBS: entrada.cMunFGIBS,
1119
+ tpImp,
1120
+ tpEmis,
1121
+ cDV,
1122
+ tpAmb,
1123
+ finNFe,
1124
+ tpNFDebito: entrada.tpNFDebito,
1125
+ tpNFCredito: entrada.tpNFCredito,
1126
+ indFinal,
1127
+ indPres,
1128
+ // NT 2020.006: indIntermed obrigatório com indPres 2, 3, 4 e 9.
1129
+ indIntermed: entrada.indIntermed ?? (['2', '3', '4', '9'].includes(indPres) ? '0' : undefined),
1130
+ procEmi: '0',
1131
+ verProc: opcoes.verProc ?? formatarVerProc('sinete', VERSAO_PACOTE),
1132
+ dhCont: cont === undefined ? undefined : formatarDh(cont.dhCont, offset),
1133
+ xJust: cont?.xJust,
1134
+ NFref: entrada.referenciadas?.map((r, n) => referencia(r, `referenciadas[${n}]`, issues)),
1135
+ gCompraGov:
1136
+ entrada.gCompraGov === undefined
1137
+ ? undefined
1138
+ : clean({
1139
+ tpEnteGov: entrada.gCompraGov.tpEnteGov,
1140
+ pRedutor: ctx.s(pRedutorGov ?? Decimal.ZERO, D0302A04),
1141
+ tpOperGov: entrada.gCompraGov.tpOperGov,
1142
+ refDFeAnt: entrada.gCompraGov.refDFeAnt as string[] | undefined,
1143
+ }),
1144
+ gPagAntecipado: entrada.gPagAntecipado === undefined ? undefined : { refNFe: [...entrada.gPagAntecipado] },
1145
+ });
1146
+
1147
+ // Locais, autorizados, transporte, cobrança, pagamento, informações adicionais
1148
+ const local = (l: Local | undefined, path: string): Record<string, unknown> | undefined =>
1149
+ l === undefined
1150
+ ? undefined
1151
+ : clean({
1152
+ ...documento(l, path, issues),
1153
+ xNome: l.xNome,
1154
+ ...endereco(l),
1155
+ cPais: '1058',
1156
+ xPais: 'BRASIL',
1157
+ email: l.email,
1158
+ IE: l.IE,
1159
+ });
1160
+ if ((entrada.autXML?.length ?? 0) > 10) issues.add('autXML', 'campo_invalido', 'no máximo 10 autorizados');
1161
+ const autXML = entrada.autXML?.map((a, n) => documento(a, `autXML[${n}]`, issues));
1162
+
1163
+ const tr = entrada.transporte;
1164
+ const transp: Record<string, unknown> = { modFrete: tr?.modFrete ?? '9' };
1165
+ if (tr?.transportador !== undefined) {
1166
+ const t = tr.transportador;
1167
+ const docT =
1168
+ t.CNPJ !== undefined || t.CPF !== undefined
1169
+ ? documento(t as DocumentoPessoa, 'transporte.transportador', issues)
1170
+ : {};
1171
+ transp.transporta = clean({ ...docT, xNome: t.xNome, IE: t.IE, xEnder: t.xEnder, xMun: t.xMun, UF: t.UF });
1172
+ }
1173
+ if (tr?.retTransp !== undefined) {
1174
+ const r = tr.retTransp;
1175
+ const vBCRet = ctx.req(r.vBCRet, 'transporte.retTransp.vBCRet', D1302);
1176
+ const pICMSRet = ctx.req(r.pICMSRet, 'transporte.retTransp.pICMSRet', D0302A04);
1177
+ const vICMSRet = ctx.calc(r.vICMSRet, vBCRet.percent(pICMSRet), 'transporte.retTransp.vICMSRet', D1302, 'icms');
1178
+ transp.retTransp = {
1179
+ vServ: S(ctx.req(r.vServ, 'transporte.retTransp.vServ', D1302)),
1180
+ vBCRet: S(vBCRet),
1181
+ pICMSRet: ctx.s(pICMSRet, D0302A04),
1182
+ vICMSRet: S(vICMSRet),
1183
+ CFOP: r.CFOP,
1184
+ cMunFG: r.cMunFG,
1185
+ };
1186
+ }
1187
+ if (tr?.veicTransp !== undefined) transp.veicTransp = tr.veicTransp;
1188
+ if (tr?.reboque !== undefined) transp.reboque = tr.reboque;
1189
+ if (tr?.vagao !== undefined) transp.vagao = tr.vagao;
1190
+ if (tr?.balsa !== undefined) transp.balsa = tr.balsa;
1191
+ if (tr?.volumes !== undefined) {
1192
+ transp.vol = tr.volumes.map((vol, n) =>
1193
+ clean({
1194
+ qVol: vol.qVol === undefined ? undefined : String(vol.qVol),
1195
+ esp: vol.esp,
1196
+ marca: vol.marca,
1197
+ nVol: vol.nVol,
1198
+ pesoL: ctx.so(ctx.opt(vol.pesoL, `transporte.volumes[${n}].pesoL`, D1203), D1203),
1199
+ pesoB: ctx.so(ctx.opt(vol.pesoB, `transporte.volumes[${n}].pesoB`, D1203), D1203),
1200
+ lacres: vol.lacres?.map((nLacre) => ({ nLacre })),
1201
+ }),
1202
+ );
1203
+ }
1204
+
1205
+ let cobr: Record<string, unknown> | undefined;
1206
+ if (entrada.cobranca !== undefined) {
1207
+ const c = entrada.cobranca;
1208
+ cobr = {};
1209
+ if (c.fatura !== undefined) {
1210
+ const f = c.fatura;
1211
+ const vOrig = ctx.opt(f.vOrig, 'cobranca.fatura.vOrig', D1302);
1212
+ const vDescF = ctx.opt(f.vDesc, 'cobranca.fatura.vDesc', D1302);
1213
+ const vLiq =
1214
+ vOrig === undefined
1215
+ ? ctx.opt(f.vLiq, 'cobranca.fatura.vLiq', D1302)
1216
+ : ctx.calc(f.vLiq, vOrig.minus(vDescF ?? Decimal.ZERO), 'cobranca.fatura.vLiq', D1302, 'produto');
1217
+ cobr.fat = clean({
1218
+ nFat: f.nFat,
1219
+ vOrig: ctx.so(vOrig, D1302),
1220
+ vDesc: ctx.so(vDescF, D1302),
1221
+ vLiq: ctx.so(vLiq, D1302),
1222
+ });
1223
+ }
1224
+ if (c.duplicatas !== undefined) {
1225
+ if (c.duplicatas.length > 120) issues.add('cobranca.duplicatas', 'campo_invalido', 'no máximo 120 duplicatas');
1226
+ cobr.dup = c.duplicatas.map((dup, n) =>
1227
+ clean({
1228
+ // NT 2016.002: nDup sequencial com 3 dígitos.
1229
+ nDup: dup.nDup ?? String(n + 1).padStart(3, '0'),
1230
+ dVenc: dup.dVenc,
1231
+ vDup: ctx.s(ctx.req(dup.vDup, `cobranca.duplicatas[${n}].vDup`, D1302_OPC), D1302_OPC),
1232
+ }),
1233
+ );
1234
+ }
1235
+ }
1236
+
1237
+ const pagIn = entrada.pagamento ?? { detPag: [{ tPag: '90' as const, vPag: '0' }] };
1238
+ const igualTotal = opcoes.pagamentoIgualTotal === true;
1239
+ if (igualTotal) {
1240
+ const unico = entrada.pagamento?.detPag.length === 1 ? entrada.pagamento.detPag[0] : undefined;
1241
+ if (unico === undefined) {
1242
+ issues.add('pagamento.detPag', 'pagamento_igual_total', 'pagamentoIgualTotal pede exatamente um detPag');
1243
+ } else if (unico.tPag === '90') {
1244
+ issues.add('pagamento.detPag[0].tPag', 'pagamento_igual_total', 'tPag 90 (sem pagamento) não recebe o total');
1245
+ }
1246
+ }
1247
+ const pag = clean({
1248
+ detPag: pagIn.detPag.map((p, n) =>
1249
+ clean({
1250
+ indPag: p.indPag,
1251
+ tPag: p.tPag,
1252
+ xPag: p.xPag,
1253
+ // Com pagamentoIgualTotal, o único pagamento é o total da nota, já calculado acima.
1254
+ vPag: igualTotal ? S(vNF) : S(ctx.req(p.vPag, `pagamento.detPag[${n}].vPag`, D1302)),
1255
+ dPag: p.dPag,
1256
+ CNPJPag: p.CNPJPag,
1257
+ UFPag: p.UFPag,
1258
+ card: p.card,
1259
+ }),
1260
+ ),
1261
+ vTroco: ctx.so(ctx.opt(pagIn.vTroco, 'pagamento.vTroco', D1302), D1302),
1262
+ }) as unknown as PagMontado;
1263
+ // Pagamentos e troco contra o vNF (W16), não o vNFTot: é o total que a YA03-10 (NT 2025.001 v1.03) e a YA09-10 (MOC 7.0
1264
+ // Anexo I) citam, e a NT 2025.002 (até a v1.51) não mudou essas regras nem o W16.
1265
+ if (nfce && entrada.pagamento !== undefined && entrada.pagamento.detPag.length > 0 && issues.empty) {
1266
+ pagamentoNfce(pag, vNF, issues);
1267
+ }
1268
+ // W16-40 (rejeição 750): acima do limite, a NFC-e identifica o destinatário por CNPJ, CPF ou idEstrangeiro.
1269
+ if (nfce && vNF.gt(Decimal.of(NFCE_LIMITE_SEM_DESTINATARIO)) && dest === undefined) {
1270
+ issues.add(
1271
+ 'destinatario',
1272
+ 'campo_obrigatorio',
1273
+ `NFC-e acima de R$ ${NFCE_LIMITE_SEM_DESTINATARIO} identifica o destinatário (W16-40, rejeição 750)`,
1274
+ );
1275
+ }
1276
+ const ia = entrada.informacoesAdicionais;
1277
+ const infAdic =
1278
+ ia === undefined || Object.values(ia).every((x) => x === undefined)
1279
+ ? undefined
1280
+ : clean({
1281
+ infAdFisco: ia.infAdFisco,
1282
+ infCpl: ia.infCpl,
1283
+ obsCont: ia.obsCont,
1284
+ obsFisco: ia.obsFisco,
1285
+ procRef: ia.procRef,
1286
+ });
1287
+
1288
+ // Responsável técnico e CSRT (NT 2018.005), com a exigência por UF como dado
1289
+ const exig = { ...exigenciaRespTec(emitUf, opcoes.ambiente, agora), ...opcoes.exigencias };
1290
+ const rt = entrada.respTec ?? opcoes.respTec;
1291
+ let infRespTec: Record<string, unknown> | undefined;
1292
+ if (rt === undefined) {
1293
+ if (exig.infRespTec === 'obrigatorio') {
1294
+ issues.add('respTec', 'resp_tec_obrigatorio', `a UF ${emitUf} exige o responsável técnico (rejeição 972)`);
1295
+ }
1296
+ } else {
1297
+ const cnpjRt = lerCnpj(rt.CNPJ, { caminho: 'respTec.CNPJ' });
1298
+ if (!cnpjRt.ok) issues.list.push(cnpjRt.erro);
1299
+ infRespTec = {
1300
+ CNPJ: cnpjRt.ok ? cnpjRt.valor : rt.CNPJ,
1301
+ xContato: rt.xContato,
1302
+ email: rt.email,
1303
+ fone: digits(rt.fone),
1304
+ };
1305
+ if (rt.csrt !== undefined) {
1306
+ infRespTec.idCSRT = rt.csrt.idCSRT;
1307
+ infRespTec.hashCSRT = await hashCsrt(rt.csrt.CSRT, chave);
1308
+ } else if (exig.csrt === 'obrigatorio') {
1309
+ issues.add('respTec.csrt', 'csrt_obrigatorio', `a UF ${emitUf} exige idCSRT e hashCSRT (rejeição 975)`);
1310
+ }
1311
+ }
1312
+
1313
+ const inf = clean({
1314
+ versao: '4.00',
1315
+ Id: `NFe${chave}`,
1316
+ ide,
1317
+ emit,
1318
+ dest,
1319
+ retirada: local(entrada.retirada, 'retirada'),
1320
+ entrega: local(entrada.entrega, 'entrega'),
1321
+ autXML,
1322
+ det: itens.map((m) => m.det),
1323
+ total,
1324
+ transp,
1325
+ cobr,
1326
+ pag,
1327
+ infIntermed: entrada.infIntermed,
1328
+ infAdic,
1329
+ exporta: entrada.exporta,
1330
+ compra: entrada.compra,
1331
+ cana: entrada.cana,
1332
+ infRespTec,
1333
+ agropecuario: entrada.agropecuario,
1334
+ }) as unknown as TNFe_infNFe;
1335
+
1336
+ // Regras da SEFAZ que só dependem do documento (ADR 0012).
1337
+ cstComIsento(entrada, idDest, issues);
1338
+ vencimentos(entrada, { dhEmi, instante: agora }, issues);
1339
+
1340
+ camposForaDoPl(pl.infNFe, inf, 'infNFe', pl.vigencia.pl, issues);
1341
+ textosForaDoXml(inf, 'infNFe', issues);
1342
+ if (!issues.empty) return { ok: false, ocorrencias: issues.classificadas };
1343
+
1344
+ // Serialização canônica e validação estrita contra o schema do PL vigente, antes de qualquer assinatura.
1345
+ // O serializer recusa com ErroSerializacao o que não cabe no modelo (escolhas exclusivas informadas juntas, por
1346
+ // exemplo vagao e balsa): vira ocorrência com o caminho do XML, nunca exceção.
1347
+ let xml: string;
1348
+ try {
1349
+ xml = `<NFe xmlns="${NFE_NS}">${serializar(pl.infNFe, 'infNFe', inf, NFE_NS)}</NFe>`;
1350
+ } catch (e) {
1351
+ if (!(e instanceof ErroSerializacao)) throw e;
1352
+ return {
1353
+ ok: false,
1354
+ ocorrencias: [{ caminho: e.caminho, code: 'schema', mensagem: e.message, origem: 'montagem' }],
1355
+ };
1356
+ }
1357
+ const doc = lerXml(xml);
1358
+ const infEl = primeiroFilho(doc.raiz, 'infNFe', NFE_NS);
1359
+ const schemaIssues = infEl === undefined ? [] : validar(pl.infNFe, infEl);
1360
+ if (schemaIssues.length > 0) {
1361
+ return {
1362
+ ok: false,
1363
+ ocorrencias: schemaIssues.map((i) => ({
1364
+ caminho: i.caminho,
1365
+ code: 'schema',
1366
+ mensagem: `${i.code}: ${i.mensagem}`,
1367
+ origem: 'montagem',
1368
+ })),
1369
+ };
1370
+ }
1371
+ let supl: NfceSupl | undefined;
1372
+ if (nfce) {
1373
+ // infNFeSupl (grupo ZX, NT 2025.001 item 04): endereços da UF no dia da emissão, pelo horário de Brasília.
1374
+ const tabela = urlsNfce(emitUf, opcoes.ambiente, diaBrasilia(agora));
1375
+ const urlQr = opcoes.urlQrCode ?? tabela.qrCode;
1376
+ const urlChave = opcoes.urlChave ?? tabela.urlChave;
1377
+ if (urlQr === undefined || !/^https?:\/\//i.test(urlQr)) {
1378
+ issues.montagem(
1379
+ 'urlQrCode',
1380
+ 'qrcode_invalido',
1381
+ `sem endereço completo do QR Code da NFC-e para ${emitUf} em ${opcoes.ambiente}: informe urlQrCode (data/nfce-urls.json)`,
1382
+ );
1383
+ }
1384
+ if (urlChave === undefined) {
1385
+ issues.montagem(
1386
+ 'urlChave',
1387
+ 'qrcode_invalido',
1388
+ `sem URL de consulta por chave da NFC-e para ${emitUf}: informe urlChave`,
1389
+ );
1390
+ }
1391
+ if (urlQr === undefined || urlChave === undefined || !issues.empty)
1392
+ return { ok: false, ocorrencias: issues.classificadas };
1393
+ const p = await parametrosQrCode(
1394
+ {
1395
+ chave,
1396
+ ambiente: opcoes.ambiente,
1397
+ tpEmis,
1398
+ dhEmi,
1399
+ vNF: (inf.total as { ICMSTot: { vNF: string } }).ICMSTot.vNF,
1400
+ ...(dest === undefined ? {} : { dest: dest as { CNPJ?: string; CPF?: string; idEstrangeiro?: string } }),
1401
+ xml,
1402
+ },
1403
+ qr,
1404
+ );
1405
+ supl = { versao: qr.versao, urlChave, base: `${urlQr.replace(/\?+$/, '')}?p=`, ...p };
1406
+ // O infNFeSupl contra o schema antes de assinar (padrões do qrCode por versão, tamanho do urlChave). Na versão 3
1407
+ // off-line, com uma assinatura do tamanho da de uma chave RSA de 2048 bits no lugar da que o certificado ainda vai
1408
+ // fazer; o `comQrCode` confere de novo com a assinatura verdadeira.
1409
+ const suplIssues = conferirSupl(
1410
+ inserirSupl({ xml, nfce: supl } as NfeMontada, supl.assinar ? ASSINATURA_2048 : undefined),
1411
+ );
1412
+ if (suplIssues.length > 0) return { ok: false, ocorrencias: suplIssues };
1413
+ }
1414
+ return {
1415
+ ok: true,
1416
+ valor: {
1417
+ chave,
1418
+ id: `NFe${chave}`,
1419
+ cNF,
1420
+ cDV,
1421
+ mod,
1422
+ tpEmis,
1423
+ dhEmi,
1424
+ ...(supl === undefined ? {} : { nfce: supl }),
1425
+ pl: pl.vigencia,
1426
+ infNFe: inf,
1427
+ xml,
1428
+ },
1429
+ };
1430
+ }
1431
+
1432
+ /** Base64 de uma assinatura RSA de 2048 bits (256 bytes): a forma da assinatura do QR Code off-line na montagem. */
1433
+ const ASSINATURA_2048 = codificarBase64(new Uint8Array(256));
1434
+
1435
+ /** Ocorrências do `infNFeSupl` do XML contra o schema (`TNFe_infNFeSupl`), como de montagem. */
1436
+ function conferirSupl(xml: string): Ocorrencia[] {
1437
+ const el = primeiroFilho(lerXml(xml).raiz, 'infNFeSupl', NFE_NS);
1438
+ const erros = el === undefined ? [] : validar(TNFe_infNFeSupl as ComplexType, el);
1439
+ return erros.map((i) => ({
1440
+ caminho: i.caminho,
1441
+ code: 'schema',
1442
+ mensagem: `${i.code}: ${i.mensagem}`,
1443
+ origem: 'montagem',
1444
+ }));
1445
+ }
1446
+
1447
+ const escapeXml = (s: string): string => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1448
+
1449
+ /**
1450
+ * Assinatura dos parâmetros do QR Code da NFC-e em contingência off-line com a versão 3 (RSA-SHA1 em Base64, com o
1451
+ * certificado que assina a nota; Manual do DANFE NFC-e 6.0, 4.4.2). `undefined` quando o QR Code não leva assinatura
1452
+ * (NF-e, emissão normal, versão 2).
1453
+ */
1454
+ export async function assinaturaQrCode(nota: NfeMontada, assinador: Assinador): Promise<string | undefined> {
1455
+ return nota.nfce?.assinar === true ? assinarParametros(nota.nfce.parametros, assinador) : undefined;
1456
+ }
1457
+
1458
+ /**
1459
+ * A NFC-e montada com o `infNFeSupl` (QR Code e `urlChave`) inserido por splice antes do fechamento de `NFe`, pronta
1460
+ * para a assinatura. Na versão 3 em contingência off-line, informe a `assinatura` (`assinaturaQrCode`); nos demais
1461
+ * casos ela não existe (ZX02-330, rejeição 445; ZX02-334, rejeição 474). Para assinar em três fases (A3, HSM), passe
1462
+ * este texto ao `prepararAssinatura` do `@sinete/core/xml` com o `id` da nota. Na NF-e (modelo 55), devolve o XML como
1463
+ * veio: ela não tem `infNFeSupl` (ZX01-10, rejeição 393).
1464
+ */
1465
+ export function comQrCode(nota: NfeMontada, assinatura?: string): string {
1466
+ const xml = inserirSupl(nota, assinatura);
1467
+ // Com a assinatura verdadeira, o qrCode pode passar do tamanho que a montagem conferiu (chave maior que 2048 bits).
1468
+ if (assinatura !== undefined) {
1469
+ const issues = conferirSupl(xml);
1470
+ if (issues.length > 0) throw new ErroDeValidacao('o QR Code da NFC-e não passou no schema', issues);
1471
+ }
1472
+ return xml;
1473
+ }
1474
+
1475
+ function inserirSupl(built: NfeMontada, assinatura: string | undefined): string {
1476
+ const s = built.nfce;
1477
+ if (s === undefined) {
1478
+ if (assinatura !== undefined) throw new ErroDeConfiguracao('a NF-e (modelo 55) não tem QR Code');
1479
+ return built.xml;
1480
+ }
1481
+ if (s.assinar && assinatura === undefined) {
1482
+ throw new ErroDeConfiguracao(
1483
+ 'NFC-e em contingência off-line com QR Code versão 3 precisa da assinatura (ZX02-334, rejeição 474)',
1484
+ );
1485
+ }
1486
+ if (!s.assinar && assinatura !== undefined) {
1487
+ throw new ErroDeConfiguracao('este QR Code não leva assinatura (ZX02-330, rejeição 445)');
1488
+ }
1489
+ const fim = '</NFe>';
1490
+ if (!built.xml.endsWith(fim)) throw new ErroDeConfiguracao('NFC-e montada fora da forma esperada');
1491
+ const qrCode = `${s.base}${s.parametros}${assinatura === undefined ? '' : `|${assinatura}`}`;
1492
+ const supl = `<infNFeSupl><qrCode>${escapeXml(qrCode)}</qrCode><urlChave>${escapeXml(s.urlChave)}</urlChave></infNFeSupl>`;
1493
+ return built.xml.slice(0, -fim.length) + supl + fim;
1494
+ }
1495
+
1496
+ /**
1497
+ * Assina a NF-e montada: na NFC-e, insere antes o `infNFeSupl` (`comQrCode`, com a assinatura do QR Code quando a
1498
+ * contingência off-line pede); depois, a `Signature` como último filho de `NFe`, tudo por splice, e devolve a string
1499
+ * final. É essa string que vai para a SEFAZ e para o banco; nada depois deve reparseá-la para reescrever.
1500
+ */
1501
+ export async function assinarNfe(nota: NfeMontada, assinador: Assinador): Promise<string> {
1502
+ return assinarXml(comQrCode(nota, await assinaturaQrCode(nota, assinador)), { id: nota.id }, assinador);
1503
+ }