# NFC-e off-line (PDV)

import { Tabs, TabsList, TabsTrigger, TabsContent } from "zudoku/ui/Tabs.js";

O Agente sobe no caixa uma **API HTTP local com o mesmo contrato da API do Brasil NFe**. O sistema de vendas passa a chamar `http://127.0.0.1:9155/services/Fiscal/EnviarNotaFiscal` com o mesmo JSON de sempre:

- **Servidor do Brasil NFe alcançável**: a nota é encaminhada e autorizada online, como hoje. A resposta volta idêntica.
- **Servidor inalcançável**: o Agente monta e assina a NFC-e em **contingência off-line (tpEmis 9)** no próprio caixa, gera QR Code off-line e DANFE, guarda a nota e responde em cerca de um segundo. Quando o servidor volta, a nota sobe sozinha e o Brasil NFe transmite à SEFAZ.

"Off-line" aqui significa **o servidor do Brasil NFe não responde** (internet da loja caiu, por exemplo). Se a internet está ok e é a **SEFAZ** que está fora, quem trata a contingência é o servidor, como já faz hoje ([Contingência](/conceitos-fiscais/contingencia)). Nos dois casos o caixa não para.

## Ativação

Não há o que ativar: **toda empresa pareada emite off-line** com o mesmo código de pareamento, com **série padrão 500**, ambiente **produção** e contador de números local. Nada precisa ser cadastrado no painel. Ajustes (série, ambiente, porta, escuta, origens do navegador, modo) ficam na tela **NFC-e › Configuração**, no comando `nfce config` ou no console do painel.

