@useinvio/nfse-sdk 3.0.0 → 3.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.
- package/CHANGELOG.md +176 -0
- package/README.en.md +146 -0
- package/README.md +77 -518
- package/dist/agent.d.ts +131 -0
- package/dist/agent.js +551 -0
- package/dist/agent.js.map +1 -0
- package/dist/certificate.d.ts +96 -0
- package/dist/certificate.js +218 -0
- package/dist/certificate.js.map +1 -0
- package/dist/certificateValidation.d.ts +11 -6
- package/dist/certificateValidation.js +23 -12
- package/dist/certificateValidation.js.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +520 -0
- package/dist/cli.js.map +1 -0
- package/dist/cliBin.d.ts +2 -0
- package/dist/cliBin.js +6 -0
- package/dist/cliBin.js.map +1 -0
- package/dist/cliExample.d.ts +6 -0
- package/dist/cliExample.js +29 -0
- package/dist/cliExample.js.map +1 -0
- package/dist/client.d.ts +51 -4
- package/dist/client.js +41 -10
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/consultas.d.ts +34 -0
- package/dist/consultas.js +61 -0
- package/dist/consultas.js.map +1 -0
- package/dist/dpsJson.js +1 -41
- package/dist/dpsJson.js.map +1 -1
- package/dist/dpsJsonSchema.d.ts +17 -0
- package/dist/dpsJsonSchema.js +184 -0
- package/dist/dpsJsonSchema.js.map +1 -0
- package/dist/emissaoNota.d.ts +11 -6
- package/dist/emissaoNota.js +10 -6
- package/dist/emissaoNota.js.map +1 -1
- package/dist/eventoJson.d.ts +76 -0
- package/dist/eventoJson.js +128 -0
- package/dist/eventoJson.js.map +1 -0
- package/dist/eventosNfse.d.ts +38 -0
- package/dist/eventosNfse.js +73 -0
- package/dist/eventosNfse.js.map +1 -0
- package/dist/fiscalDomains.d.ts +22 -0
- package/dist/fiscalDomains.js +99 -0
- package/dist/fiscalDomains.js.map +1 -0
- package/dist/fiscalValidation.js +21 -53
- package/dist/fiscalValidation.js.map +1 -1
- package/dist/gzipB64.d.ts +5 -0
- package/dist/gzipB64.js +12 -0
- package/dist/gzipB64.js.map +1 -1
- package/dist/index.d.ts +22 -7
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/jsonSchemaCheck.d.ts +12 -0
- package/dist/jsonSchemaCheck.js +70 -0
- package/dist/jsonSchemaCheck.js.map +1 -0
- package/dist/loadPfx.d.ts +2 -0
- package/dist/loadPfx.js +2 -1
- package/dist/loadPfx.js.map +1 -1
- package/dist/mcpCli.d.ts +2 -0
- package/dist/mcpCli.js +7 -0
- package/dist/mcpCli.js.map +1 -0
- package/dist/mcpServer.d.ts +49 -0
- package/dist/mcpServer.js +159 -0
- package/dist/mcpServer.js.map +1 -0
- package/dist/nfseXml.d.ts +48 -0
- package/dist/nfseXml.js +86 -0
- package/dist/nfseXml.js.map +1 -0
- package/dist/rejeicoes.d.ts +50 -0
- package/dist/rejeicoes.js +62 -0
- package/dist/rejeicoes.js.map +1 -0
- package/dist/rejeicoesCatalog.d.ts +10 -0
- package/dist/rejeicoesCatalog.js +4449 -0
- package/dist/rejeicoesCatalog.js.map +1 -0
- package/dist/sefinClient.d.ts +107 -7
- package/dist/sefinClient.js +232 -22
- package/dist/sefinClient.js.map +1 -1
- package/dist/signXml.d.ts +3 -3
- package/dist/signXml.js +17 -7
- package/dist/signXml.js.map +1 -1
- package/dist/testing.d.ts +80 -0
- package/dist/testing.js +285 -0
- package/dist/testing.js.map +1 -0
- package/dist/xmlBuild.d.ts +11 -0
- package/dist/xmlBuild.js +44 -0
- package/dist/xmlBuild.js.map +1 -0
- package/dist/xmlInspection.d.ts +1 -1
- package/dist/xmlInspection.js +10 -9
- package/dist/xmlInspection.js.map +1 -1
- package/dist/xsdValidation.d.ts +24 -5
- package/dist/xsdValidation.js +97 -67
- package/dist/xsdValidation.js.map +1 -1
- package/docs/README.md +47 -0
- package/docs/REGRAS_NEGOCIO_EMISSAO.md +8 -8
- package/docs/conceitos/como-funciona.md +87 -0
- package/docs/guias/adn.md +51 -0
- package/docs/guias/agentes-ia.md +111 -0
- package/docs/guias/cancelar.md +70 -0
- package/docs/guias/certificado.md +88 -0
- package/docs/guias/cli.md +68 -0
- package/docs/guias/consultar.md +54 -0
- package/docs/guias/emitir.md +172 -0
- package/docs/guias/recuperar-emissao.md +33 -0
- package/docs/guias/rejeicoes.md +70 -0
- package/docs/guias/testes.md +79 -0
- package/docs/guias/timeouts.md +33 -0
- package/docs/referencia/ambientes.md +35 -0
- package/docs/referencia/api.md +137 -0
- package/docs/referencia/erros.md +55 -0
- package/docs/tutoriais/primeira-nfse.md +130 -0
- package/examples/README.md +3 -1
- package/examples/cancel-nfse.ts +29 -0
- package/examples/client-resource.ts +3 -5
- package/examples/emit-homologation.ts +3 -4
- package/examples/handle-errors.ts +3 -3
- package/examples/recover-emission.ts +29 -0
- package/llms.txt +97 -82
- package/package.json +31 -6
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Cancelar uma NFS-e
|
|
2
|
+
|
|
3
|
+
## Cancelamento (e101101)
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { cancelarNfse, RegistrarEventoError } from '@useinvio/nfse-sdk';
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const evento = await cancelarNfse(
|
|
10
|
+
{
|
|
11
|
+
ambiente: 'restrita',
|
|
12
|
+
chaveAcesso,
|
|
13
|
+
cMotivo: '1', // 1 = erro na emissão, 2 = serviço não prestado, 9 = outros
|
|
14
|
+
xMotivo: 'Nota emitida com valor incorreto', // 15 a 255 caracteres
|
|
15
|
+
// autor: { CNPJ: '...' } // padrão: CNPJ/CPF do titular do certificado
|
|
16
|
+
},
|
|
17
|
+
certificado,
|
|
18
|
+
);
|
|
19
|
+
console.log(evento.pedRegEventoId, evento.eventoXml);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof RegistrarEventoError) console.error(error.status, error.erros);
|
|
22
|
+
else throw error;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
O SDK monta o `pedRegEvento` v1.01, valida contra o XSD oficial, assina
|
|
27
|
+
`infPedReg`, compacta e envia em `pedidoRegistroEventoXmlGZipB64`. Guarde
|
|
28
|
+
`evento.pedRegEventoXml` (o pedido assinado) junto com a resposta.
|
|
29
|
+
|
|
30
|
+
As constantes `MOTIVO_CANCELAMENTO` e `MOTIVO_SUBSTITUICAO` têm os códigos com
|
|
31
|
+
nomes legíveis, por exemplo `MOTIVO_CANCELAMENTO.servicoNaoPrestado`.
|
|
32
|
+
|
|
33
|
+
## Cancelamento por substituição (e105102)
|
|
34
|
+
|
|
35
|
+
Emita primeiro a nota substituta, depois cancele a original apontando para ela:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { cancelarNfsePorSubstituicao, MOTIVO_SUBSTITUICAO } from '@useinvio/nfse-sdk';
|
|
39
|
+
|
|
40
|
+
await cancelarNfsePorSubstituicao(
|
|
41
|
+
{
|
|
42
|
+
chaveAcesso: chaveOriginal,
|
|
43
|
+
chaveSubstituta,
|
|
44
|
+
cMotivo: MOTIVO_SUBSTITUICAO.outros,
|
|
45
|
+
xMotivo: 'Correcao do valor do servico prestado', // opcional
|
|
46
|
+
},
|
|
47
|
+
certificado,
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Consultar eventos
|
|
52
|
+
|
|
53
|
+
- `consultarEvento(chave, TP_EVENTO.cancelamento, certificado)`: SEFIN,
|
|
54
|
+
`GET /nfse/{chave}/eventos/{tpEvento}/{nSeqEvento}` (`nSeqEvento` padrão 1).
|
|
55
|
+
- `consultarEventosPorChaveAdn(chave, certificado)`: ADN, todos os eventos da nota.
|
|
56
|
+
|
|
57
|
+
## Montar sem enviar
|
|
58
|
+
|
|
59
|
+
`buildCancelamentoFromJson(...)` e `buildCancelamentoPorSubstituicaoFromJson(...)`
|
|
60
|
+
devolvem `{ id, xml }` não assinado. `validatePedRegEventoXmlAgainstXsd(xml)`
|
|
61
|
+
confere contra o XSD; `registrarEvento(built, certificado)` assina e envia.
|
|
62
|
+
|
|
63
|
+
## No cliente
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
await client.invoices.cancel({ chaveAcesso, cMotivo: '2', xMotivo: 'Servico nao foi prestado ao tomador' });
|
|
67
|
+
await client.invoices.cancelBySubstitution({ chaveAcesso, chaveSubstituta, cMotivo: '99' });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Eventos são POSTs e nunca são repetidos automaticamente, mesmo com `retries`.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Configurar o certificado A1
|
|
2
|
+
|
|
3
|
+
Use `NfseCertificate`. Ele lê o `.pfx`/`.p12` uma vez, mantém a chave privada
|
|
4
|
+
fora do objeto e descarta a senha e os bytes do arquivo. `console.log(certificado)`
|
|
5
|
+
e `JSON.stringify(certificado)` mostram só CNPJ/CPF, validade e fingerprint.
|
|
6
|
+
|
|
7
|
+
## Variáveis de ambiente (recomendado)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
NFSE_CERT_PATH=./certificado.pfx # ou NFSE_CERT_BASE64=<PFX em base64>
|
|
11
|
+
NFSE_CERT_PASSWORD=sua-senha # ou NFSE_CERT_PASSWORD_FILE=/run/secrets/nfse_cert_password
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { NfseCertificate } from '@useinvio/nfse-sdk';
|
|
16
|
+
|
|
17
|
+
const certificado = NfseCertificate.fromEnv();
|
|
18
|
+
console.log(certificado.cnpj, certificado.notAfter, certificado.daysUntilExpiry());
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`NFSE_CERT_PASSWORD_FILE` lê a senha de um arquivo, o formato de secrets do
|
|
22
|
+
Docker e do Kubernetes. Nunca versione o `.pfx` nem a senha: o `.gitignore` do
|
|
23
|
+
projeto já ignora `*.pfx`, `*.p12` e `.env*`.
|
|
24
|
+
|
|
25
|
+
Para gerar o base64 do PFX:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
base64 -i certificado.pfx | tr -d '\n'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Outras origens
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
NfseCertificate.fromFile('./certificado.pfx', senha);
|
|
35
|
+
NfseCertificate.fromBase64(pfxBase64, senha); // aceita quebras de linha e data: URLs
|
|
36
|
+
NfseCertificate.fromPfx(pfxBuffer, senha); // ex.: PFX vindo do seu cofre de segredos
|
|
37
|
+
NfseCertificate.fromPem({ key, cert, passphrase }); // par extraído com openssl
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Vários emitentes (multi-tenant)
|
|
41
|
+
|
|
42
|
+
Crie um `NfseCertificate` por emitente e reaproveite-o. Cada certificado tem o
|
|
43
|
+
próprio agente mTLS, então conexões nunca são compartilhadas entre emitentes.
|
|
44
|
+
Guarde o PFX cifrado (KMS, Vault, Secrets Manager) e a senha em um segredo
|
|
45
|
+
separado, e descriptografe só ao criar o objeto.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { assertCertificateForProvider, NfseCertificate } from '@useinvio/nfse-sdk';
|
|
49
|
+
|
|
50
|
+
const cache = new Map<string, NfseCertificate>();
|
|
51
|
+
|
|
52
|
+
async function certificadoDoEmitente(cnpj: string): Promise<NfseCertificate> {
|
|
53
|
+
const cached = cache.get(cnpj);
|
|
54
|
+
if (cached?.isValidAt()) return cached;
|
|
55
|
+
const { pfx, senha } = await cofre.buscarCertificado(cnpj); // seu secret manager
|
|
56
|
+
const certificado = NfseCertificate.fromPfx(pfx, senha);
|
|
57
|
+
assertCertificateForProvider(certificado, cnpj);
|
|
58
|
+
cache.set(cnpj, certificado);
|
|
59
|
+
return certificado;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Vigência e titular
|
|
64
|
+
|
|
65
|
+
- A vigência é verificada antes de assinar e de novo antes de enviar.
|
|
66
|
+
- `certificado.daysUntilExpiry()` ajuda a alertar antes do vencimento.
|
|
67
|
+
- `assertCertificateForProvider(certificado, documento)` confere se o CNPJ/CPF do
|
|
68
|
+
titular (campos ICP-Brasil ou sufixo `NOME:DOCUMENTO` do CN) é o do emitente.
|
|
69
|
+
É uma chamada explícita: o SDK não resolve procuração nem representação fiscal.
|
|
70
|
+
|
|
71
|
+
## Erros
|
|
72
|
+
|
|
73
|
+
Todos lançam `CertificateValidationError` com um `code` estável:
|
|
74
|
+
|
|
75
|
+
| `code` | Quando |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `CERTIFICATE_INVALID_PASSWORD` | Senha incorreta. |
|
|
78
|
+
| `CERTIFICATE_UNREADABLE` | O conteúdo não é um PFX/PEM válido. |
|
|
79
|
+
| `CERTIFICATE_MISSING`, `CERTIFICATE_PASSWORD_MISSING` | Variáveis de ambiente ausentes em `fromEnv`. |
|
|
80
|
+
| `CERTIFICATE_NOT_YET_VALID`, `CERTIFICATE_EXPIRED` | Fora da vigência. |
|
|
81
|
+
| `CERTIFICATE_HOLDER_MISMATCH` | `assertCertificateForProvider` com CPF/CNPJ diferente do titular. |
|
|
82
|
+
|
|
83
|
+
## Legado
|
|
84
|
+
|
|
85
|
+
`loadPfx` e `loadPfxFromBuffer` continuam funcionando, mas retornam a chave
|
|
86
|
+
privada em PEM dentro de um objeto comum. Prefira `NfseCertificate`. Em
|
|
87
|
+
`NfseClient`, `certificate: { content, password }` também é aceito e é
|
|
88
|
+
convertido para `NfseCertificate`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Usar pelo terminal
|
|
2
|
+
|
|
3
|
+
O pacote traz o comando `nfse`. No seu projeto, depois de
|
|
4
|
+
`npm install @useinvio/nfse-sdk`, rode com `npx nfse`. Sem instalar:
|
|
5
|
+
`npx -p @useinvio/nfse-sdk nfse`.
|
|
6
|
+
|
|
7
|
+
## Em um minuto, sem certificado
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx nfse init # cria nota.json com valores ilustrativos
|
|
11
|
+
npx nfse validate nota.json # regras do SDK + XSD oficial, sem enviar
|
|
12
|
+
npx nfse emit nota.json --mock # emite numa SEFIN simulada, com certificado de teste
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Troque CNPJ, municípios, código de serviço e tributação pelos do seu prestador
|
|
16
|
+
antes de emitir de verdade.
|
|
17
|
+
|
|
18
|
+
## Com o seu certificado
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export NFSE_CERT_PATH=./certificado.pfx
|
|
22
|
+
export NFSE_CERT_PASSWORD_FILE=./senha.txt # ou deixe que o nfse pergunte a senha
|
|
23
|
+
|
|
24
|
+
npx nfse doctor # Node.js, motor XSD, certificado e vencimento
|
|
25
|
+
npx nfse emit nota.json # mostra o resumo e pede confirmação
|
|
26
|
+
npx nfse get <chave> -o nfse.xml
|
|
27
|
+
npx nfse danfse <chave>
|
|
28
|
+
npx nfse cancel <chave> --motivo 1 --justificativa "Nota emitida com valor incorreto"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
O ambiente padrão é a produção restrita. `--producao` (ou `NFSE_AMBIENTE=producao`)
|
|
32
|
+
emite notas com validade jurídica e exige digitar `emitir` para confirmar.
|
|
33
|
+
|
|
34
|
+
## Comandos
|
|
35
|
+
|
|
36
|
+
| Comando | O que faz |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `init [nota.json]` | Cria um JSON de nota de exemplo. Nunca sobrescreve. |
|
|
39
|
+
| `doctor` | Confere Node.js, motor XSD, certificado e dias até o vencimento. |
|
|
40
|
+
| `cert [arquivo.pfx]` | Titular, CNPJ/CPF, emissor, validade e fingerprint. Nunca mostra a chave. |
|
|
41
|
+
| `validate <nota.json>` | Valida o JSON e o XML contra o XSD oficial. |
|
|
42
|
+
| `xml <nota.json>` | Gera o XML da DPS não assinado (`-o` para salvar). |
|
|
43
|
+
| `emit <nota.json>` | Assina, mostra o resumo, confirma e envia. Após timeout, consulta a DPS antes de qualquer reenvio. |
|
|
44
|
+
| `get <chave>` | Resumo da NFS-e (`-o` salva o XML). |
|
|
45
|
+
| `danfse <chave>` | Salva o PDF do DANFSe. |
|
|
46
|
+
| `cancel <chave>` | Cancela (`--motivo 1\|2\|9 --justificativa "..."`), mostrando a nota antes. |
|
|
47
|
+
| `recover <dpsId>` | Diz se a SEFIN gerou NFS-e para uma DPS. |
|
|
48
|
+
| `explain <código...>` | Mensagem, regra e campo oficiais de códigos de rejeição. |
|
|
49
|
+
| `mcp` | Inicia o [servidor MCP](agentes-ia.md). |
|
|
50
|
+
|
|
51
|
+
## Opções
|
|
52
|
+
|
|
53
|
+
| Opção | |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `--producao` | Ambiente de produção. |
|
|
56
|
+
| `--mock` | SEFIN simulada e certificado de teste. Não combina com `--producao`. |
|
|
57
|
+
| `--cert <arquivo>` / `--password-file <arquivo>` | Certificado e senha, no lugar das variáveis de ambiente. |
|
|
58
|
+
| `-o, --output <arquivo>` | Onde salvar XML ou PDF. |
|
|
59
|
+
| `-y, --yes` | Confirma sem perguntar (para scripts). Sem terminal interativo, `emit` e `cancel` exigem `--yes`. |
|
|
60
|
+
| `--json` | Saída em JSON, para scripts e CI. |
|
|
61
|
+
|
|
62
|
+
## Códigos de saída
|
|
63
|
+
|
|
64
|
+
| Código | Significado |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `0` | Sucesso (ou cancelado por você na confirmação). |
|
|
67
|
+
| `1` | Erro de entrada, certificado, validação ou rede. |
|
|
68
|
+
| `2` | A SEFIN rejeitou; a explicação oficial de cada código é impressa. |
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Consultar uma NFS-e e baixar o DANFSe
|
|
2
|
+
|
|
3
|
+
## Pela chave de acesso
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { ConsultaNfseError, obterNfse } from '@useinvio/nfse-sdk';
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const nota = await obterNfse(chaveAcesso, certificado, 'restrita');
|
|
10
|
+
console.log(nota.resumo.nNFSe, nota.resumo.cStat, nota.resumo.valores.vLiq);
|
|
11
|
+
console.log(nota.nfseXml); // XML autorizado, já descompactado
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error instanceof ConsultaNfseError && error.notFound) console.log('NFS-e não existe neste ambiente');
|
|
14
|
+
else throw error;
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`resumo` (`NfseResumo`) traz:
|
|
19
|
+
|
|
20
|
+
| Campo | Conteúdo |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `chaveAcesso`, `nNFSe`, `nDFSe` | Identificação da nota. |
|
|
23
|
+
| `cStat`, `dhProc` | Situação e data/hora de processamento. |
|
|
24
|
+
| `ambiente` | `restrita` ou `producao`, lido do `tpAmb` da DPS. |
|
|
25
|
+
| `emitente`, `tomador` | `CNPJ`/`CPF`/`NIF`, `IM`, `xNome`. |
|
|
26
|
+
| `dps` | `id`, `serie`, `nDPS`, `dhEmi`, `dCompet` da DPS de origem. |
|
|
27
|
+
| `valores` | `vServ`, `vBC`, `pAliqAplic`, `vISSQN`, `vTotalRet`, `vLiq`. |
|
|
28
|
+
| `xLocEmi`, `xLocPrestacao`, `cLocIncid`, `xLocIncid`, `xTribNac` | Locais e descrição da tributação. |
|
|
29
|
+
|
|
30
|
+
Os valores ficam como strings, exatamente como no XML. Para ler um XML que você
|
|
31
|
+
já tem, use `lerNfse(xml)`. No cliente: `client.invoices.retrieve(chave)`.
|
|
32
|
+
|
|
33
|
+
A resposta crua (`{ status, body }`) continua em `consultarNfse(...)` e
|
|
34
|
+
`client.invoices.get(...)`.
|
|
35
|
+
|
|
36
|
+
## Pelo Id da DPS
|
|
37
|
+
|
|
38
|
+
`consultarDps(dpsId, certificado)` chama `GET /dps/{id}` e devolve a chave de
|
|
39
|
+
acesso gerada. Para o fluxo completo depois de um timeout, use
|
|
40
|
+
[`recuperarNfsePorDps`](recuperar-emissao.md).
|
|
41
|
+
|
|
42
|
+
## DANFSe em PDF
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { writeFile } from 'node:fs/promises';
|
|
46
|
+
import { baixarDanfse } from '@useinvio/nfse-sdk';
|
|
47
|
+
|
|
48
|
+
const danfse = await baixarDanfse(chaveAcesso, certificado, 'restrita');
|
|
49
|
+
if (danfse.pdf) await writeFile(`danfse-${chaveAcesso}.pdf`, danfse.pdf);
|
|
50
|
+
else console.error(danfse.status, danfse.body);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
O PDF oficial vem do ADN (`GET /danfse/{chave}`). No cliente:
|
|
54
|
+
`client.invoices.danfse(chave)`.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Emitir uma NFS-e
|
|
2
|
+
|
|
3
|
+
Há três entradas. Todas assinam, compactam, enviam por mTLS e devolvem
|
|
4
|
+
`ResultadoEmissaoNota` (`chaveAcesso`, `dpsId`, `nfseXml`, `status`, `body`).
|
|
5
|
+
|
|
6
|
+
| Situação | Use |
|
|
7
|
+
|---|---|
|
|
8
|
+
| Você monta os dados e quer que o SDK gere o XML | `emitirNfse(notaJson, certificado)` |
|
|
9
|
+
| Prefere configurar certificado, ambiente e defaults uma vez | `new NfseClient(...)` e `client.invoices.create(...)` |
|
|
10
|
+
| Outro sistema já gera o XML da DPS | `emitirNfse(xmlString, certificado)` |
|
|
11
|
+
|
|
12
|
+
## A partir de JSON declarativo
|
|
13
|
+
|
|
14
|
+
A referência de campos está em [JSON_MAPPING.md](../../JSON_MAPPING.md) e as
|
|
15
|
+
combinações fiscais aceitas em [FISCAL_MATRIX.md](../FISCAL_MATRIX.md). O
|
|
16
|
+
builder valida formatos e combinações mínimas antes de gerar XML, mas não
|
|
17
|
+
escolhe códigos fiscais por você.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { emitirNfse, NfseCertificate, type DpsJsonRequest } from '@useinvio/nfse-sdk';
|
|
21
|
+
|
|
22
|
+
const nota: DpsJsonRequest = {
|
|
23
|
+
ambiente: 'restrita',
|
|
24
|
+
prestador: {
|
|
25
|
+
cnpj: '12345678000195',
|
|
26
|
+
cLocEmi: '4106902',
|
|
27
|
+
serie: '1601',
|
|
28
|
+
opSimpNac: '1',
|
|
29
|
+
regEspTrib: '0',
|
|
30
|
+
},
|
|
31
|
+
servico: {
|
|
32
|
+
cTribNac: '010201',
|
|
33
|
+
xDescServ: 'Desenvolvimento de software',
|
|
34
|
+
cLocPrestacao: '4106902',
|
|
35
|
+
cNBS: '115022000',
|
|
36
|
+
},
|
|
37
|
+
emissao: {
|
|
38
|
+
nDPS: '1',
|
|
39
|
+
dhEmi: '2026-06-15T10:30:00-03:00',
|
|
40
|
+
dCompet: '2026-06-01',
|
|
41
|
+
valores: { vServ: '1500.00' },
|
|
42
|
+
tributacaoMunicipal: {
|
|
43
|
+
tribISSQN: '1',
|
|
44
|
+
tpRetISSQN: '1',
|
|
45
|
+
},
|
|
46
|
+
tributacaoFederal: {
|
|
47
|
+
piscofins: { CST: '07' },
|
|
48
|
+
},
|
|
49
|
+
totTrib: {
|
|
50
|
+
pTotTribFed: '0.00',
|
|
51
|
+
pTotTribEst: '0.00',
|
|
52
|
+
pTotTribMun: '5.00',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const resultado = await emitirNfse(nota, NfseCertificate.fromEnv());
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
> `pTotTribFed`, `pTotTribEst` e `pTotTribMun` são percentuais com duas casas
|
|
61
|
+
> (ex.: `"5.00"`), diferentes das alíquotas com quatro casas (`pAliq`). O shape
|
|
62
|
+
> simplificado legado de `tribNac` é rejeitado: IBS/CBS exige o bloco RTC
|
|
63
|
+
> `IBSCBS`, ainda não implementado.
|
|
64
|
+
|
|
65
|
+
## Com o cliente
|
|
66
|
+
|
|
67
|
+
O `NfseClient` usa os mesmos campos (`prestador`, `servico`, `emissao`). Os
|
|
68
|
+
`defaults` seguem o mesmo formato e podem ser sobrescritos em cada chamada.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { NfseCertificate, NfseClient } from '@useinvio/nfse-sdk';
|
|
72
|
+
|
|
73
|
+
const client = new NfseClient({
|
|
74
|
+
environment: 'sandbox',
|
|
75
|
+
certificate: NfseCertificate.fromEnv(),
|
|
76
|
+
defaults: {
|
|
77
|
+
prestador: {
|
|
78
|
+
cnpj: '12345678000195',
|
|
79
|
+
cLocEmi: '4106902',
|
|
80
|
+
serie: '1601',
|
|
81
|
+
opSimpNac: '1',
|
|
82
|
+
regEspTrib: '0',
|
|
83
|
+
},
|
|
84
|
+
servico: {
|
|
85
|
+
cTribNac: '010201',
|
|
86
|
+
xDescServ: 'Servico ficticio de desenvolvimento de software',
|
|
87
|
+
cLocPrestacao: '4106902',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const invoice = await client.invoices.create({
|
|
93
|
+
emissao: {
|
|
94
|
+
nDPS: '1',
|
|
95
|
+
valores: { vServ: '1000.00' },
|
|
96
|
+
tomador: {
|
|
97
|
+
CPF: '00000000000',
|
|
98
|
+
xNome: 'Cliente Exemplo',
|
|
99
|
+
end: {
|
|
100
|
+
endNac: { cMun: '4106902', CEP: '80010000' },
|
|
101
|
+
xLgr: 'Rua Exemplo', nro: '100', xBairro: 'Centro',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
tributacaoMunicipal: { tribISSQN: '1', tpRetISSQN: '1' },
|
|
105
|
+
tributacaoFederal: { piscofins: { CST: '07' } },
|
|
106
|
+
totTrib: { pTotTribFed: '0.00', pTotTribEst: '0.00', pTotTribMun: '5.00' },
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`environment: 'sandbox'` é alias de `restrita`; `production`, de `producao`.
|
|
112
|
+
`client.invoices.buildDpsJson(...)` devolve o JSON combinado sem assinar nem enviar.
|
|
113
|
+
|
|
114
|
+
## A partir de XML pronto
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const resultado = await emitirNfse(dpsXml, certificado, { ambiente: 'restrita' });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
O XML precisa ter `Id` válido em `<infDPS>` e `tpAmb` igual ao ambiente de envio.
|
|
121
|
+
`options.dpsId` só seleciona a referência da assinatura; não corrige um XML
|
|
122
|
+
incompleto nem dispensa o XSD.
|
|
123
|
+
|
|
124
|
+
## Inspecionar o XML antes de enviar
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { buildDpsFromJson, validateDpsXmlAgainstXsd } from '@useinvio/nfse-sdk';
|
|
128
|
+
|
|
129
|
+
const { id, xml } = buildDpsFromJson(nota); // XML não assinado
|
|
130
|
+
validateDpsXmlAgainstXsd(xml); // lança DpsXsdValidationError se violar o XSD
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Para validar o JSON sem lançar erro, use `validateDpsJsonRequest(nota)`: retorna
|
|
134
|
+
`valid`, `issues`, `warnings` e uma cópia `normalizedPayload`.
|
|
135
|
+
|
|
136
|
+
## Preparar e transmitir separadamente
|
|
137
|
+
|
|
138
|
+
Salve a DPS assinada antes de enviar; assim uma queda de conexão nunca deixa
|
|
139
|
+
você sem saber o que foi transmitido.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { prepararNota, transmitirNotaPreparada } from '@useinvio/nfse-sdk';
|
|
143
|
+
|
|
144
|
+
const preparada = prepararNota(nota, certificado); // valida, assina e verifica a assinatura
|
|
145
|
+
await salvar(preparada.dpsId, preparada.signedXml);
|
|
146
|
+
const resultado = await transmitirNotaPreparada(preparada, certificado);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Série e número da DPS
|
|
150
|
+
|
|
151
|
+
A SEFIN identifica uma DPS por prestador, município, `serie` e `nDPS`. Mantenha
|
|
152
|
+
uma `serie` fixa (em `prestador.serie` ou por emissão em `emissao.serie`) e
|
|
153
|
+
incremente `nDPS` a cada nova DPS: `1601/1`, `1601/2`, `1601/3`...
|
|
154
|
+
|
|
155
|
+
O Emissor Web usa séries como `70000`. Para emissão via API, use uma série
|
|
156
|
+
válida para API (ex.: `1601`) e controle o `nDPS` na sua aplicação.
|
|
157
|
+
|
|
158
|
+
## Regras que o SDK aplica
|
|
159
|
+
|
|
160
|
+
- Campos opcionais devem ser omitidos ou `undefined`; string vazia, `null` e
|
|
161
|
+
tipos incorretos são rejeitados.
|
|
162
|
+
- Valores monetários são arredondados para duas casas; valores de serviço que
|
|
163
|
+
arredondam para zero são rejeitados.
|
|
164
|
+
- Divergências entre `vServ` e a conversão cambial viram `warnings`, preservando
|
|
165
|
+
o valor explícito.
|
|
166
|
+
- Ambiente para JSON: `options.ambiente` → `request.ambiente` → `restrita`.
|
|
167
|
+
- HTTP 2xx sem XML autorizado e chave de 50 dígitos consistente gera
|
|
168
|
+
`EmitirNotaError` com `INVALID_AUTHORIZATION_RESPONSE`. Concilie antes de
|
|
169
|
+
reenviar (veja [Recuperar uma emissão](recuperar-emissao.md)).
|
|
170
|
+
|
|
171
|
+
Exemplos testados: [`examples/payloads.ts`](../../examples/payloads.ts) traz um
|
|
172
|
+
cenário nacional e um de exportação.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Recuperar uma emissão após timeout
|
|
2
|
+
|
|
3
|
+
`POST /nfse` não é idempotente, por isso o SDK **nunca** repete uma emissão
|
|
4
|
+
automaticamente. Se a conexão cair sem resposta, a nota pode ter sido
|
|
5
|
+
autorizada ou não. Pergunte à SEFIN antes de reenviar:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { prepararNota, recuperarNfsePorDps, transmitirNotaPreparada } from '@useinvio/nfse-sdk';
|
|
9
|
+
|
|
10
|
+
const nota = prepararNota(dps, certificado); // persista nota.dpsId e nota.signedXml antes de enviar
|
|
11
|
+
try {
|
|
12
|
+
return await transmitirNotaPreparada(nota, certificado, { timeoutMs: 30_000 });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error instanceof Error && error.name === 'EmitirNotaError') throw error; // rejeição real
|
|
15
|
+
const existente = await recuperarNfsePorDps(nota.dpsId, certificado, nota.ambiente, { retries: 3 });
|
|
16
|
+
return existente ?? transmitirNotaPreparada(nota, certificado); // null: a DPS não chegou à SEFIN
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`recuperarNfsePorDps` chama `GET /dps/{id}` e, se a DPS foi processada,
|
|
21
|
+
`GET /nfse/{chave}`. O resultado é o mesmo de [`obterNfse`](consultar.md):
|
|
22
|
+
|
|
23
|
+
| Resultado | Significado | O que fazer |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `NfseConsultada` | A nota foi autorizada antes da queda. | Use o resultado; não reenvie. |
|
|
26
|
+
| `null` | A SEFIN não conhece a DPS. | Reenvie a **mesma** DPS assinada (mesmo `Id`). |
|
|
27
|
+
| `ConsultaNfseError` | A consulta falhou. | Tente de novo mais tarde; não reenvie às cegas. |
|
|
28
|
+
|
|
29
|
+
Reenvie sempre a mesma DPS assinada, nunca uma DPS nova: o `Id` (prestador,
|
|
30
|
+
série e número) identifica a DPS na SEFIN, e um número novo geraria outra nota.
|
|
31
|
+
|
|
32
|
+
No cliente: `client.invoices.recover(dpsId)`. Exemplo completo em
|
|
33
|
+
[`examples/recover-emission.ts`](../../examples/recover-emission.ts).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Tratar rejeições e erros
|
|
2
|
+
|
|
3
|
+
Cada etapa lança um erro tipado. Use `instanceof` para decidir o que fazer.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import {
|
|
7
|
+
CertificateValidationError,
|
|
8
|
+
DpsFiscalValidationError,
|
|
9
|
+
DpsXsdValidationError,
|
|
10
|
+
EmitirNotaError,
|
|
11
|
+
emitirNfse,
|
|
12
|
+
} from '@useinvio/nfse-sdk';
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await emitirNfse(nota, certificado);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof DpsFiscalValidationError) {
|
|
18
|
+
// JSON inválido antes de gerar XML: corrija a entrada
|
|
19
|
+
for (const issue of error.issues) console.log(issue.code, issue.path, issue.message);
|
|
20
|
+
} else if (error instanceof DpsXsdValidationError) {
|
|
21
|
+
// XML viola o XSD oficial: error.details traz o diagnóstico do libxml2
|
|
22
|
+
} else if (error instanceof CertificateValidationError) {
|
|
23
|
+
// senha, leitura ou vigência do A1: error.code
|
|
24
|
+
} else if (error instanceof EmitirNotaError) {
|
|
25
|
+
// a SEFIN respondeu com rejeição (ou com resposta 2xx incompleta)
|
|
26
|
+
for (const rejeicao of error.erros) {
|
|
27
|
+
console.log(rejeicao.Codigo, rejeicao.Descricao, rejeicao.Complemento);
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
throw error; // rede, timeout, XsdEngineUnavailableError: o resultado é incerto
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Rejeição da SEFIN ou resultado incerto?
|
|
36
|
+
|
|
37
|
+
| Erro | A nota foi emitida? | Ação |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `DpsFiscalValidationError`, `DpsXsdValidationError`, `CertificateValidationError` | Não: nada foi enviado. | Corrija a entrada ou o certificado. |
|
|
40
|
+
| `EmitirNotaError` com `status` 4xx | Não. | Corrija conforme `erros` e envie de novo. |
|
|
41
|
+
| `EmitirNotaError` com `INVALID_AUTHORIZATION_RESPONSE` | Talvez. | [Recupere pela DPS](recuperar-emissao.md) antes de reenviar. |
|
|
42
|
+
| Erro de rede ou timeout | Talvez. | [Recupere pela DPS](recuperar-emissao.md) antes de reenviar. |
|
|
43
|
+
| `XsdEngineUnavailableError` | Não: falha local de infraestrutura. | Tente de novo; não é rejeição fiscal. |
|
|
44
|
+
|
|
45
|
+
## Explicar um código
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { explicarErros, explicarRejeicao } from '@useinvio/nfse-sdk';
|
|
49
|
+
|
|
50
|
+
explicarRejeicao('E0014');
|
|
51
|
+
// { codigo: 'E0014', conhecida: true, mensagemOficial: 'Conjunto de Série, Número, ...',
|
|
52
|
+
// regra: '...', campo: 'NFSe/infNFSe/DPS/infDPS/serie', nivel: 1, fonte: { documento, url } }
|
|
53
|
+
|
|
54
|
+
explicarErros(error.erros); // uma explicação por erro de EmitirNotaError ou RegistrarEventoError
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
O catálogo tem as regras de rejeição dos anexos oficiais publicados em
|
|
58
|
+
gov.br/nfse (Anexo I v1.01 de 09/02/2026, DPS e NFS-e; Anexo II v1.01 de
|
|
59
|
+
22/01/2026, eventos), com o texto oficial sem alterações. Um código fora do
|
|
60
|
+
catálogo volta com `conhecida: false`: não há causa inventada.
|
|
61
|
+
`buscarRejeicao(codigo)` e `listarRejeicoes()` dão acesso direto às regras.
|
|
62
|
+
Cada código também tem uma página: [docs/rejeicoes](../rejeicoes/README.md). No
|
|
63
|
+
terminal: `npx nfse explain E0014`.
|
|
64
|
+
|
|
65
|
+
## Normalizar o corpo da resposta
|
|
66
|
+
|
|
67
|
+
`extrairErros(body)` normaliza os formatos de erro da SEFIN (`erros`, `Erros`,
|
|
68
|
+
`erro`, objeto único) para `{ Codigo, Descricao, Complemento? }`.
|
|
69
|
+
|
|
70
|
+
A lista completa de classes e códigos está em [Erros](../referencia/erros.md).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Testar sem certificado e sem rede
|
|
2
|
+
|
|
3
|
+
`@useinvio/nfse-sdk/testing` traz duas peças para testar a sua integração de
|
|
4
|
+
ponta a ponta, no CI, sem certificado A1 real e sem chamar a SEFIN:
|
|
5
|
+
|
|
6
|
+
- `createTestCertificate()`: um certificado A1 autoassinado com o CNPJ (ou CPF)
|
|
7
|
+
nos mesmos campos ICP-Brasil de um certificado real;
|
|
8
|
+
- `installMockSefin()`: uma SEFIN e um ADN simulados dentro do processo. Toda
|
|
9
|
+
chamada do SDK é respondida localmente até `restore()`.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import test from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { cancelarNfse, emitirNfse, obterNfse } from '@useinvio/nfse-sdk';
|
|
15
|
+
import { createTestCertificate, installMockSefin } from '@useinvio/nfse-sdk/testing';
|
|
16
|
+
|
|
17
|
+
const { certificate } = createTestCertificate({ cnpj: '12345678000195' });
|
|
18
|
+
|
|
19
|
+
test('emite e cancela', async (t) => {
|
|
20
|
+
const mock = installMockSefin();
|
|
21
|
+
t.after(() => mock.restore());
|
|
22
|
+
|
|
23
|
+
const { chaveAcesso } = await emitirNfse(minhaNota, certificate);
|
|
24
|
+
const { resumo } = await obterNfse(chaveAcesso, certificate);
|
|
25
|
+
assert.equal(resumo.emitente.CNPJ, '12345678000195');
|
|
26
|
+
|
|
27
|
+
await cancelarNfse({ chaveAcesso, cMotivo: '1', xMotivo: 'Teste de cancelamento' }, certificate);
|
|
28
|
+
assert.equal(mock.notas.get(chaveAcesso)?.cancelada, true);
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## O que a SEFIN simulada verifica
|
|
33
|
+
|
|
34
|
+
- A DPS e o pedido de evento contra o XSD oficial (como a SEFIN, responde `E1235`).
|
|
35
|
+
- A assinatura XMLDSIG com o certificado da chamada (`E0714`).
|
|
36
|
+
- O ambiente do XML contra o ambiente de envio (`E0006`, `E1845`).
|
|
37
|
+
- DPS repetida (`E0014`) e cancelamento repetido (`E0840`, `E0845`).
|
|
38
|
+
|
|
39
|
+
As mensagens vêm do catálogo oficial, como na SEFIN. A simulação **não** aplica
|
|
40
|
+
regras fiscais nem municipais: uma nota aceita pelo mock ainda pode ser rejeitada
|
|
41
|
+
na produção restrita.
|
|
42
|
+
|
|
43
|
+
Rotas simuladas: emissão, consulta por chave e por DPS, registro e consulta de
|
|
44
|
+
eventos, DANFSe (um PDF mínimo), distribuição por NSU, eventos por chave e
|
|
45
|
+
parâmetros municipais (respostas genéricas).
|
|
46
|
+
|
|
47
|
+
## Simular falhas
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
mock.rejectNext('E0312'); // próxima chamada rejeitada com a mensagem oficial do código
|
|
51
|
+
mock.rejectNext([{ Codigo: 'E0001', Descricao: '...' }], { status: 400, path: /^\/nfse$/ });
|
|
52
|
+
mock.failNext(); // erro de rede antes de a SEFIN processar
|
|
53
|
+
mock.dropResponseNext(); // a SEFIN processa, mas a resposta se perde
|
|
54
|
+
mock.route('GET', /^\/parametrizacao\/.+\/aliquota$/, () => ({ status: 200, body: '{"aliquota":2}' }));
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`dropResponseNext` é o cenário de [recuperação após timeout](recuperar-emissao.md):
|
|
58
|
+
a nota existe na SEFIN, mas a sua aplicação não recebeu a chave.
|
|
59
|
+
|
|
60
|
+
## Inspecionar
|
|
61
|
+
|
|
62
|
+
- `mock.notas`: notas emitidas, por chave (`nfseXml`, `dpsId`, `cancelada`, `eventos`).
|
|
63
|
+
- `mock.requests`: cada chamada recebida, com método, host, caminho e status.
|
|
64
|
+
|
|
65
|
+
## Certificados de teste
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
createTestCertificate({ cpf: '39053344705' }); // e-CPF
|
|
69
|
+
createTestCertificate({ validDays: -1 }); // vencido: testa CERTIFICATE_EXPIRED
|
|
70
|
+
const { pfx, password } = createTestCertificate(); // bytes do PFX para testar fromPfx/fromEnv
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
O certificado é autoassinado e não é ICP-Brasil: a SEFIN real o recusa.
|
|
74
|
+
|
|
75
|
+
## No servidor MCP
|
|
76
|
+
|
|
77
|
+
`NFSE_MCP_MOCK=1` liga a mesma SEFIN simulada dentro do servidor MCP, para
|
|
78
|
+
experimentar as ferramentas de emissão sem certificado. Veja
|
|
79
|
+
[Agentes de IA e MCP](agentes-ia.md).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Timeouts, novas tentativas e cancelamento
|
|
2
|
+
|
|
3
|
+
Toda função de rede aceita um último argumento `SefinRequestOptions`:
|
|
4
|
+
|
|
5
|
+
| Opção | Padrão | Efeito |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `timeoutMs` | `60000` | Tempo máximo de cada requisição. |
|
|
8
|
+
| `retries` | `0` | Novas tentativas com backoff exponencial (0,5 s, 1 s, 2 s, até 4 s, com jitter). |
|
|
9
|
+
| `signal` | – | `AbortSignal` que cancela a requisição e as tentativas pendentes. |
|
|
10
|
+
|
|
11
|
+
`retries` vale só para GETs e só para falhas transitórias: erro de rede e HTTP
|
|
12
|
+
429, 502, 503 ou 504. Emissão (`POST /nfse`) e eventos nunca são repetidos,
|
|
13
|
+
mesmo que você passe `retries`, porque não são idempotentes. Para emissão, veja
|
|
14
|
+
[Recuperar uma emissão após timeout](recuperar-emissao.md).
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { obterNfse } from '@useinvio/nfse-sdk';
|
|
18
|
+
|
|
19
|
+
const nota = await obterNfse(chaveAcesso, certificado, 'producao', {
|
|
20
|
+
timeoutMs: 15_000,
|
|
21
|
+
retries: 3,
|
|
22
|
+
signal: AbortSignal.timeout(60_000),
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
No cliente, configure uma vez para todas as chamadas:
|
|
27
|
+
`new NfseClient({ ..., request: { timeoutMs: 30_000, retries: 2 } })`.
|
|
28
|
+
|
|
29
|
+
## Métricas de latência
|
|
30
|
+
|
|
31
|
+
Com `NFSE_SEFIN_LATENCY_METRICS=1`, o SDK emite uma métrica por requisição
|
|
32
|
+
(operação, ambiente, status e duração) para o observador registrado em
|
|
33
|
+
`setSefinRequestObserver`. O guia completo está em [METRICS.md](../METRICS.md).
|