@sinete/nfe 0.3.0 → 0.5.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.
@@ -11,32 +11,18 @@
11
11
  * escrito aqui: a tabela abaixo só diz qual elemento do `infNFe` recebe cada campo da entrada.
12
12
  */
13
13
 
14
- import type { ComplexType, OcorrenciaSchema, Particle, SimpleType } from '@sinete/schemas';
15
- import { conferirTipoSimples, ehComplexType, ehElementParticle, ehWildcard } from '@sinete/schemas';
14
+ import type { CampoDeTexto, ComplexType } from '@sinete/schemas';
15
+ import { conferirTextos } from '@sinete/schemas';
16
16
  import type { Issues } from '../issues.ts';
17
17
  import type { DadosNfe } from '../model.ts';
18
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
- }
19
+ export { textoXmlValido } from '@sinete/schemas';
34
20
 
35
21
  /**
36
22
  * Campo de texto da entrada (com `[]` onde a entrada é uma lista) e o elemento do `infNFe` que o recebe como veio. Só
37
23
  * os campos copiados sem transformação: o que a montagem calcula ou formata é conferido por ela.
38
24
  */
39
- const CAMPOS: readonly (readonly [entrada: string, xml: string])[] = [
25
+ const CAMPOS: readonly CampoDeTexto[] = [
40
26
  ['natOp', 'ide.natOp'],
41
27
  ['emitente.xNome', 'emit.xNome'],
42
28
  ['emitente.xFant', 'emit.xFant'],
@@ -82,117 +68,6 @@ const CAMPOS: readonly (readonly [entrada: string, xml: string])[] = [
82
68
  ['compra.xCont', 'compra.xCont'],
83
69
  ];
84
70
 
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
71
  /**
197
72
  * Confere os textos da entrada: em qualquer campo, o caractere que o XML não representa; nos campos da tabela, o tipo
198
73
  * do elemento no PL (tamanho, espaço nas pontas, caractere fora do conjunto aceito). Tudo sai como `campo_invalido`,
@@ -206,23 +81,6 @@ export function conferirTextosDaEntrada(
206
81
  issues: Issues,
207
82
  substituidos: ReadonlySet<string> = new Set(),
208
83
  ): 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
- }
84
+ const pular = new Set([...issues.list.map((i) => i.caminho), ...substituidos]);
85
+ for (const t of conferirTextos(entrada, infNFe, CAMPOS, pular)) issues.add(t.caminho, 'campo_invalido', t.mensagem);
228
86
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "descricao": "Códigos de status de sucesso e de processamento (MOC 7.0 Anexo I, tabela 4.4.1) e os códigos que o @sinete/nfe trata de forma própria. Rejeições e denegações com mensagem oficial estão no @sinete/rejeicoes; aqui fica só o desfecho de cada código por serviço.",
3
- "fonte": "MOC 7.0 Anexo I, tabela 4.4.1 (códigos de sucesso) e 4.4.2 (denegação 301, 302, 303); NT 2014.002 (Distribuição DF-e, 137 e 138); NT 2011.004 (lote em processamento, 105); MOC 7.0 Anexo I, consulta protocolo J03 a J06 (561, 562 e 613: a numeração da chave consultada tem outra NF-e)",
3
+ "fonte": "MOC 7.0 Anexo I, tabela 4.4.1 (códigos de sucesso) e 4.4.2 (denegação 301, 302, 303); NT 2014.002 (Distribuição DF-e, 137 e 138); NT 2011.004 (lote em processamento, 105); MOC 7.0 Anexo I, consulta protocolo J03 a J06 (561, 562 e 613: a numeração da chave consultada tem outra NF-e); MOC 7.0 Visão Geral, tabela 5-12, regra I07 (563: a mesma faixa já inutilizada, com o nProt anterior)",
4
4
  "atualizadoEm": "2026-09-26",
5
5
  "autorizada": ["100", "150"],
6
6
  "denegada": ["110", "301", "302", "303"],
@@ -11,6 +11,7 @@
11
11
  "loteNaoLocalizado": ["106"],
12
12
  "servicoEmOperacao": ["107"],
13
13
  "inutilizacaoHomologada": ["102"],
14
+ "inutilizacaoJaHomologada": ["563"],
14
15
  "eventoRegistrado": ["135", "136", "155"],
15
16
  "loteEventoProcessado": ["128"],
16
17
  "cadastroEncontrado": ["111", "112"],
package/src/index.ts CHANGED
@@ -107,7 +107,13 @@ export type {
107
107
  Volume,
108
108
  } from './model.ts';
109
109
  export { MotivoDesoneracaoIcms, TipoPagamento } from './model.ts';
110
- export type { CalculadoraIbsCbs, PedidoIbsCbsItem, PedidoIbsCbsNota, RespostaIbsCbs } from './ports.ts';
110
+ export type {
111
+ AliquotaIbsCbsInformada,
112
+ CalculadoraIbsCbs,
113
+ PedidoIbsCbsItem,
114
+ PedidoIbsCbsNota,
115
+ RespostaIbsCbs,
116
+ } from './ports.ts';
111
117
  export { rotuloDoCaminho } from './rotulo.ts';
112
118
  export type { CalculadoraIbsCbsOpcoes, GrupoIbsCbs } from './rtc.ts';
113
119
  export { calculadoraIbsCbs, carregarDatasetEmbarcado, localDaOperacao } from './rtc.ts';
@@ -131,11 +137,13 @@ export type {
131
137
  Espera,
132
138
  EventoRegistrado,
133
139
  Inutilizacao,
140
+ InutilizacaoAnterior,
134
141
  InutilizacaoPedido,
135
142
  ManifestacaoPedido,
136
143
  ManifestacaoTipo,
137
144
  PoliticaRecibo,
138
145
  ProtocoloNfe,
146
+ RecusadoInutilizacao,
139
147
  ResultadoAutorizacao,
140
148
  ResultadoConsulta,
141
149
  ResultadoEvento,
package/src/ports.ts CHANGED
@@ -74,9 +74,28 @@ export interface PedidoIbsCbsNota {
74
74
  readonly compraGov?: { readonly tpEnteGov: string; readonly pRedutor: Decimal; readonly tpOperGov: string };
75
75
  }
76
76
 
77
+ /**
78
+ * Alíquota do IBS/CBS que entrou no cálculo de um item vinda de quem integra (`comAliquotasInformadas`), e não da tabela
79
+ * oficial do pacote.
80
+ */
81
+ export interface AliquotaIbsCbsInformada {
82
+ /** Número do item na nota (1 a 990). */
83
+ readonly nItem: number;
84
+ readonly tributo: 'CBS' | 'IBSUF' | 'IBSMun';
85
+ /** Percentual nominal informado (`'8.7'` = 8,7%). */
86
+ readonly valor: string;
87
+ /** O motivo declarado em `comAliquotasInformadas`. */
88
+ readonly motivo: string;
89
+ }
90
+
77
91
  /** Resultado da calculadora: o grupo `IBSCBS` de cada item pedido, já na forma lexical do leiaute. */
78
92
  export interface RespostaIbsCbs {
79
93
  readonly itens: readonly { readonly nItem: number; readonly IBSCBS: TTribNFe }[];
94
+ /**
95
+ * As alíquotas que não vieram da tabela oficial: a montagem as repassa em `NfeMontada.aliquotasInformadas`. Ausente
96
+ * ou vazio quando todas foram oficiais; a calculadora de fora do sinete que não sabe dizer pode omitir.
97
+ */
98
+ readonly aliquotasInformadas?: readonly AliquotaIbsCbsInformada[];
80
99
  /**
81
100
  * Problemas de classificação ou de dado (`caminho` relativo ao item, como `itens[2].impostos.ibsCbs`). A ocorrência sem
82
101
  * `origem` entra como `montagem` (ADR 0011); marque `entrada` a que aponta um valor da nota.
package/src/rtc.ts CHANGED
@@ -28,7 +28,13 @@ import { calcular, ErroClassificacao, ErroRegimeNaoSuportado } from '@sinete/ibs
28
28
  import type { Regra } from '@sinete/ibs-cbs/validar';
29
29
  import { documentoDoRoc, validar } from '@sinete/ibs-cbs/validar';
30
30
  import type { DatasetIbsCbs } from '@sinete/ibs-cbs-dados';
31
- import type { CalculadoraIbsCbs, PedidoIbsCbsItem, PedidoIbsCbsNota, RespostaIbsCbs } from './ports.ts';
31
+ import type {
32
+ AliquotaIbsCbsInformada,
33
+ CalculadoraIbsCbs,
34
+ PedidoIbsCbsItem,
35
+ PedidoIbsCbsNota,
36
+ RespostaIbsCbs,
37
+ } from './ports.ts';
32
38
 
33
39
  /** Grupo `IBSCBS` do item, como o `@sinete/nfe` o recebe. */
34
40
  export type GrupoIbsCbs = RespostaIbsCbs['itens'][number]['IBSCBS'];
@@ -109,15 +115,42 @@ function grupoDoLeiaute(g: IBSCBS, indDoacao: '1' | undefined): GrupoIbsCbs {
109
115
  } as GrupoIbsCbs;
110
116
  }
111
117
 
112
- /** Erro do motor ou das alíquotas como ocorrência no item (ou na nota, quando o erro não diz o item). */
113
- function ocorrenciaDoMotor(e: unknown, itens: readonly PedidoIbsCbsItem[]): Ocorrencia | undefined {
118
+ /**
119
+ * Erro do motor ou das alíquotas como uma ocorrência no caminho do item (o primeiro, quando o erro não diz o item).
120
+ * `semAliquota` acha o item que pediu a alíquota desconhecida: o erro do provedor diz o tributo e a data, não o item.
121
+ */
122
+ function ocorrenciaDoMotor(
123
+ e: unknown,
124
+ itens: readonly PedidoIbsCbsItem[],
125
+ semAliquota: () => number | undefined,
126
+ ): Ocorrencia | undefined {
114
127
  if (e instanceof ErroClassificacao || e instanceof ErroRegimeNaoSuportado) {
115
128
  const path = e.item !== undefined ? caminho(e.item) : caminho(itens[0]?.nItem ?? 1);
116
129
  return { caminho: path, code: e.code, mensagem: e.message, origem: 'entrada' };
117
130
  }
118
131
  // Alíquota que o sinete não conhece para a data: falta de dado do pacote, não da nota.
119
- if (e instanceof ErroAliquotaDesconhecida)
120
- return { caminho: 'impostos.ibsCbs', code: e.code, mensagem: e.message, origem: 'montagem' };
132
+ if (e instanceof ErroAliquotaDesconhecida) {
133
+ const n = semAliquota() ?? itens[0]?.nItem ?? 1;
134
+ return { caminho: caminho(n), code: e.code, mensagem: e.message, origem: 'montagem' };
135
+ }
136
+ return undefined;
137
+ }
138
+
139
+ /**
140
+ * O primeiro item (pela ordem de `nItem`, a do motor) cujo cálculo sozinho pede uma alíquota desconhecida. Só roda na
141
+ * recusa, para pôr a ocorrência no item em vez de na nota.
142
+ */
143
+ function primeiroItemSemAliquota(
144
+ op: OperacaoClassificada,
145
+ calc: (op: OperacaoClassificada) => unknown,
146
+ ): number | undefined {
147
+ for (const it of [...op.itens].sort((a, b) => a.n - b.n)) {
148
+ try {
149
+ calc({ ...op, itens: [it] });
150
+ } catch (e) {
151
+ if (e instanceof ErroAliquotaDesconhecida) return it.n;
152
+ }
153
+ }
121
154
  return undefined;
122
155
  }
123
156
 
@@ -234,16 +267,18 @@ function calcularCom(
234
267
  itens: classificados,
235
268
  };
236
269
  const time = contextoDeTempo({ emissao: relogioFixo(nota.emissao), fatoGerador: relogioFixo(nota.fatoGerador) });
237
- let roc: Roc;
238
- try {
239
- roc = calcular(op, {
270
+ const calc = (o: OperacaoClassificada): Roc =>
271
+ calcular(o, {
240
272
  dataset,
241
273
  aliquotas: rates,
242
274
  tempo: time,
243
275
  ...(options.deslocamentoMin === undefined ? {} : { deslocamentoMin: options.deslocamentoMin }),
244
276
  });
277
+ let roc: Roc;
278
+ try {
279
+ roc = calc(op);
245
280
  } catch (e) {
246
- const issue = ocorrenciaDoMotor(e, itens);
281
+ const issue = ocorrenciaDoMotor(e, itens, () => primeiroItemSemAliquota(op, calc));
247
282
  if (issue === undefined) throw e;
248
283
  return { itens: [], ocorrencias: [issue] };
249
284
  }
@@ -295,11 +330,17 @@ function calcularCom(
295
330
  }
296
331
 
297
332
  const porItem = new Map(itens.map((it) => [it.nItem, it]));
333
+ const informadas: AliquotaIbsCbsInformada[] = roc.itens.flatMap((r) =>
334
+ r.aliquotas
335
+ .filter((a) => a.situacao === 'informada')
336
+ .map((a) => ({ nItem: r.nItem, tributo: a.tributo, valor: a.valor, motivo: a.motivo ?? '' })),
337
+ );
298
338
  return {
299
339
  itens: roc.itens.map((r) => ({
300
340
  nItem: r.nItem,
301
341
  IBSCBS: grupoDoLeiaute(r.IBSCBS, porItem.get(r.nItem)?.indDoacao),
302
342
  })),
343
+ ...(informadas.length === 0 ? {} : { aliquotasInformadas: informadas }),
303
344
  ...(issues.length === 0 ? {} : { ocorrencias: issues }),
304
345
  };
305
346
  }
@@ -13,7 +13,7 @@
13
13
  * número. Consulte a chave (`resolverEnvioSemResposta`) e, se ela não constar, reenvie exatamente os mesmos bytes.
14
14
  */
15
15
 
16
- import type { Ambiente, Assinador, CUf, Logger, Relogio, ResultadoSefaz, Uf } from '@sinete/core';
16
+ import type { Ambiente, Assinador, CUf, Logger, Recusado, Relogio, ResultadoSefaz, Uf } from '@sinete/core';
17
17
  import {
18
18
  criarAutorizado,
19
19
  criarDenegado,
@@ -112,6 +112,16 @@ export interface ClienteNfeOpcoes {
112
112
  readonly endpointNfce?: (servico: NfeServico, uf: Uf) => EndpointResolvido;
113
113
  /** Gerador de `idLote` (até 15 dígitos); padrão: os milissegundos do relógio. */
114
114
  readonly idLote?: () => string;
115
+ /**
116
+ * O titular emite NF-e sem inscrição estadual, como contribuinte exclusivo do IBS/CBS (NT 2026.007 v1.10). A
117
+ * autorização dessa NF-e é só na SVRS (RV C17-11, rejeição 166), e os eventos de autoria do emitente dela também
118
+ * (RV 1P10-40, rejeição 188), menos na série 890 a 919. Com a NF-e assinada na mão (`autorizar`, `consultar` e
119
+ * `consultarRecibo` com ela) o cliente vê a falta de `emit/IE` sozinho; esta opção é para o que parte só da chave:
120
+ * cancelamento, carta de correção, consulta e recibo sem a nota (o recibo sem a nota vai à SVRS no modelo 55). A série 890 a
121
+ * 919 é exceção só dos eventos. Não vale para a NFC-e, que não admite emitente sem
122
+ * IE (RV C17-42, rejeição 156).
123
+ */
124
+ readonly contribuinteExclusivoIbsCbs?: boolean;
115
125
  }
116
126
 
117
127
  /** Opções de toda chamada que vai à rede. */
@@ -213,7 +223,24 @@ export interface Inutilizacao {
213
223
  readonly procInutNFe: string;
214
224
  }
215
225
 
216
- export type ResultadoInutilizacao = ResultadoSefaz<Inutilizacao, never>;
226
+ /**
227
+ * Protocolo da inutilização que já valia para a mesma faixa, trazido pelo 563 (MOC 7.0 Visão Geral, tabela 5-12,
228
+ * regra I07). Não existe consulta de inutilização na NF-e 4.00: depois de um pedido sem resposta, reenviar a mesma
229
+ * faixa e ler este campo é o único caminho para guardar o `nProt` que valeu. O `procInutNFe` não se monta com ele,
230
+ * porque a resposta 563 não é a homologação.
231
+ */
232
+ export interface InutilizacaoAnterior {
233
+ readonly nProt: string;
234
+ /** `retInutNFe` do 563 como veio na resposta. */
235
+ readonly retInutNFe: string;
236
+ }
237
+
238
+ /** Recusa da inutilização; no 563 com protocolo, `anterior` traz o protocolo da faixa já inutilizada. */
239
+ export interface RecusadoInutilizacao extends Recusado {
240
+ readonly anterior?: InutilizacaoAnterior;
241
+ }
242
+
243
+ export type ResultadoInutilizacao = Exclude<ResultadoSefaz<Inutilizacao, never>, Recusado> | RecusadoInutilizacao;
217
244
 
218
245
  export interface Cadastro {
219
246
  readonly UF: string;
@@ -430,6 +457,13 @@ function autorDoEmitente(a: AutorDocumento | undefined, c: ChaveAcesso): { CNPJ:
430
457
  */
431
458
  const SVC_DO_TPEMIS: Readonly<Record<string, 'SVC-AN' | 'SVC-RS'>> = { '6': 'SVC-AN', '7': 'SVC-RS' };
432
459
 
460
+ /** A NF-e tem o grupo `emit` e ele não traz `IE`: contribuinte exclusivo do IBS/CBS (NT 2026.007). */
461
+ function emitenteSemIe(a: DocumentoAssinado): boolean {
462
+ const inf = primeiroFilho(a.documento.raiz, 'infNFe', NFE_NS);
463
+ const emit = inf === undefined ? undefined : primeiroFilho(inf, 'emit', NFE_NS);
464
+ return emit !== undefined && primeiroFilho(emit, 'IE', NFE_NS) === undefined;
465
+ }
466
+
433
467
  /** Chave do documento assinado, lida para rotear: o emitente é conferido pela SEFAZ, não aqui. */
434
468
  function chaveDoDocumento(a: DocumentoAssinado): ChaveAcesso {
435
469
  const r = lerChaveAcesso(a.id.slice(3), { caminho: 'infNFe.Id', conferirEmitente: false });
@@ -592,8 +626,18 @@ export function criarClienteNfe(opcoesDoCliente: ClienteNfeOpcoes): ClienteNfe {
592
626
  * autorizou a nota. `naUf` força o autorizador da UF (a CC-e não existe no SVC). A NFC-e vai sempre ao autorizador
593
627
  * normal da NFC-e.
594
628
  */
595
- const endpointDaChave = (servico: NfeServico, c: ChaveAcesso, naUf = false): EndpointResolvido => {
629
+ const endpointDaChave = (
630
+ servico: NfeServico,
631
+ c: ChaveAcesso,
632
+ naUf = false,
633
+ semIe = opcoesDoCliente.contribuinteExclusivoIbsCbs === true,
634
+ ): EndpointResolvido => {
596
635
  if (c.mod === '65') return endpointNfce(servico, c.uf);
636
+ // Contribuinte exclusivo do IBS/CBS (NT 2026.007): autorização, consulta e eventos do emitente só na SVRS (RV
637
+ // C17-11 e 1P10-40). A série 890 a 919 é exceção só dos eventos (exceção 1 da RV 1P10-40).
638
+ const serie = Number(c.serie);
639
+ if (semIe && !(servico === 'RecepcaoEvento' && serie >= 890 && serie <= 919))
640
+ return nfeEndpoint({ ambiente: opcoesDoCliente.ambiente, servico, autorizador: 'SVRS' });
597
641
  const svc = naUf || c.tpEmis === undefined ? undefined : SVC_DO_TPEMIS[c.tpEmis];
598
642
  return nfeEndpoint({
599
643
  ambiente: opcoesDoCliente.ambiente,
@@ -602,6 +646,10 @@ export function criarClienteNfe(opcoesDoCliente: ClienteNfeOpcoes): ClienteNfe {
602
646
  });
603
647
  };
604
648
 
649
+ /** A NF-e assinada diz se o emitente tem IE; sem ela, vale a opção `contribuinteExclusivoIbsCbs`. */
650
+ const semIeOuOpcao = (a: DocumentoAssinado | undefined): boolean =>
651
+ a === undefined ? opcoesDoCliente.contribuinteExclusivoIbsCbs === true : emitenteSemIe(a);
652
+
605
653
  const call = (
606
654
  ep: EndpointResolvido,
607
655
  servico: NfeServico,
@@ -640,7 +688,11 @@ export function criarClienteNfe(opcoesDoCliente: ClienteNfeOpcoes): ClienteNfe {
640
688
  const msg = envelope('consReciNFe', '4.00', [`<tpAmb>${tpAmb}</tpAmb><nRec>${nRec}</nRec>`]);
641
689
  // O recibo é do autorizador que recebeu o lote: com a NF-e, o da chave; sem ela, o das opções.
642
690
  const ep =
643
- c === undefined ? endpoint('NFeRetAutorizacao', opcoes.mod ?? '55') : endpointDaChave('NFeRetAutorizacao', c);
691
+ c === undefined
692
+ ? (opcoes.mod ?? '55') === '55' && opcoesDoCliente.contribuinteExclusivoIbsCbs === true
693
+ ? nfeEndpoint({ ambiente: opcoesDoCliente.ambiente, servico: 'NFeRetAutorizacao', autorizador: 'SVRS' })
694
+ : endpoint('NFeRetAutorizacao', opcoes.mod ?? '55')
695
+ : endpointDaChave('NFeRetAutorizacao', c, false, semIeOuOpcao(a));
644
696
  const r = await call(ep, 'NFeRetAutorizacao', msg, 'retConsReciNFe', opcoes.signal);
645
697
  const v = decodificar(TRetConsReciNFe, r.ret, r.doc.texto).valor;
646
698
  if (v.nRec !== undefined && v.nRec !== nRec) {
@@ -663,7 +715,7 @@ export function criarClienteNfe(opcoesDoCliente: ClienteNfeOpcoes): ClienteNfe {
663
715
  const a = nfeAssinada === undefined ? undefined : documentoAssinado(nfeAssinada, 'NFe', 'infNFe');
664
716
  if (a && a.id !== `NFe${c.chave}`) throw new ErroDeConfiguracao('a NF-e assinada não é a da chave consultada');
665
717
  const msg = serializarRaiz(consSitNFeElement, { versao: '4.00', tpAmb, xServ: 'CONSULTAR', chNFe: c.chave });
666
- const ep = endpointDaChave('NfeConsultaProtocolo', c);
718
+ const ep = endpointDaChave('NfeConsultaProtocolo', c, false, semIeOuOpcao(a));
667
719
  const r = await call(ep, 'NfeConsultaProtocolo', msg, 'retConsSitNFe', opcoes?.signal);
668
720
  const v = decodificar(TRetConsSitNFe, r.ret, r.doc.texto).valor;
669
721
  const status = { cStat: v.cStat, xMotivo: v.xMotivo };
@@ -797,7 +849,7 @@ export function criarClienteNfe(opcoesDoCliente: ClienteNfeOpcoes): ClienteNfe {
797
849
  a.xml,
798
850
  ]);
799
851
  // O autorizador é o do documento: a UF do cUF e, assinada em SVC (tpEmis 6 ou 7), o SVC da chave.
800
- const ep = endpointDaChave('NFeAutorizacao', chaveDoDocumento(a));
852
+ const ep = endpointDaChave('NFeAutorizacao', chaveDoDocumento(a), false, semIeOuOpcao(a));
801
853
  const r = await call(ep, 'NFeAutorizacao', msg, 'retEnviNFe', opcoes.signal);
802
854
  const v = decodificar(TRetEnviNFe, r.ret, r.doc.texto).valor;
803
855
  const status = { cStat: v.cStat, xMotivo: v.xMotivo };
@@ -1015,8 +1067,11 @@ export function criarClienteNfe(opcoesDoCliente: ClienteNfeOpcoes): ClienteNfe {
1015
1067
  const v = decodificar(TRetInutNFe, r.ret, r.doc.texto).valor;
1016
1068
  const status = { cStat: v.infInut.cStat, xMotivo: v.infInut.xMotivo };
1017
1069
  logger.info('nfe.inutilizacao', { id, cStat: status.cStat });
1018
- if (!cstatEm(status.cStat, 'inutilizacaoHomologada')) return rejeitado(status);
1019
- // A homologação tem de ser desta faixa: os campos que o retorno trouxer, iguais aos do pedido.
1070
+ const homologada = cstatEm(status.cStat, 'inutilizacaoHomologada');
1071
+ // `<nProt></nProt>` vazio não é protocolo: sem ele, o 563 fica como a recusa simples.
1072
+ const anterior = cstatEm(status.cStat, 'inutilizacaoJaHomologada') ? v.infInut.nProt || undefined : undefined;
1073
+ if (!homologada && anterior === undefined) return rejeitado(status);
1074
+ // A homologação (ou a anterior, no 563) tem de ser desta faixa: os campos que o retorno trouxer, iguais aos do pedido.
1020
1075
  const r0 = v.infInut;
1021
1076
  const n = (x: string | undefined): string | undefined => (x === undefined ? undefined : String(Number(x)));
1022
1077
  const confere: readonly (readonly [string, string | undefined, string])[] = [
@@ -1033,6 +1088,9 @@ export function criarClienteNfe(opcoesDoCliente: ClienteNfeOpcoes): ClienteNfe {
1033
1088
  detalhes: Object.fromEntries(fora.map(([k, v]) => [k, v])),
1034
1089
  });
1035
1090
  }
1091
+ if (anterior !== undefined) {
1092
+ return { ...rejeitado(status), anterior: { nProt: anterior, retInutNFe: avulso(r.doc, r.ret) } };
1093
+ }
1036
1094
  return criarAutorizado(status, {
1037
1095
  dhRecbto: v.infInut.dhRecbto,
1038
1096
  retInutNFe: avulso(r.doc, r.ret),
@@ -19,6 +19,7 @@ export type CStatClasse =
19
19
  | 'loteNaoLocalizado'
20
20
  | 'servicoEmOperacao'
21
21
  | 'inutilizacaoHomologada'
22
+ | 'inutilizacaoJaHomologada'
22
23
  | 'eventoRegistrado'
23
24
  | 'loteEventoProcessado'
24
25
  | 'cadastroEncontrado'
@@ -1,3 +1,3 @@
1
1
  // GERADO por scripts/versao-gerada.ts a partir do package.json. Não edite: rode `bun scripts/versao-gerada.ts`.
2
2
  /** Versão de `@sinete/nfe`, para o padrão de `verProc`/`verAplic` (`formatarVerProc` do @sinete/core). */
3
- export const VERSAO_PACOTE = '0.3.0';
3
+ export const VERSAO_PACOTE = '0.5.0';