Para o modo off-line de fato funcionar, o Agente precisa ter conseguido **conectar ao servidor pelo menos uma vez** depois do pareamento, para baixar a configuração fiscal da empresa e o pacote fiscal (ver [Sincronização](#sincronização-e-pacote-fiscal)). `nfce status` (ou a tela Situação) mostra os dois itens.

## Integração do PDV

### Requisição

```http
POST http://127.0.0.1:9155/services/Fiscal/EnviarNotaFiscal
Content-Type: application/json
X-Empresa-CNPJ: 12345678000195
```

| Item | Regra |
| --- | --- |
| Base URL | `http://127.0.0.1:9155` no próprio caixa, ou `http://IP-DO-SERVIDOR-DA-LOJA:9155` quando um Agente atende vários caixas. Porta configurável. |
| Corpo | O mesmo `NotaFiscalEnvio` da API ([referência](/api)), com `ModeloDocumento` 65. |
| Token | **Não envie** `Token`/`UserToken`. O PDV não tem e não precisa: ao encaminhar online o Agente se identifica no servidor pelo código de pareamento e o servidor chama a API com o token da empresa por dentro. |
| `X-Empresa-CNPJ` (ou `?cnpj=`) | Obrigatório só quando o Agente tem **mais de uma** empresa pareada. Com uma só, é opcional. CNPJ sem máscara. |
| `Serie`, `Numero`, `Codigo` (cNF) | **Preenchidos pelo Agente**, em todo envio (online e off-line). O que o PDV mandar nesses campos é ignorado. Assim o caixa nunca colide com a numeração online do painel/ERP. |
| `TipoAmbiente` | Vale o da nota, se vier; senão o ambiente da empresa no Agente (padrão 1, produção). |
| `ModeloDocumento` diferente de 65 | Só encaminhado online (NF-e, por exemplo). Sem servidor, retorna erro: "apenas NFC-e pode ser emitida offline". |
| Concorrência | O Agente emite **uma nota por vez** (numeração sequencial). Vários caixas podem chamar ao mesmo tempo; as requisições são enfileiradas. |

### Resposta

O mesmo `NotaFiscalRetorno` da API. **Online**, é exatamente o que o servidor devolveu. **Off-line**, o Agente responde:

```json
{
  "ReturnNF": {
    "Numero": 128,
    "Serie": 500,
    "ChaveNF": "35260912345678000195650050000001281234567801",
    "NumeroProtocolo": "",
    "CodTipoAmbiente": 1,
    "DsTipoAmbiente": "Produção",
    "CodStatusRespostaSefaz": 100,
    "DsStatusRespostaSefaz": "NFC-e emitida em contingência off-line; será transmitida quando a conexão voltar",
    "Ok": true,
    "Detalhes": { "valorNf": 89.90, "valorIcms": 0, "valorIpi": 0, "valorPis": 0, "valorCofins": 0 }
  },
  "Base64Xml": "PE5GZSB4bWxucz0iaHR0cDovL3d3dy5wb3J0YWxmaXNjYWwuaW5mLmJyL25mZSI+...",
  "Base64File": "PCFET0NUWVBFIGh0bWw+PGh0bWw+...",
  "Error": "",
  "Avisos": ["Emitida off-line (contingência) pelo BrasilNFe Agent; aguardando transmissão à SEFAZ."]
}
```

Como o PDV reconhece a emissão off-line e o que fazer com ela:

| Campo | Off-line | Ação no PDV |
| --- | --- | --- |
| `ReturnNF.Ok` / `CodStatusRespostaSefaz` | `true` / `100` | Tratar como nota emitida: fechar a venda. |
| `NumeroProtocolo` | vazio | Ainda não há protocolo; ele chega depois da transmissão (consulte pelo painel, pela API ou pela chave). |
| `Base64Xml` | XML da NFC-e **assinado**, com `tpEmis=9`, `dhCont` e `xJust` | Guardar como o XML da nota; é o mesmo que será transmitido. |
| `Base64File` | **DANFE NFC-e em HTML** (não PDF), com o aviso de contingência e o QR Code off-line | Decodificar e renderizar/imprimir no navegador ou no componente HTML da impressora. Se o seu fluxo online espera PDF, trate os dois formatos (o conteúdo começa com `<!DOCTYPE html>`). |
| `Avisos[]` | Mensagem de emissão off-line | Opcional: mostrar ao operador. |

Erros de validação chegam como na API: HTTP 200 com `Error` preenchido e `ReturnNF.Ok=false` (nesse caso o número **não** é consumido). Requisição de navegador cuja origem não está autorizada recebe **HTTP 403** com `Error` explicando.

### Exemplos

<Tabs defaultValue="curl">
  <TabsList>
    <TabsTrigger value="curl">cURL</TabsTrigger>
    <TabsTrigger value="js">JavaScript (PDV web)</TabsTrigger>
    <TabsTrigger value="csharp">.NET</TabsTrigger>
    <TabsTrigger value="python">Python</TabsTrigger>
  </TabsList>

  <TabsContent value="curl">

```sh
curl -s http://127.0.0.1:9155/services/Fiscal/EnviarNotaFiscal \
  -H 'Content-Type: application/json' \
  -H 'X-Empresa-CNPJ: 12345678000195' \
  -d '{
    "ModeloDocumento": 65,
    "NaturezaOperacao": "Venda ao consumidor",
    "IndicadorPresenca": 1,
    "ConsumidorFinal": true,
    "Produtos": [{ "NmProduto": "Produto Exemplo", "NCM": "61091000", "CFOP": 5102, "Quantidade": 1, "ValorUnitario": 89.90, "Imposto": { "ICMS": { "CSOSN": 102 } } }],
    "Pagamentos": [{ "FormaPagamento": 1, "Valor": 89.90 }]
  }'

curl -s http://127.0.0.1:9155/status
```

  </TabsContent>

  <TabsContent value="js">

```js
// A origem desta página precisa estar autorizada no Agente:
//   brasilnfe-agent nfce config --origem https://pdv.suaempresa.com.br
const AGENTE = "http://127.0.0.1:9155";

async function emitirNfce(nota) {
  const r = await fetch(`${AGENTE}/services/Fiscal/EnviarNotaFiscal`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Empresa-CNPJ": "12345678000195" },
    body: JSON.stringify({ ...nota, ModeloDocumento: 65 }),
  });
  if (r.status === 403) throw new Error("origem do PDV não autorizada no Agente");
  const ret = await r.json();
  if (!ret.ReturnNF?.Ok) throw new Error(ret.Error || ret.ReturnNF?.DsStatusRespostaSefaz);

  const offline = !ret.ReturnNF.NumeroProtocolo;
  const danfe = atob(ret.Base64File);                 // HTML off-line; PDF quando online
  if (danfe.startsWith("<!DOCTYPE html")) {
    const w = window.open("", "_blank"); w.document.write(danfe); w.document.close(); w.print();
  } else {
    // PDF em Base64: abrir como data URL ou enviar ao componente de impressão
  }
  return { chave: ret.ReturnNF.ChaveNF, numero: ret.ReturnNF.Numero, serie: ret.ReturnNF.Serie, offline };
}
```

  </TabsContent>

  <TabsContent value="csharp">

```csharp
using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:9155/") };
http.DefaultRequestHeaders.Add("X-Empresa-CNPJ", "12345678000195");

var nota = new NotaFiscalEnvio { ModeloDocumento = 65, /* ... */ };   // SDK BrasilNFe (NuGet), sem Serie/Numero
var resp = await http.PostAsJsonAsync("services/Fiscal/EnviarNotaFiscal", nota);
var ret = await resp.Content.ReadFromJsonAsync<NotaFiscalRetorno>();

if (ret.ReturnNF.Ok)
{
    bool offline = string.IsNullOrEmpty(ret.ReturnNF.NumeroProtocolo);
    byte[] danfe = Convert.FromBase64String(ret.Base64File);   // HTML se offline, PDF se online
    // imprimir / guardar XML (ret.Base64Xml)
}
```

  </TabsContent>

  <TabsContent value="python">

```python
import base64, requests

AGENTE = "http://127.0.0.1:9155"
ret = requests.post(f"{AGENTE}/services/Fiscal/EnviarNotaFiscal",
                    json={**nota, "ModeloDocumento": 65},
                    headers={"X-Empresa-CNPJ": "12345678000195"}, timeout=130).json()

if ret["ReturnNF"]["Ok"]:
    offline = not ret["ReturnNF"]["NumeroProtocolo"]
    danfe = base64.b64decode(ret["Base64File"])      # HTML se offline, PDF se online
    xml = base64.b64decode(ret["Base64Xml"])
```

  </TabsContent>
</Tabs>

Use timeout de pelo menos **130 s** no cliente: online, o Agente espera a resposta do servidor por até 120 s (conexão em 4 s); só depois cai para o off-line.

### `GET /status`

Devolve em JSON a situação completa (o mesmo de `nfce status`): API local ativa, servidor alcançável, modo, porta/escuta, origens, pacote fiscal instalado, versão do Agente, backup e, por empresa, série, próximo número, ambiente, configuração sincronizada e contagem da fila (`pendentes`, `enviadas`, `transmitidas`, `rejeitadas`, `pendenteMaisAntigaS`). Útil para o PDV mostrar um indicador "modo off-line" ao operador.

## Origens do navegador (PDV web / PWA)

A API local aceita chamadas de páginas web **somente** das origens listadas em `origens` (esquema + host, opcionalmente porta). O CORS sozinho só impede a leitura da resposta; o Agente também checa o header `Origin` e recusa com **403** o que não está na lista. Sem origem configurada, nenhuma página web consegue chamar. Clientes que não enviam `Origin` (sistemas desktop, `curl`, serviços) passam sem essa etapa.

```sh
brasilnfe-agent nfce config --origem https://pdv.suaempresa.com.br     # acumula; repita para várias
brasilnfe-agent nfce config --origem http://localhost:5173              # desenvolvimento
brasilnfe-agent nfce config --sem-origens                               # limpa a lista
```

Na tela: NFC-e › Configuração › "Origens do navegador autorizadas" (uma por linha). No painel: `config --origem ...`.

## Série, numeração e ambiente

- **Série padrão 500** por empresa e por Agente; válidas de 1 a 889 (890 em diante são reservadas ao Fisco). Use uma série **diferente** das que o painel/ERP usa online para a mesma empresa. Não é preciso cadastrar a série no painel.
- O **contador fica no Agente** e vale para online e off-line: o Agente preenche `Serie`/`Numero`/`Codigo` em todo envio. Série nova começa em 1.
- Um número só é **consumido** quando a nota foi aceita pelo servidor (autorizada, denegada ou registrada como duplicidade) ou ficou na fila off-line. Rejeição de validação devolve o número, como no servidor.
- **Próximo número** ajustável (caixa novo assumindo uma série que já emitiu): tela, `nfce config --numero N` ou painel. **Só sobe**: nunca abaixo do que já foi consumido.
- **Ambiente**: `TipoAmbiente` da nota, se vier; senão o da empresa no Agente (padrão produção; `nfce config --ambiente 2` para homologação). Nota de homologação emitida off-line sobe para o servidor, mas **a rotina de contingência só transmite produção**: ela fica gravada sem transmissão. Para homologar o fluxo off-line inteiro use o modo de contingência forçada em produção com valores baixos, ou valide em homologação apenas a emissão/impressão local.

## Modos: automático e contingência forçada

| Modo | Comportamento | Quando usar |
| --- | --- | --- |
| `auto` (padrão) | Encaminha ao servidor; emite off-line só quando ele não responde. | Operação normal. |
| `contingencia` | Toda NFC-e sai off-line (tpEmis 9) **mesmo com o servidor no ar**, entra na fila e o Brasil NFe transmite. Documentos que não são NFC-e continuam encaminhados online. | Homologar o fluxo ponta a ponta (emissão local, impressão do DANFE off-line, subida da fila, transmissão) sem derrubar a rede. Desligue depois. |

Tela: NFC-e › Configuração › "Sempre emitir em contingência (modo de teste)". CLI: `nfce config --modo contingencia` / `--modo auto`. O painel destaca "contingência forçada" na situação do agente enquanto o modo está ativo.

## Sincronização e pacote fiscal

Enquanto a API local está no ar, a cada **30 s** o Agente:

1. **Testa o servidor**. Sem resposta, marca o servidor como inalcançável; o próximo envio do PDV vai direto para o off-line, sem esperar timeout.
2. **Atualiza a configuração fiscal** de cada empresa a cada 6 h: os dados fiscais necessários à emissão (empresa, UF, tabelas, CSC do QR Code). **Sem senhas, sem tokens, sem código de pareamento**: o servidor remove esses campos antes de enviar.
3. **Baixa/atualiza o pacote fiscal** quando o servidor publica versão nova (verificado a cada 6 h; ~45 MB). Antes de ativar, o Agente confere a **assinatura digital** e o **hash de cada arquivo** e faz um autoteste. A versão anterior fica no disco.
4. **Sobe a fila**: cada nota pendente vai para o servidor com o JSON original e o XML assinado. O servidor valida (NFC-e em contingência, emitente = empresa pareada, assinatura do XML) e grava a nota **como contingência**, exatamente como uma NFC-e que caiu em contingência no próprio servidor; reenviar a mesma nota não duplica.
5. **Acompanha** as notas já no servidor: a rotina de contingência do Brasil NFe transmite à SEFAZ em poucos minutos e o Agente marca "transmitida (protocolo)" ou "rejeitada pela SEFAZ (motivo)".
6. **Alerta** (log, tela e painel) quando há nota pendente há mais de **12 h** sem chegar ao servidor: o prazo legal de transmissão da NFC-e em contingência off-line é curto (24 h na maioria das UFs). Verifique a conexão.
7. Faz o **backup** do banco de notas quando ele mudou (ver abaixo).

O que é o pacote fiscal (aparece como "sidecar" na tela e no `nfce status`): um componente publicado pelo Brasil NFe com **as mesmas regras fiscais do servidor**. Validações e mudanças de leiaute chegam ao caixa junto com a atualização do servidor, sem uma segunda implementação. Ele **não tem acesso ao certificado**: cada assinatura é pedida ao Agente, com autorização de uso único para a nota em curso. Também é ele que gera o DANFE a partir do XML na reimpressão (o HTML não fica guardado) e que valida/reassina XML corrigido.

## Estados das notas

| Estado | Significado |
| --- | --- |
| **Pendente** (no caixa) | Emitida off-line, ainda não entregue ao servidor. Sobe na próxima sincronização. |
| **Enviada** (no servidor, aguardando SEFAZ) | Entregue ao Brasil NFe; o job de contingência vai transmitir. |
| **Transmitida** | SEFAZ autorizou; há protocolo. A nota aparece na listagem normal do painel e na API como qualquer outra. |
| **Rejeitada** | O servidor recusou a subida (o motivo fica na nota; corrija e reenvie) ou a SEFAZ rejeitou a transmissão (cStat e motivo; trate pelo painel como uma rejeição de contingência). |

Onde ver e agir:

- **Tela NFC-e › Notas**: filtros por estado, DANFE (regenerado do XML, com impressão), XML (download), Reenviar, Corrigir JSON, Corrigir XML.
- **CLI**: `nfce notas [pendentes|enviadas|transmitidas|rejeitadas|todas] [N]`, `nfce nota CHAVE`, `nfce nota CHAVE danfe --out danfe.html`, `nfce nota CHAVE xml --out nota.xml`, `nfce nota CHAVE json --out nota.json`, `nfce nota CHAVE reenviar`.
- **Painel › aba Agente › Console**: os mesmos comandos, de longe; o DANFE abre em nova aba.

Nada é apagado do banco local: o histórico fica no caixa para reimpressão.

Cancelamento, inutilização e demais **eventos** de uma nota emitida off-line são feitos **depois da transmissão**, pela API ou pelo painel, como para qualquer NFC-e.

## Corrigir uma nota com erro

Vale para nota que **ainda está no caixa** (pendente ou recusada na subida). Nota que já chegou ao servidor se corrige lá, como hoje (Configurações › Banco de dados › Reassinar). A correção **não muda a identidade da nota**: série, número, cNF, modelo e ambiente são mantidos; totais, chave e QR Code são recalculados. Série/número diferentes do original são recusados.

- **Por JSON (recomendado)**: edite o JSON original da venda e o Agente **regera** a NFC-e pelo pacote fiscal, com as mesmas regras da emissão. Tela: "Corrigir JSON". CLI: `nfce nota CHAVE json --out nota.json`, edite, `nfce nota CHAVE corrigir --arquivo nota.json` (ou pelo stdin). Painel: comando `nota`, argumentos `CHAVE corrigir`, JSON colado no campo grande.
- **Por XML**: edite o XML assinado e o Agente **valida contra o schema e reassina**. Elemento inválido é recusado com a mensagem do schema. Tela: "Corrigir XML". CLI/painel: mesmos comandos com `nota.xml`.

A nota corrigida volta como pendente e sobe na próxima sincronização. Só o Agente (tela, CLI, painel) corrige: o sistema de vendas não tem acesso a essa operação.

## Backup e recuperação (não perder nota)

Três camadas em volta do banco local de notas:

1. **Arquivo por nota**: junto com a gravação no banco, cada NFC-e off-line vira um arquivo em `nfce/notas/` (JSON original + XML assinado). Arquivos de notas transmitidas há mais de 90 dias são apagados; o banco e os backups guardam o histórico.
2. **Backup do banco**: cópia consistente em `Documentos\BrasilNFe Agente\backup-nfce` (Windows; costuma cair no OneDrive) ou `nfce/backup` (sem pasta Documentos, como no Linux headless), sempre que o banco mudou e passaram 5 min do último, com **30 cópias rotativas**.
3. **Verificação no início**: banco corrompido é posto de lado, o backup íntegro mais novo volta e os arquivos por nota reimportam o que faltar como pendentes (reenviar não duplica). A numeração é realinhada pelo maior número encontrado: **nunca volta para trás**, então a próxima nota não repete número.

`nfce status` e a tela Situação mostram a pasta, a quantidade e a hora do último backup. Milhares de notas ocupam alguns MB.

## Requisitos do JSON para a emissão off-line

O pacote fiscal aplica as mesmas validações do servidor, mas **não consulta o banco** do Brasil NFe. Por isso:

- Envie o bloco `Imposto` **completo por item** (a "macro de tributação" cadastrada no painel, `CodTributacao`, não está na configuração sincronizada).
- Se a nota tem cliente com endereço, informe `Cliente.Endereco.CodMunicipio` (o pacote não faz a normalização de endereço que o servidor faz).
- Tudo o que valeria online vale off-line: NCM, CFOP, pagamentos, CSC configurado na empresa. Erros voltam em `Error`, o número não é consumido e a venda deve ser corrigida e reenviada.

## Checklist de homologação

1. Pareie a empresa, abra a tela NFC-e e confirme na Situação: API local **no ar**, servidor **alcançável**, pacote fiscal **verificado**, configuração da empresa **sincronizada**.
2. Se o PDV roda no navegador, autorize a origem e confirme que uma chamada de teste não recebe 403.
3. Emita uma NFC-e online pela API local e confira no painel que ela saiu com a **série do Agente** (padrão 500).
4. Ligue `--modo contingencia`, emita uma NFC-e, imprima o `Base64File` (HTML) e confira o aviso de contingência e o QR Code. Veja a nota como **pendente**, depois **enviada** e, em poucos minutos, **transmitida** com protocolo.
5. Desligue o modo de contingência (`--modo auto`).
6. Simule a queda: bloqueie a saída para `api.brasilnfe.com.br` no firewall, emita, veja a resposta em ~1 s, libere a rede e acompanhe a fila esvaziar.

## Limitações conhecidas

- Só **NFC-e (modelo 65)** tem caminho off-line. NF-e e os demais documentos passam pela API local apenas quando o servidor está no ar.
- Nota de **homologação** emitida off-line não é transmitida pela rotina de contingência (só produção).
- Uma **empresa por Agente por vez**; vários caixas da mesma empresa usam um servidor de loja.
- O DANFE off-line é **HTML**, não PDF.
- O prazo legal de transmissão da contingência off-line (24 h na maioria das UFs) é responsabilidade do emitente: mantenha o Agente ligado e a rede monitorada; o alerta de 12 h existe para isso.
