@sinete/nfe 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +147 -1
- package/dist/build/build.d.ts +127 -0
- package/dist/build/build.d.ts.map +1 -0
- package/dist/build/destinatario.d.ts +19 -0
- package/dist/build/destinatario.d.ts.map +1 -0
- package/dist/build/ibscbs.d.ts +27 -0
- package/dist/build/ibscbs.d.ts.map +1 -0
- package/dist/build/icms.d.ts +42 -0
- package/dist/build/icms.d.ts.map +1 -0
- package/dist/build/nfce.d.ts +122 -0
- package/dist/build/nfce.d.ts.map +1 -0
- package/dist/build/pl.d.ts +19 -0
- package/dist/build/pl.d.ts.map +1 -0
- package/dist/build/rejeicoes.d.ts +52 -0
- package/dist/build/rejeicoes.d.ts.map +1 -0
- package/dist/build/tributos.d.ts +45 -0
- package/dist/build/tributos.d.ts.map +1 -0
- package/dist/build/values.d.ts +48 -0
- package/dist/build/values.d.ts.map +1 -0
- package/dist/decimal.d.ts +66 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/format.d.ts +48 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/ibs-cbs.d.ts +14 -0
- package/dist/ibs-cbs.d.ts.map +1 -0
- package/dist/ibs-cbs.js +43 -0
- package/dist/ibs-cbs.js.map +10 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4370 -0
- package/dist/index.js.map +33 -0
- package/dist/issues.d.ts +24 -0
- package/dist/issues.d.ts.map +1 -0
- package/dist/model.d.ts +861 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/ports.d.ts +109 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/rotulo.d.ts +12 -0
- package/dist/rotulo.d.ts.map +1 -0
- package/dist/rtc.d.ts +65 -0
- package/dist/rtc.d.ts.map +1 -0
- package/dist/services/client.d.ts +273 -0
- package/dist/services/client.d.ts.map +1 -0
- package/dist/services/gzip.d.ts +7 -0
- package/dist/services/gzip.d.ts.map +1 -0
- package/dist/services/index.d.ts +14 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/outcome.d.ts +15 -0
- package/dist/services/outcome.d.ts.map +1 -0
- package/dist/services/proc.d.ts +41 -0
- package/dist/services/proc.d.ts.map +1 -0
- package/dist/services/recuperar.d.ts +28 -0
- package/dist/services/recuperar.d.ts.map +1 -0
- package/dist/services/resolver.d.ts +88 -0
- package/dist/services/resolver.d.ts.map +1 -0
- package/dist/services/soap.d.ts +43 -0
- package/dist/services/soap.d.ts.map +1 -0
- package/dist/time.d.ts +12 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/versao-gerada.d.ts +3 -0
- package/dist/versao-gerada.d.ts.map +1 -0
- package/package.json +58 -3
- package/src/build/build.ts +1486 -0
- package/src/build/destinatario.ts +212 -0
- package/src/build/ibscbs.ts +131 -0
- package/src/build/icms.ts +645 -0
- package/src/build/nfce.ts +425 -0
- package/src/build/pl.ts +73 -0
- package/src/build/rejeicoes.ts +142 -0
- package/src/build/tributos.ts +217 -0
- package/src/build/values.ts +126 -0
- package/src/data/arredondamento.json +30 -0
- package/src/data/cstat.json +36 -0
- package/src/data/fusos.json +17 -0
- package/src/data/nfce-urls.json +170 -0
- package/src/data/produtor-rural.json +17 -0
- package/src/data/reforma.json +6 -0
- package/src/data/resp-tec.json +13 -0
- package/src/data/servicos.json +44 -0
- package/src/data/suframa.json +7 -0
- package/src/decimal.ts +231 -0
- package/src/format.ts +64 -0
- package/src/ibs-cbs.ts +89 -0
- package/src/index.ts +153 -0
- package/src/issues.ts +80 -0
- package/src/model.ts +1033 -0
- package/src/ports.ts +96 -0
- package/src/rotulo.ts +137 -0
- package/src/rtc.ts +305 -0
- package/src/services/client.ts +1085 -0
- package/src/services/gzip.ts +26 -0
- package/src/services/index.ts +43 -0
- package/src/services/outcome.ts +50 -0
- package/src/services/proc.ts +164 -0
- package/src/services/recuperar.ts +91 -0
- package/src/services/resolver.ts +127 -0
- package/src/services/soap.ts +133 -0
- package/src/time.ts +23 -0
- package/src/versao-gerada.ts +3 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NFC-e (modelo 65): as regras de montagem que valem só para ela e o `infNFeSupl` (QR Code e URL de consulta por
|
|
3
|
+
* chave). As regras vêm do MOC 7.0 Anexo I (regras de validação com modelo 65 e aplicação obrigatória), da NT 2023.002
|
|
4
|
+
* (emitente pessoa física, v1.01), da NT 2025.001 v1.03 (QR Code versão 3 e pagamentos) e da NT 2025.002 v1.51 (reforma
|
|
5
|
+
* tributária); o leiaute do QR Code, do Manual de Padrões Técnicos do DANFE NFC-e e QR Code, versão 6.0 (março de
|
|
6
|
+
* 2025), itens 4.3 e 4.4. Regra opcional por UF (aplicação "Facult." sem mudança posterior) não entra aqui: a SEFAZ da
|
|
7
|
+
* UF decide, e recusar localmente bloquearia a venda onde a regra não vale.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Ambiente, Signer, Uf } from '@sinete/core';
|
|
11
|
+
import { tpAmbOf } from '@sinete/core';
|
|
12
|
+
import { base64Encode, c14n, firstChild, parseXml, SHA1_DIGEST_INFO_PREFIX } from '@sinete/core/xml';
|
|
13
|
+
import { parseCnpj } from '@sinete/validators';
|
|
14
|
+
import urls from '../data/nfce-urls.json' with { type: 'json' };
|
|
15
|
+
import { Decimal } from '../decimal.ts';
|
|
16
|
+
import type { Issues } from '../issues.ts';
|
|
17
|
+
import type { NfeInput } from '../model.ts';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Versão do QR Code da NFC-e. A 3 (padrão) dispensa o CSC: na emissão normal leva só chave, versão e ambiente, e na
|
|
21
|
+
* contingência off-line leva a assinatura dos parâmetros com o certificado da nota (NT 2025.001, item 02.1). A 2 usa
|
|
22
|
+
* o CSC e o identificador dele (`idCSC`), fornecidos pela SEFAZ da UF; o emitente pessoa física não pode usá-la
|
|
23
|
+
* (ZX02-222, rejeição 444).
|
|
24
|
+
*/
|
|
25
|
+
export type QrCodeNfceOpcoes =
|
|
26
|
+
| { readonly versao: '3' }
|
|
27
|
+
| {
|
|
28
|
+
readonly versao: '2';
|
|
29
|
+
/** Identificador do CSC na SEFAZ (até 6 dígitos); os zeros à esquerda saem do QR Code (Manual 6.0, 4.3.1). */
|
|
30
|
+
readonly idCSC: string;
|
|
31
|
+
/** O CSC (16 a 36 caracteres, Manual 6.0, 4.6). Nunca vai para o XML: só entra no hash. */
|
|
32
|
+
readonly CSC: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** O que a montagem deixa pronto para o `infNFeSupl` da NFC-e. */
|
|
36
|
+
export interface NfceSupl {
|
|
37
|
+
readonly versao: '2' | '3';
|
|
38
|
+
/** URL de consulta por chave de acesso (`urlChave`), impressa no DANFC-e. */
|
|
39
|
+
readonly urlChave: string;
|
|
40
|
+
/** Endereço do QR Code até o `?p=`, inclusive. */
|
|
41
|
+
readonly base: string;
|
|
42
|
+
/** Parâmetros já separados por `|`: completos, ou de 1 a 7 quando falta a assinatura (versão 3 off-line). */
|
|
43
|
+
readonly parametros: string;
|
|
44
|
+
/** Versão 3 em contingência off-line: os parâmetros são assinados com o certificado da nota (Manual 6.0, 4.4.2). */
|
|
45
|
+
readonly assinar: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Entrada = { readonly url: string | null; readonly desde?: string; readonly nota?: string };
|
|
49
|
+
type TabelaUrls = Readonly<Record<Ambiente, Readonly<Record<string, readonly Entrada[]>>>>;
|
|
50
|
+
|
|
51
|
+
/** O endereço vigente no dia (`AAAA-MM-DD`, horário de Brasília): o de início mais recente que não passou do dia. */
|
|
52
|
+
function vigente(lista: readonly Entrada[] | undefined, dia: string): Entrada | undefined {
|
|
53
|
+
let achado: Entrada | undefined;
|
|
54
|
+
for (const e of lista ?? []) {
|
|
55
|
+
if ((e.desde ?? '') <= dia && (achado === undefined || (e.desde ?? '') >= (achado.desde ?? ''))) achado = e;
|
|
56
|
+
}
|
|
57
|
+
return achado;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Endereços da NFC-e da UF no ambiente e no dia (`data/nfce-urls.json`, das tabelas do Portal Nacional da NFC-e). O
|
|
62
|
+
* `qrCode` é `undefined` onde a tabela não traz o endereço completo (AM e MA publicam sem o protocolo): informe
|
|
63
|
+
* `BuildNfeOptions.urlQrCode`.
|
|
64
|
+
*/
|
|
65
|
+
export function urlsNfce(
|
|
66
|
+
uf: Uf,
|
|
67
|
+
ambiente: Ambiente,
|
|
68
|
+
dia: string,
|
|
69
|
+
): { readonly qrCode: string | undefined; readonly urlChave: string | undefined } {
|
|
70
|
+
const q = vigente((urls.qrCode as TabelaUrls)[ambiente][uf], dia);
|
|
71
|
+
const c = vigente((urls.urlChave as TabelaUrls)[ambiente][uf], dia);
|
|
72
|
+
return { qrCode: q?.url ?? undefined, urlChave: c?.url ?? undefined };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const te = new TextEncoder();
|
|
76
|
+
const NFE_NS = 'http://www.portalfiscal.inf.br/nfe';
|
|
77
|
+
|
|
78
|
+
async function sha1(bytes: Uint8Array): Promise<Uint8Array> {
|
|
79
|
+
return new Uint8Array(await globalThis.crypto.subtle.digest('SHA-1', bytes as Uint8Array<ArrayBuffer>));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hex = (b: Uint8Array): string => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `digVal` do QR Code versão 2 off-line: o DigestValue do `infNFe` (SHA-1 do C14N, em Base64, o mesmo da assinatura)
|
|
86
|
+
* convertido para hexadecimal caractere a caractere (Manual 6.0, 4.3.2 e o exemplo de 4.3.6.2: `yzGY...` vira
|
|
87
|
+
* `797a4759...`).
|
|
88
|
+
*/
|
|
89
|
+
async function digValHex(xml: string): Promise<string> {
|
|
90
|
+
const doc = parseXml(xml);
|
|
91
|
+
const inf = firstChild(doc.root, 'infNFe', NFE_NS);
|
|
92
|
+
if (inf === undefined) throw new Error('NF-e montada sem infNFe');
|
|
93
|
+
return hexDoDigestValue(base64Encode(await sha1(te.encode(c14n(inf)))));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** O DigestValue em Base64 convertido para hexadecimal, caractere a caractere (Manual 6.0, 4.3.5, passo 1). */
|
|
97
|
+
export function hexDoDigestValue(digestValue: string): string {
|
|
98
|
+
return hex(te.encode(digestValue));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Hash do QR Code versão 2: SHA-1 dos parâmetros concatenados ao CSC, em hexadecimal (Manual 6.0, 4.3.4 e 4.3.5). */
|
|
102
|
+
export async function hashQrCodeV2(parametros: string, csc: string): Promise<string> {
|
|
103
|
+
return hex(await sha1(te.encode(parametros + csc))).toUpperCase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DadosQrCode {
|
|
107
|
+
readonly chave: string;
|
|
108
|
+
readonly ambiente: Ambiente;
|
|
109
|
+
readonly tpEmis: string;
|
|
110
|
+
/** `dhEmi` do documento. */
|
|
111
|
+
readonly dhEmi: string;
|
|
112
|
+
/** `vNF` como vai no XML (ponto decimal, sem milhar). */
|
|
113
|
+
readonly vNF: string;
|
|
114
|
+
readonly dest?: { readonly CNPJ?: string; readonly CPF?: string; readonly idEstrangeiro?: string };
|
|
115
|
+
/** O XML montado (`<NFe>` sem assinatura): o `digVal` da versão 2 off-line sai dele. */
|
|
116
|
+
readonly xml: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parâmetros do QR Code (Manual 6.0): versão 2 on-line `chave|2|tpAmb|idCSC|hash` e off-line
|
|
121
|
+
* `chave|2|tpAmb|dia|vNF|digVal|idCSC|hash`, com o hash SHA-1 em hexadecimal dos parâmetros concatenados ao CSC
|
|
122
|
+
* (4.3.4 e 4.3.5); versão 3 on-line `chave|3|tpAmb` e off-line `chave|3|tpAmb|dia|vNF|tpIdDest|idDest`, que o
|
|
123
|
+
* certificado assina (4.4.1 e 4.4.2). Destinatário não identificado deixa os campos 6 e 7 vazios; o estrangeiro, só o
|
|
124
|
+
* 7. Na contingência off-line (tpEmis 9) entram o dia da emissão e o valor da nota, que o consumidor confere sem a
|
|
125
|
+
* nota ter chegado à SEFAZ.
|
|
126
|
+
*/
|
|
127
|
+
export async function parametrosQrCode(
|
|
128
|
+
d: DadosQrCode,
|
|
129
|
+
qr: QrCodeNfceOpcoes,
|
|
130
|
+
): Promise<{ readonly parametros: string; readonly assinar: boolean }> {
|
|
131
|
+
const tpAmb = tpAmbOf(d.ambiente);
|
|
132
|
+
const offline = d.tpEmis === '9';
|
|
133
|
+
const dia = d.dhEmi.slice(8, 10);
|
|
134
|
+
if (qr.versao === '3') {
|
|
135
|
+
if (!offline) return { parametros: `${d.chave}|3|${tpAmb}`, assinar: false };
|
|
136
|
+
const dest = d.dest;
|
|
137
|
+
const [tp, id] =
|
|
138
|
+
dest?.CNPJ !== undefined
|
|
139
|
+
? ['1', dest.CNPJ]
|
|
140
|
+
: dest?.CPF !== undefined
|
|
141
|
+
? ['2', dest.CPF]
|
|
142
|
+
: dest?.idEstrangeiro !== undefined
|
|
143
|
+
? ['3', '']
|
|
144
|
+
: ['', ''];
|
|
145
|
+
return { parametros: `${d.chave}|3|${tpAmb}|${dia}|${d.vNF}|${tp}|${id}`, assinar: true };
|
|
146
|
+
}
|
|
147
|
+
const idCSC = String(Number(qr.idCSC));
|
|
148
|
+
const antes = offline
|
|
149
|
+
? `${d.chave}|2|${tpAmb}|${dia}|${d.vNF}|${await digValHex(d.xml)}|${idCSC}`
|
|
150
|
+
: `${d.chave}|2|${tpAmb}|${idCSC}`;
|
|
151
|
+
return { parametros: `${antes}|${await hashQrCodeV2(antes, qr.CSC)}`, assinar: false };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Assinatura dos parâmetros 1 a 7 do QR Code versão 3 off-line: RSA com SHA-1 (PKCS#1 v1.5), em Base64, com o mesmo
|
|
156
|
+
* certificado que assina a NFC-e (Manual 6.0, 4.4.2, parâmetro 8).
|
|
157
|
+
*/
|
|
158
|
+
export async function assinarParametros(parametros: string, signer: Signer): Promise<string> {
|
|
159
|
+
const bytes = te.encode(parametros);
|
|
160
|
+
if (signer.kind === 'data') return base64Encode(await signer.sign(bytes, 'SHA-1'));
|
|
161
|
+
const h = await sha1(bytes);
|
|
162
|
+
const di = new Uint8Array(SHA1_DIGEST_INFO_PREFIX.length + h.length);
|
|
163
|
+
di.set(SHA1_DIGEST_INFO_PREFIX);
|
|
164
|
+
di.set(h, SHA1_DIGEST_INFO_PREFIX.length);
|
|
165
|
+
return base64Encode(await signer.signDigestInfo(di));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** CFOP da prestação de serviço tributada pelo ISSQN na NFC-e (MOC 7.0 Anexo I, RV I08-150 a I08-170). */
|
|
169
|
+
const CFOP_ISSQN = '5933';
|
|
170
|
+
|
|
171
|
+
/** Valor da NFC-e acima do qual o destinatário tem de ser identificado (MOC 7.0 Anexo I, RV W16-40, rejeição 750). */
|
|
172
|
+
export const NFCE_LIMITE_SEM_DESTINATARIO = '10000.00';
|
|
173
|
+
|
|
174
|
+
/** Meios de pagamento que a NFC-e não aceita: 14 duplicata mercantil (YA02-10), 90 sem pagamento (YA02-40), 99 outros (YA02-50). */
|
|
175
|
+
const TPAG_VEDADOS: Readonly<Record<string, string>> = {
|
|
176
|
+
'14': 'duplicata mercantil não é meio de pagamento da NFC-e (YA02-10, rejeição 857)',
|
|
177
|
+
'90': 'a NFC-e sempre tem pagamento: tPag 90 (sem pagamento) é recusado (YA02-40, rejeição 899)',
|
|
178
|
+
'99': 'tPag 99 (outros) não é aceito na NFC-e (YA02-50, rejeição 436, NT 2020.006)',
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** Identificação já resolvida pelo montador (com os padrões aplicados). */
|
|
182
|
+
export interface IdeNfce {
|
|
183
|
+
readonly tpNF: string;
|
|
184
|
+
readonly idDest: string;
|
|
185
|
+
readonly tpImp: string;
|
|
186
|
+
readonly finNFe: string;
|
|
187
|
+
readonly indFinal: string;
|
|
188
|
+
readonly indPres: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Regras da NFC-e sobre a entrada (origem `entrada`, ADR 0011), todas de aplicação obrigatória. Grupos que a NFC-e
|
|
193
|
+
* não tem saem como `grupo_vedado` no caminho da entrada.
|
|
194
|
+
*/
|
|
195
|
+
export function conferirNfce(input: NfeInput, ide: IdeNfce, issues: Issues): void {
|
|
196
|
+
const vedado = (path: string, msg: string): void => issues.add(path, 'grupo_vedado', `NFC-e ${msg}`);
|
|
197
|
+
const invalido = (path: string, msg: string): void => issues.add(path, 'campo_invalido', `NFC-e ${msg}`);
|
|
198
|
+
|
|
199
|
+
// B. Identificação
|
|
200
|
+
if (input.dhSaiEnt !== undefined) vedado('dhSaiEnt', 'sem data de entrada ou saída (B10-10, rejeição 705)');
|
|
201
|
+
if (input.dPrevEntrega !== undefined) {
|
|
202
|
+
vedado('dPrevEntrega', 'sem data de previsão de entrega (NT 2025.002, B10a-10, rejeição 1153)');
|
|
203
|
+
}
|
|
204
|
+
if (ide.tpNF !== '1') invalido('tpNF', 'só de saída, tpNF 1 (B11-10, rejeição 706)');
|
|
205
|
+
if (ide.idDest !== '1') {
|
|
206
|
+
invalido('idDest', 'só em operação interna, idDest 1 (B11a-10, rejeição 707)');
|
|
207
|
+
}
|
|
208
|
+
if (ide.tpImp !== '4' && ide.tpImp !== '5') {
|
|
209
|
+
invalido('tpImp', 'com DANFC-e: tpImp 4 (impresso) ou 5 (mensagem eletrônica) (B21-10, rejeição 709)');
|
|
210
|
+
}
|
|
211
|
+
if (ide.finNFe !== '1') invalido('finNFe', 'só com finalidade normal, finNFe 1 (B25-20, rejeição 715)');
|
|
212
|
+
if (ide.indFinal !== '1') invalido('indFinal', 'só com consumidor final, indFinal 1 (B25a-10, rejeição 716)');
|
|
213
|
+
if (!['1', '4', '5'].includes(ide.indPres)) {
|
|
214
|
+
invalido('indPres', 'só presencial: indPres 1, 4 (entrega a domicílio) ou 5 (NT 2025.002, B25b-20, rejeição 717)');
|
|
215
|
+
}
|
|
216
|
+
if (input.cMunFGIBS !== undefined && ide.indPres !== '5') {
|
|
217
|
+
invalido(
|
|
218
|
+
'cMunFGIBS',
|
|
219
|
+
'com cMunFGIBS só em operação presencial fora do estabelecimento, indPres 5 (NT 2025.002, B25b-60, rejeição 1000)',
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (input.cMunFGIBS === undefined && ide.indPres === '5') {
|
|
223
|
+
issues.add(
|
|
224
|
+
'cMunFGIBS',
|
|
225
|
+
'campo_obrigatorio',
|
|
226
|
+
'NFC-e presencial fora do estabelecimento (indPres 5) informa cMunFGIBS (NT 2025.002, B25b-70, rejeição 1005)',
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (input.referenciadas !== undefined && input.referenciadas.length > 0) {
|
|
230
|
+
vedado('referenciadas', 'não referencia documento fiscal (BA01-10, rejeição 708)');
|
|
231
|
+
}
|
|
232
|
+
if (input.gCompraGov !== undefined)
|
|
233
|
+
vedado('gCompraGov', 'sem compra governamental (NT 2025.002, BB01-10, rejeição 1006)');
|
|
234
|
+
|
|
235
|
+
// C. Emitente
|
|
236
|
+
if (input.emitente.IEST !== undefined)
|
|
237
|
+
vedado('emitente.IEST', 'sem IE de substituto tributário (C18-10, rejeição 718)');
|
|
238
|
+
|
|
239
|
+
// E. Destinatário: opcional, obrigatório na entrega a domicílio
|
|
240
|
+
const d = input.destinatario;
|
|
241
|
+
if (ide.indPres === '4' && d === undefined) {
|
|
242
|
+
issues.add(
|
|
243
|
+
'destinatario',
|
|
244
|
+
'campo_obrigatorio',
|
|
245
|
+
'NFC-e com entrega a domicílio identifica o destinatário (E01-20, rejeição 787)',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (ide.indPres === '4' && d !== undefined && d.endereco === undefined) {
|
|
249
|
+
issues.add(
|
|
250
|
+
'destinatario.endereco',
|
|
251
|
+
'campo_obrigatorio',
|
|
252
|
+
'NFC-e com entrega a domicílio informa o endereço do destinatário (E05-20, rejeição 788)',
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (d !== undefined) {
|
|
256
|
+
if (d.CNPJ !== undefined && input.emitente.CNPJ !== undefined && digitos(d.CNPJ) === digitos(input.emitente.CNPJ)) {
|
|
257
|
+
invalido('destinatario.CNPJ', 'com destinatário igual ao emitente (E02-20, rejeição 220)');
|
|
258
|
+
}
|
|
259
|
+
if (d.indIEDest !== '9') {
|
|
260
|
+
invalido('destinatario.indIEDest', 'só para não contribuinte, indIEDest 9 (E16a-10, rejeição 789)');
|
|
261
|
+
}
|
|
262
|
+
if (d.ISUF !== undefined) vedado('destinatario.ISUF', 'sem inscrição na Suframa (E18-10, rejeição 730)');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// I a UA. Itens
|
|
266
|
+
input.itens.forEach((it, n) => {
|
|
267
|
+
const p = `itens[${n}]`;
|
|
268
|
+
const esp = (it.produto.especifico ?? {}) as Record<string, unknown>;
|
|
269
|
+
if (esp.veicProd !== undefined)
|
|
270
|
+
vedado(`${p}.produto.especifico.veicProd`, 'sem veículo novo (J01-10, rejeição 736)');
|
|
271
|
+
if (esp.arma !== undefined) vedado(`${p}.produto.especifico.arma`, 'sem armamento (L01-10, rejeição 738)');
|
|
272
|
+
if (esp.nRECOPI !== undefined)
|
|
273
|
+
vedado(`${p}.produto.especifico.nRECOPI`, 'sem papel imune RECOPI (LB01-10, rejeição 348)');
|
|
274
|
+
if (it.produto.tpCredPresIBSZFM !== undefined) {
|
|
275
|
+
vedado(
|
|
276
|
+
`${p}.produto.tpCredPresIBSZFM`,
|
|
277
|
+
'sem classificação para subapuração do IBS na ZFM (NT 2025.002, I05k-10, rejeição 1165)',
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const cfop = it.produto.CFOP.replace(/\D/g, '');
|
|
281
|
+
const imp = it.impostos;
|
|
282
|
+
if (cfop === CFOP_ISSQN && imp.issqn === undefined) {
|
|
283
|
+
issues.add(
|
|
284
|
+
`${p}.impostos.issqn`,
|
|
285
|
+
'combinacao_invalida',
|
|
286
|
+
'NFC-e com CFOP 5.933 tem o grupo do ISSQN (I08-160, rejeição 374)',
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (cfop !== CFOP_ISSQN && imp.issqn !== undefined) {
|
|
290
|
+
issues.add(
|
|
291
|
+
`${p}.produto.CFOP`,
|
|
292
|
+
'combinacao_invalida',
|
|
293
|
+
'NFC-e com ISSQN usa o CFOP 5.933 (I08-170, rejeição 374)',
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const icms = imp.icms as { grupo?: string; CST?: string; st?: unknown } | undefined;
|
|
297
|
+
if (icms?.grupo === 'Part') vedado(`${p}.impostos.icms`, 'sem partilha do ICMS entre UF (N12-50, rejeição 741)');
|
|
298
|
+
if (icms?.grupo === 'ST') vedado(`${p}.impostos.icms`, 'sem repasse do ICMS-ST retido (N12-60, rejeição 740)');
|
|
299
|
+
if (icms?.grupo === undefined && icms?.CST === '90' && icms.st !== undefined) {
|
|
300
|
+
vedado(`${p}.impostos.icms.st`, 'com CST 90 sem os dados do ICMS-ST (N12-34, rejeição 381)');
|
|
301
|
+
}
|
|
302
|
+
if (imp.icmsUfDest !== undefined)
|
|
303
|
+
vedado(`${p}.impostos.icmsUfDest`, 'sem ICMS da UF de destino (NA01-10, rejeição 807)');
|
|
304
|
+
if (imp.ipi !== undefined) vedado(`${p}.impostos.ipi`, 'sem IPI (O01-10, rejeição 742)');
|
|
305
|
+
if (imp.ii !== undefined) vedado(`${p}.impostos.ii`, 'sem imposto de importação (P01-10, rejeição 743)');
|
|
306
|
+
if (imp.pisSt !== undefined) vedado(`${p}.impostos.pisSt`, 'sem PIS-ST (R01-10, rejeição 746)');
|
|
307
|
+
if (imp.cofinsSt !== undefined) vedado(`${p}.impostos.cofinsSt`, 'sem COFINS-ST (T01-10, rejeição 749)');
|
|
308
|
+
if (it.impostoDevol !== undefined) vedado(`${p}.impostoDevol`, 'sem devolução de tributos (UA01-20, rejeição 390)');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// X. Transporte: só na entrega a domicílio
|
|
312
|
+
const tr = input.transporte;
|
|
313
|
+
const entrega = ide.indPres === '4';
|
|
314
|
+
if (tr !== undefined && tr.modFrete !== '9' && !entrega) {
|
|
315
|
+
invalido('transporte.modFrete', 'sem frete fora da entrega a domicílio: modFrete 9 (X02-10, rejeição 753)');
|
|
316
|
+
}
|
|
317
|
+
if (tr?.transportador !== undefined && !entrega) {
|
|
318
|
+
vedado('transporte.transportador', 'só tem transportador na entrega a domicílio (X03-10, rejeição 754)');
|
|
319
|
+
}
|
|
320
|
+
if (entrega && tr?.transportador === undefined) {
|
|
321
|
+
issues.add(
|
|
322
|
+
'transporte.transportador',
|
|
323
|
+
'campo_obrigatorio',
|
|
324
|
+
'NFC-e com entrega a domicílio identifica o transportador (X03-20, rejeição 786)',
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (tr?.retTransp !== undefined)
|
|
328
|
+
vedado('transporte.retTransp', 'sem retenção do ICMS no transporte (X11-10, rejeição 755)');
|
|
329
|
+
if (tr?.veicTransp !== undefined) vedado('transporte.veicTransp', 'sem veículo de transporte (X18-10, rejeição 756)');
|
|
330
|
+
if (tr?.reboque !== undefined) vedado('transporte.reboque', 'sem reboque (X22-10, rejeição 757)');
|
|
331
|
+
|
|
332
|
+
// Y e YA. Cobrança e pagamento
|
|
333
|
+
if (input.cobranca !== undefined) vedado('cobranca', 'sem fatura nem duplicata (Y01-10, rejeição 760)');
|
|
334
|
+
if (input.pagamento === undefined || input.pagamento.detPag.length === 0) {
|
|
335
|
+
issues.add(
|
|
336
|
+
'pagamento',
|
|
337
|
+
'campo_obrigatorio',
|
|
338
|
+
'a NFC-e informa o pagamento: tPag 90 (sem pagamento) é recusado (YA02-40, rejeição 899)',
|
|
339
|
+
);
|
|
340
|
+
} else {
|
|
341
|
+
input.pagamento.detPag.forEach((p, n) => {
|
|
342
|
+
const motivo = TPAG_VEDADOS[p.tPag];
|
|
343
|
+
if (motivo !== undefined) issues.add(`pagamento.detPag[${n}].tPag`, 'pagamento_invalido', motivo);
|
|
344
|
+
// Cartão e PIX pedem o grupo do cartão (NT 2025.001, YA04-10, rejeição 391; obrigatória no modelo 65).
|
|
345
|
+
if (['03', '04', '17'].includes(p.tPag) && p.card === undefined) {
|
|
346
|
+
issues.add(
|
|
347
|
+
`pagamento.detPag[${n}].card`,
|
|
348
|
+
'campo_obrigatorio',
|
|
349
|
+
'pagamento com cartão (tPag 03 ou 04) ou PIX (17) informa o grupo card (NT 2025.001, YA04-10, rejeição 391)',
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Z. Grupos que a NFC-e não tem
|
|
356
|
+
if (input.exporta !== undefined) vedado('exporta', 'sem comércio exterior (ZA01-30, rejeição 814)');
|
|
357
|
+
if (input.compra !== undefined) vedado('compra', 'sem dados de compra (ZB01-10, rejeição 762)');
|
|
358
|
+
if (input.cana !== undefined) vedado('cana', 'sem aquisição de cana (ZC01-10, rejeição 763)');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** O `pag` montado, na forma que as regras de pagamento leem e completam. */
|
|
362
|
+
export interface PagMontado {
|
|
363
|
+
readonly detPag: readonly {
|
|
364
|
+
readonly tPag: string;
|
|
365
|
+
readonly vPag: string;
|
|
366
|
+
readonly card?: { readonly tpIntegra?: string; readonly CNPJ?: string; readonly cAut?: string };
|
|
367
|
+
}[];
|
|
368
|
+
vTroco?: string;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Regras de pagamento da NFC-e sobre o grupo montado, com o `vNF` já calculado (NT 2025.001, item 02.7: YA03-10 a
|
|
373
|
+
* YA06-10 passam a ser obrigatórias; MOC 7.0 Anexo I, YA09-10). Sem `vTroco` e com pagamento acima do total, o builder
|
|
374
|
+
* calcula o troco (YA03-20, rejeição 866); informado, ele tem de ser a diferença (YA09-10, rejeição 869). O pagamento
|
|
375
|
+
* posterior (tPag 91) tem valor zero e dispensa a soma igual ao total (NT 2025.001 v1.03, YA03-10 e YA03-30).
|
|
376
|
+
*/
|
|
377
|
+
export function pagamentoNfce(pag: PagMontado, vNF: Decimal, issues: Issues): void {
|
|
378
|
+
const total = pag.detPag.reduce((a, p) => a.plus(Decimal.of(p.vPag)), Decimal.ZERO);
|
|
379
|
+
const troco = total.minus(vNF);
|
|
380
|
+
const posterior = pag.detPag.some((p) => p.tPag === '91');
|
|
381
|
+
pag.detPag.forEach((p, n) => {
|
|
382
|
+
if (p.tPag === '91' && !Decimal.of(p.vPag).isZero()) {
|
|
383
|
+
issues.add(
|
|
384
|
+
`pagamento.detPag[${n}].vPag`,
|
|
385
|
+
'pagamento_invalido',
|
|
386
|
+
'pagamento posterior (tPag 91) tem vPag zero (NT 2025.001 v1.03, YA03-30, rejeição 904)',
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
if (troco.isNegative() && !posterior) {
|
|
391
|
+
issues.add(
|
|
392
|
+
'pagamento.detPag',
|
|
393
|
+
'pagamento_invalido',
|
|
394
|
+
`pagamentos (${total.toFixed(2)}) abaixo do total da NFC-e (${vNF.toFixed(2)}) (NT 2025.001 v1.03, YA03-10, rejeição 865)`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
// O troco informado é sempre pagamentos menos o total, também com o pagamento posterior (YA09-10 não tem exceção).
|
|
398
|
+
if (pag.vTroco === undefined) {
|
|
399
|
+
if (!troco.isNegative() && !troco.isZero()) pag.vTroco = troco.toFixed(2);
|
|
400
|
+
} else if (!Decimal.of(pag.vTroco).eq(troco)) {
|
|
401
|
+
issues.add(
|
|
402
|
+
'pagamento.vTroco',
|
|
403
|
+
'valor_divergente',
|
|
404
|
+
`troco ${pag.vTroco} difere de pagamentos menos o total (${troco.toFixed(2)}) (YA09-10, rejeição 869)`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
pag.detPag.forEach((p, n) => {
|
|
408
|
+
const c = p.card;
|
|
409
|
+
if (c === undefined) return;
|
|
410
|
+
const path = `pagamento.detPag[${n}].card`;
|
|
411
|
+
if (c.tpIntegra === '1' && (c.CNPJ === undefined || c.cAut === undefined)) {
|
|
412
|
+
issues.add(
|
|
413
|
+
path,
|
|
414
|
+
'campo_obrigatorio',
|
|
415
|
+
'cartão integrado à automação (tpIntegra 1) informa o CNPJ da credenciadora e o cAut (NT 2025.001, YA05-10, rejeição 392)',
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (c.CNPJ !== undefined) {
|
|
419
|
+
const r = parseCnpj(c.CNPJ, { path: `${path}.CNPJ` });
|
|
420
|
+
if (!r.ok) issues.list.push(r.error);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const digitos = (s: string): string => s.replace(/[^0-9A-Za-z]/g, '').toUpperCase();
|
package/src/build/pl.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escolha do pacote de liberação por vigência e conferência do objeto contra o descritor do PL escolhido.
|
|
3
|
+
*
|
|
4
|
+
* O builder monta sempre na forma do PL mais novo (`PL_010f`). Quando a vigência escolhe um PL mais antigo, o objeto é
|
|
5
|
+
* serializado com o descritor dele; o serializer ignora campos que o descritor não conhece, então antes de serializar
|
|
6
|
+
* `camposForaDoPl` aponta cada campo que sumiria em silêncio.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Ambiente, Clock } from '@sinete/core';
|
|
10
|
+
import { UnsupportedError } from '@sinete/core';
|
|
11
|
+
import type { ComplexType, Particle, VigenciaEntry } from '@sinete/schemas';
|
|
12
|
+
import { isComplexType, isElementParticle, isWildcard, selecionarPl } from '@sinete/schemas';
|
|
13
|
+
import { TNFe_infNFe as Inf010e } from '@sinete/schemas/nfe/PL_010e';
|
|
14
|
+
import { TNFe_infNFe as Inf010f } from '@sinete/schemas/nfe/PL_010f';
|
|
15
|
+
import type { Issues } from '../issues.ts';
|
|
16
|
+
|
|
17
|
+
/** Descritor de `infNFe` de cada módulo da família `nfe` da tabela de vigências. */
|
|
18
|
+
const INF_NFE: Readonly<Record<string, ComplexType>> = {
|
|
19
|
+
'nfe/PL_010e': Inf010e as ComplexType,
|
|
20
|
+
'nfe/PL_010f': Inf010f as ComplexType,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface PlEscolhido {
|
|
24
|
+
readonly vigencia: VigenciaEntry;
|
|
25
|
+
readonly infNFe: ComplexType;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** PL da NF-e vigente para o ambiente no relógio de emissão (ADR 0002, decisão 6). */
|
|
29
|
+
export function escolherPl(ambiente: Ambiente, emissao: Clock): PlEscolhido {
|
|
30
|
+
const vigencia = selecionarPl('nfe', ambiente, emissao);
|
|
31
|
+
const infNFe = INF_NFE[vigencia.modulo];
|
|
32
|
+
if (infNFe === undefined) {
|
|
33
|
+
// A tabela de vigências ganhou um PL que este pacote ainda não conhece: falha explícita, nunca tentativa.
|
|
34
|
+
throw new UnsupportedError(`@sinete/nfe não conhece o módulo ${vigencia.modulo}; atualize o pacote`);
|
|
35
|
+
}
|
|
36
|
+
return { vigencia, infNFe };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function memberNames(p: Particle, acc: Set<string>): Set<string> {
|
|
40
|
+
if (isWildcard(p)) acc.add('$any');
|
|
41
|
+
else if (isElementParticle(p)) acc.add(p.e);
|
|
42
|
+
else for (const i of p.i) memberNames(i, acc);
|
|
43
|
+
return acc;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function elementTypes(p: Particle, acc: Map<string, ComplexType>): Map<string, ComplexType> {
|
|
47
|
+
if (isWildcard(p)) return acc;
|
|
48
|
+
if (isElementParticle(p)) {
|
|
49
|
+
if (isComplexType(p.t)) acc.set(p.e, p.t);
|
|
50
|
+
} else for (const i of p.i) elementTypes(i, acc);
|
|
51
|
+
return acc;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Aponta cada chave do objeto que o descritor não declara (o serializer a descartaria sem aviso). */
|
|
55
|
+
export function camposForaDoPl(ct: ComplexType, value: unknown, path: string, pl: string, issues: Issues): void {
|
|
56
|
+
if (typeof value !== 'object' || value === null) return;
|
|
57
|
+
const allowed = new Set<string>(['$attrs', '$text']);
|
|
58
|
+
for (const a of ct.a ?? []) allowed.add(a.a);
|
|
59
|
+
if (ct.c) memberNames(ct.c, allowed);
|
|
60
|
+
const children = ct.c ? elementTypes(ct.c, new Map()) : new Map<string, ComplexType>();
|
|
61
|
+
for (const [k, v] of Object.entries(value)) {
|
|
62
|
+
if (v === undefined) continue;
|
|
63
|
+
if (!allowed.has(k)) {
|
|
64
|
+
issues.montagem(`${path}.${k}`, 'campo_fora_do_pl', `o campo não existe no ${pl} vigente para esta emissão`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const child = children.get(k);
|
|
68
|
+
if (!child) continue;
|
|
69
|
+
if (Array.isArray(v)) {
|
|
70
|
+
for (const [n, x] of v.entries()) camposForaDoPl(child, x, `${path}.${k}[${n}]`, pl, issues);
|
|
71
|
+
} else camposForaDoPl(child, v, `${path}.${k}`, pl, issues);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regras de validação da SEFAZ que dependem só do documento e que rejeições reais de um integrador em produção mostraram
|
|
3
|
+
* chegando à SEFAZ: conferidas antes de assinar, com o caminho da entrada (ADR 0012).
|
|
4
|
+
*
|
|
5
|
+
* Só entra aqui regra obrigatória (`Obrig.` no MOC) cujo resultado sai do próprio documento, sem cadastro nem
|
|
6
|
+
* configuração da UF. Exceção que depende de tabela externa ou do critério da UF faz a conferência não recusar: na
|
|
7
|
+
* dúvida, a nota vai e a SEFAZ decide, porque recusar localmente uma nota que a SEFAZ aceitaria é pior que a rejeição.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ValidationIssue } from '@sinete/core';
|
|
11
|
+
import type { Issues } from '../issues.ts';
|
|
12
|
+
import type { NfeInput } from '../model.ts';
|
|
13
|
+
import type { Instante } from '../time.ts';
|
|
14
|
+
import { formatDh } from '../time.ts';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CFOP em que o CST 50 (suspensão) é aceito com destinatário contribuinte isento: conserto ou reparo e remessa para
|
|
18
|
+
* demonstração dentro do estado (MOC 7.0 Anexo I, RV N12-80, exceção 1).
|
|
19
|
+
*/
|
|
20
|
+
const CFOP_CST50_ISENTO: ReadonlySet<string> = new Set([
|
|
21
|
+
'1915',
|
|
22
|
+
'1916',
|
|
23
|
+
'2915',
|
|
24
|
+
'2916',
|
|
25
|
+
'5915',
|
|
26
|
+
'5916',
|
|
27
|
+
'6915',
|
|
28
|
+
'6916',
|
|
29
|
+
'1912',
|
|
30
|
+
'1913',
|
|
31
|
+
'5912',
|
|
32
|
+
'5913',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* CST 50 ou 51 com destinatário contribuinte isento de IE (indIEDest 2): rejeição 529 (MOC 7.0 Anexo I, RV N12-80,
|
|
37
|
+
* obrigatória no modelo 55). Fica de fora o que as exceções liberam: o CST 50 nos CFOP da exceção 1 e o CST 51 em
|
|
38
|
+
* qualquer operação interna, que a exceção 3 deixa a critério da UF. A exceção 3 fala em destinatário CNPJ, mas uma UF
|
|
39
|
+
* autorizou em produção CST 51 interno com destinatário CPF isento; como o critério é da UF, o CST 51 interno não é
|
|
40
|
+
* recusado aqui, seja qual for o documento do destinatário. A exceção 2 (emissão antes de 01/07/2016) não alcança nota
|
|
41
|
+
* montada agora.
|
|
42
|
+
*/
|
|
43
|
+
export function cstComIsento(input: NfeInput, idDest: string, issues: Issues): void {
|
|
44
|
+
const d = input.destinatario;
|
|
45
|
+
if (d?.indIEDest !== '2') return;
|
|
46
|
+
input.itens.forEach((it, n) => {
|
|
47
|
+
const icms = it.impostos.icms;
|
|
48
|
+
if (icms === undefined || !('CST' in icms)) return;
|
|
49
|
+
const cst = icms.CST;
|
|
50
|
+
if (cst === '50' && CFOP_CST50_ISENTO.has(it.produto.CFOP.replace(/\D/g, ''))) return;
|
|
51
|
+
if (cst === '51' && idDest === '1') return;
|
|
52
|
+
if (cst !== '50' && cst !== '51') return;
|
|
53
|
+
issues.add(
|
|
54
|
+
`itens[${n}].impostos.icms.CST`,
|
|
55
|
+
'combinacao_invalida',
|
|
56
|
+
`CST ${cst} não se usa com destinatário contribuinte isento de IE (indIEDest 2) (N12-80, rejeição 529)`,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Data (`AAAA-MM-DD`) do instante no deslocamento dado, em minutos. */
|
|
62
|
+
const dataNoFuso = (instante: Instante, offsetMinutes: number): string =>
|
|
63
|
+
formatDh(instante, offsetMinutes).slice(0, 10);
|
|
64
|
+
|
|
65
|
+
/** Brasília (UTC-3), o fuso das SEFAZ, para a comparação de datas que o MOC faz "desconsiderando a hora". */
|
|
66
|
+
const OFFSET_BRASILIA = -180;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Vencimento das duplicatas (grupo Y, modelo 55): uma parcela só, vencendo na data de emissão, cai na rejeição 853,
|
|
70
|
+
* "dados de cobrança não devem ser informados para pagamento à vista" (NT 2025.001 v1.03, RV Y09-40, em produção desde
|
|
71
|
+
* 01/09/2025).
|
|
72
|
+
*
|
|
73
|
+
* As RV Y09-20 (sem `dVenc` ou vencendo antes da emissão, rejeição 900) e Y09-30 (antes da parcela anterior, 850) não
|
|
74
|
+
* são conferidas aqui: uma UF autorizou em produção, em 2026, parcela única vencendo dois meses antes da emissão, o que
|
|
75
|
+
* mostra que a aplicação delas depende da UF. Na dúvida, a nota vai e a SEFAZ decide (ADR 0012).
|
|
76
|
+
*
|
|
77
|
+
* A data de emissão é a do `dhEmi`. O MOC não diz em que fuso a SEFAZ a compara; para não recusar nota que ela aceitaria,
|
|
78
|
+
* "na data da emissão" só vale quando a data local do `dhEmi`, a de Brasília e a de UTC coincidem.
|
|
79
|
+
*/
|
|
80
|
+
export function vencimentos(
|
|
81
|
+
input: NfeInput,
|
|
82
|
+
emissao: { readonly dhEmi: string; readonly instante: Instante },
|
|
83
|
+
issues: Issues,
|
|
84
|
+
): void {
|
|
85
|
+
const dups = input.cobranca?.duplicatas;
|
|
86
|
+
if (dups === undefined || dups.length !== 1) return;
|
|
87
|
+
const unica = dups[0]?.dVenc;
|
|
88
|
+
if (unica === undefined) return;
|
|
89
|
+
const local = emissao.dhEmi.slice(0, 10);
|
|
90
|
+
const datas = new Set([local, dataNoFuso(emissao.instante, OFFSET_BRASILIA), dataNoFuso(emissao.instante, 0)]);
|
|
91
|
+
if (datas.size === 1 && datas.has(unica)) {
|
|
92
|
+
issues.add(
|
|
93
|
+
'cobranca.duplicatas',
|
|
94
|
+
'combinacao_invalida',
|
|
95
|
+
'uma parcela só, vencendo na data de emissão, é pagamento à vista: não informe a cobrança (Y09-40, rejeição 853)',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Documento sem pontuação e em maiúsculas (o CNPJ alfanumérico mantém as letras). */
|
|
101
|
+
const soDocumento = (s: string): string => s.replace(/[^0-9A-Za-z]/g, '').toUpperCase();
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Confere o emitente com o titular do certificado que assina a NF-e (MOC 7.0 Anexo I, grupo F):
|
|
105
|
+
*
|
|
106
|
+
* - e-CNPJ: o CNPJ-base (8 primeiros caracteres) do emitente é o do certificado, senão rejeição 213 (RV F03);
|
|
107
|
+
* - e-CPF: o CPF do emitente é o do certificado, senão rejeição 227 (RV F03A).
|
|
108
|
+
*
|
|
109
|
+
* Só compara documento com documento do mesmo tipo: e-CNPJ assinando nota de emitente CPF (ou o contrário) fica para a
|
|
110
|
+
* SEFAZ. O certificado da SEFAZ que a F03 dispensa só assina a nota avulsa, que o `buildNfe` não monta (procEmi 0).
|
|
111
|
+
* Devolve as ocorrências (vazio quando confere), com o caminho da entrada.
|
|
112
|
+
*/
|
|
113
|
+
export function conferirEmitenteDoCertificado(
|
|
114
|
+
nfe: NfeInput,
|
|
115
|
+
titular: { readonly cnpj?: string | undefined; readonly cpf?: string | undefined },
|
|
116
|
+
): readonly ValidationIssue[] {
|
|
117
|
+
const e = nfe.emitente;
|
|
118
|
+
if (e.CNPJ !== undefined && titular.cnpj !== undefined) {
|
|
119
|
+
const base = soDocumento(e.CNPJ).slice(0, 8);
|
|
120
|
+
if (base !== soDocumento(titular.cnpj).slice(0, 8)) {
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
path: 'emitente.CNPJ',
|
|
124
|
+
code: 'emitente_difere_do_certificado',
|
|
125
|
+
message: `CNPJ-base ${base} do emitente difere do CNPJ-base do certificado (F03, rejeição 213)`,
|
|
126
|
+
origem: 'entrada',
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (e.CPF !== undefined && titular.cpf !== undefined && soDocumento(e.CPF) !== soDocumento(titular.cpf)) {
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
path: 'emitente.CPF',
|
|
135
|
+
code: 'emitente_difere_do_certificado',
|
|
136
|
+
message: 'CPF do emitente difere do CPF do certificado (F03A, rejeição 227)',
|
|
137
|
+
origem: 'entrada',
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
return [];
|
|
142
|
+
}
|