> For the complete documentation index, see [llms.txt](https://docs.skail.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.skail.dev/quickstart/mais-exemplos-praticos/emissao-de-nota-fiscal-de-servico.md).

# Emissão de nota fiscal de serviço

Este projeto é um template de demonstração de como emitir NFS-e automaticamente usando a API da [Nota Gateway](https://notagateway.com.br/), orquestrada pelo skail.

Esse exemplo cobre:&#x20;

* Emissão
* Retorno via webhook
* Consulta por polling
* Revisão manual em caso de pendência (demora)
* Postgres, worker .NET (runtime skail), BFF Node.js e frontend React.&#x20;
* Rodando com Docker Compose.&#x20;

> O fluxo principal está em `src/Worker.cs`. É nele que a lógica de orquestração vive.

\ <i class="fa-download">:download:</i>   [Clique aqui](https://drive.google.com/file/d/13jgtE84sjbB6GYy0q8genlP-RqY5O-hx/view?usp=sharing) para baixar o projeto completo.<br>

## O fluxo principal

```csharp
[SkailFunction]
public async SkailTask EmitirNfseAsync(NfseEmissaoRequest request)
{
    var nfse = await EnviarNotaGateway(request);

    // Cria o evento para aguardar o retorno do NotaGateway
    var retornoNotaGateway = SkailTask.WaitForEvent<NfseResponse>(
        "EVENTO_AGUARDANDO_RETORNO", request.IdExterno
    );

    // Configura um limite de tempo para aguardar o retorno
    var timeout = SkailTask.DelayMinutes(3);

    // Aguarda o retorno OU o timeout — o que ocorrer primeiro
    var retornoOuTimeout = await SkailTask.WhenAny(retornoNotaGateway, timeout);

    if (retornoOuTimeout == retornoNotaGateway)
    {
        nfse = await retornoNotaGateway;
    }
    else if (retornoOuTimeout == timeout)
    {
        nfse = await ConsultarNfseNotaGateway(request.IdExterno);
    }

    await SalvarNotaAsync(nfse);

    if (nfse.Status != "Autorizada")
        await AguardarRevisaoNfseAsync(nfse);
}
```

Só de ler o código você já entende facilmente toda a jornada de emissão que contempla desde o envio inicial até a resolução final da nota, incluindo espera, fallback e revisão manual.<br>

**O que importa aqui:**

* **A combinação de** **`WaitForEvent` + `DelayMinutes` + `WhenAny`** — resolve um problema clássico: esperar uma resposta externa sem travar o sistema ("espera com timeout"). A execução hiberna (sem consumir recurso) até a Nota Gateway responder via webhook **OU** até passar 3 minutos — o que acontecer primeiro.<br>
* **Se o webhook chegar primeiro**, a nota já vem pronta. Se o timeout vencer, o fluxo segue para a consulta (polling), sem precisar de lógica paralela ou controle manual.<br>
* **Se a nota não for autorizada**, o fluxo entra em espera (hiberna) novamente (`AguardarRevisaoNfseAsync`) — dessa vez aguardando uma correção manual. Quando alguém revisa e reenvia, a execução continua exatamente de onde parou.

Tudo isso acontece sem manter processos ativos, sem filas e sem você precisar controlar “em que etapa está” manualmente (em banco, flags, status, etc.).

<mark style="background-color:$success;">E se qualquer falha ocorrer no meio do caminho, o skail retoma do ponto exato de onde parou — sem repetir o que já foi executado e sem gerar duplicidade.</mark><br>

{% hint style="info" icon="lightbulb" %}
**O que é “hibernação” no skail?**

[Hibernação](https://docs.skailhq.com/conceitos/fundamentos/hibernacao-e-retomada-resumir) significa que a execução “pausa de verdade”.\
\
Ela não fica rodando em background, não consome CPU e nem mantém worker/thread ativa.\
\ <mark style="background-color:cyan;">O estado com todo o contexto é salvo, e o skail simplesmente “desliga” a execução naquele ponto.</mark><br>

Quando algo acontece (como um webhook chegando) ou o tempo passa, ela é reativada e continua exatamente de onde parou — como se nunca tivesse sido interrompida.
{% endhint %}

## Consulta ativa com timeout (polling)

Caso o webhook não chegue a tempo, o código passa a consultar a API da Nota Gateway periodicamente para verificar se a nota já foi processada.

No exemplo abaixo, essa consulta acontece a cada 30 segundos, por até 10 minutos:

```csharp
[SkailFunction]
public async SkailTask<NfseResponse> ConsultarNfseNotaGateway(string idExterno)
{
    var timeout = SkailTask.DelayMinutes(10);
    var nfse = new NfseResponse();

    while (!timeout.IsCompleted)
    {
        nfse = await ConsultarNfseAsync(idExterno);

        if (nfse.EmitidaOuNegada() || timeout.IsCompleted)
            return nfse;

        await SkailTask.DelaySeconds(30);
    }

    return nfse;
}
```

\
A lógica é simples:

* enquanto o tempo limite não acabou, o fluxo consulta a nota
* se a nota for autorizada, o processo segue normalmente
* se ainda estiver pendente, ele espera mais 30 segundos e consulta de novo
* se passar de 10 minutos sem uma resposta definitiva, o fluxo para de consultar e envia a nota para revisão manual

<mark style="background-color:$success;">O ponto importante é que essa espera entre uma consulta e outra não é um</mark> <mark style="background-color:$success;"></mark><mark style="background-color:$success;">`Thread.Sleep`</mark><mark style="background-color:$success;">, nada fica rodando durante esse tempo.</mark>

A execução [hiberna](https://docs.skailhq.com/conceitos/fundamentos/hibernacao-e-retomada-resumir), não consome recurso e volta automaticamente na próxima tentativa.\
\
Se o sistema cair no meio desse processo, o skail retoma exatamente da consulta onde parou — sem reiniciar o processo do zero.

## Revisão humana (Human-in-the-Loop)

Se a nota não for autorizada, o fluxo [hiberna](https://docs.skailhq.com/conceitos/fundamentos/hibernacao-e-retomada-resumir) esperando uma correção manual:

```csharp
[SkailFunction]
public async SkailTask AguardarRevisaoNfseAsync(NfseResponse nfse)
{
    await RegistrarPedidoRevisaoAsync(nfse);

    var nfseCorrigida = await SkailTask.WaitForEvent<NfseEmissaoRequest>(
        "EVENTO_REVISAO_NFSE", nfse.IdExterno
    );

    await EnviarNotaGateway(nfseCorrigida);

    await SkailTask.DelayMinutes(3);
    var retornoNfse = await ConsultarNfseNotaGateway(nfse.IdExterno);

    await SalvarNotaAsync(retornoNfse);
}
```

**O que importa aqui:**

* **`WaitForEvent<NfseEmissaoRequest>("EVENTO_REVISAO_NFSE", ...)`** — a função [hiberna](https://docs.skailhq.com/conceitos/fundamentos/hibernacao-e-retomada-resumir) até alguém (via frontend ou API) disparar um [`FireEvent`](/funcionalidades/sdk/externalevents.md#fireevent) com a nota corrigida. Pode levar minutos, horas ou dias, pouco importa pra sua aplicação.
* **Depois da revisão**, o código reenvia a nota à Nota Gateway e repete o ciclo de consulta automaticamente.<br>

## Os SkailCommands (operações individuais)

<mark style="background-color:$success;">Se qualquer coisa falhar:</mark>

* <mark style="background-color:$success;">API fora do ar</mark>
* <mark style="background-color:$success;">erro no banco</mark>
* <mark style="background-color:$success;">restart do container</mark>

<mark style="background-color:$success;">O skail retoma do ponto exato onde parou.</mark>

Sem reprocessar tudo. Sem duplicar execução. Sem lógica extra.

```csharp
[SkailCommand]
public async SkailTask<NfseResponse> EnviarNotaGateway(NfseEmissaoRequest request)
{
    var result = await notaGatewayClient.EmitirNfseAsync(request, CancellationToken.None);
    return result;
}

[SkailCommand]
public async SkailTask<NfseResponse> ConsultarNfseAsync(string idExterno)
{
    var result = await notaGatewayClient.ConsultarNfsePorIdExternoAsync(
        idExterno, CancellationToken.None
    );
    return result;
}

[SkailCommand]
public async SkailTask SalvarNotaAsync(NfseResponse nfse)
{
    await db.SalvarNotaAsync(nfse);
    await db.RegistrarArquivoNotaAsync(nfse);
}
```

\
**O que importa aqui:**<br>

* Tudo que interage com o mundo externo (HTTP, banco, etc.) fica em [`[SkailCommand]`](/funcionalidades/sdk/skailcommand.md), <mark style="background-color:$info;">garantindo que não seja executado duas vezes, mesmo que a função principal rode novamente</mark>.<br>
* **Se algo falhar, o skail faz retry automático** — sem você precisar implementar nada extra ou gerenciar a complexidade disso.

## Webhook → fire-event (acordando a função)

Quando o NotaGateway confirma a emissão via webhook, o serviço converte para um fire-event skail:

```csharp
public async Task HandleNfseAsync(NfseWebhookDto dto, CancellationToken cancellationToken)
{
    NfseResponse response = NfseWebhookMapper.Map(dto);

    if (!response.EmitidaOuNegada())
        return; // ignora status intermediários

    await skailEventFirer.FireAsync(
        "EVENTO_AGUARDANDO_RETORNO",
        dto.NfeIdExterno,
        response,
        cancellationToken
    );
}
```

**O que importa aqui:**

* **O webhook não decide nada** — ele só traduz o formato do NotaGateway para o formato do skail (fire-event).
* **O nome do evento é o contrato.** `"EVENTO_AGUARDANDO_RETORNO"` precisa bater com o `WaitForEvent` da função principal.
* **O `IdExterno` é o correlator** — garante que o evento acorda a instância certa da função.

## Trigger via BFF (Node.js)

O frontend chama o BFF, que dispara a função skail:

```typescript
export async function triggerNfse(request: NfseEmissaoRequest): Promise<void> {
  const url = `${baseUrl}/trigger/${slug}/${version}/${fn}/${request.idExterno}`;

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${key}`,
      'skail-key': key,
      'Skail-namespace': namespace,
    },
    body: JSON.stringify([request]),
  });
}
```

**O que importa aqui:**

* **O payload é um array.** `JSON.stringify([request])` — a API de trigger espera o corpo como array.
* **O `idExterno` na URL é o instanceId** — identifica essa execução específica no skail.
* **Headers:** `Authorization` (Bearer), `skail-key` e `Skail-namespace` são obrigatórios.

## Bootstrap (.NET)

```csharp
using Skail.Platform.Runtime;

[assembly: VisibleToSkailPlatform]

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddNotaGateway();
builder.Services.AddSingleton(new NfseRepository(...));
builder.Services.Configure<SkailOptions>(...);
builder.UseSkail();

var host = builder.Build();
host.Run();
```

**O que importa aqui:**

* **`[assembly: VisibleToSkailPlatform]`** — expõe as funções marcadas com `[SkailFunction]` para o runtime.
* **`builder.UseSkail()`** — conecta o runtime do skail. A partir daqui, durabilidade, retry e hibernação funcionam.
* **O resto é seu código.** Registros de DI, repositórios, clients HTTP — tudo padrão .NET.

## Estrutura do projeto

```
nfse/
├── docker-compose.yml              ← Sobe os 4 serviços
├── .env.example                    ← Variáveis de ambiente
│
├── src/                            ← Worker .NET (runtime skail)
│   ├── Worker.cs                   ← ⭐ Fluxo principal
│   ├── Program.cs                  ← Bootstrap
│   ├── Webhooks/                   ← Recebe webhook do NotaGateway
│   ├── Skail/                      ← Wrapper fire-event HTTP
│   ├── NotaGateway/                ← Client HTTP do NotaGateway
│   └── Db/                         ← Repositório Postgres
│
├── bff/                            ← BFF Node.js
│   └── src/services/skail.ts       ← Trigger + fire-event
│
├── frontend/                       ← React (formulário + acompanhamento)
│
└── migrations/                     ← SQL inicial
```

## Como rodar

### Pré-requisitos

* Docker e Docker Compose
* Conta ativa no NotaGateway com empresa cadastrada
* Conta skail ativa

### Configuração

```bash
cp .env.example .env
```

Preencha o `.env`:

```env
# skail
SKAIL_SIDECAR=
SKAIL_BASE_URL=
SKAIL_WORKLOAD=
SKAIL_NAMESPACE=
SKAIL_KEY=

# NotaGateway
NOTAGATEWAY_BASE_URL=https://api.notagateway.com.br
NOTAGATEWAY_EMPRESA_ID=
NOTAGATEWAY_BASIC_API_KEY=

# Postgres
POSTGRES_CONNECTION_STRING=Host=localhost;Database=nfse;Username=nfse;Password=nfse

# Webhook
WEBHOOK_API_KEY=
```

### Subir o ambiente

```bash
docker compose up --build
```

| Serviço    | Porta | Descrição                   |
| ---------- | ----- | --------------------------- |
| `postgres` | 5432  | Banco de dados              |
| `worker`   | 8080  | Worker .NET (runtime skail) |
| `bff`      | 3001  | API Node.js intermediária   |
| `frontend` | 3000  | Interface web               |

## Conceitos skail usados

| Conceito                        | Onde aparece                                                                                | O que faz                                                   |
| ------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `[SkailFunction]`               | `EmitirNfseAsync`, `ConsultarNfseNotaGateway`, `AguardarRevisaoNfseAsync`                   | Funções duráveis. Se o processo cair, retoma de onde parou. |
| `[SkailCommand]`                | `EnviarNotaGateway`, `ConsultarNfseAsync`, `SalvarNotaAsync`, `RegistrarPedidoRevisaoAsync` | Operações individuais duráveis e retriáveis.                |
| `WaitForEvent<T>`               | Webhook do NotaGateway + revisão manual                                                     | Hiberna a execução até o evento externo chegar. Zero custo. |
| `WhenAny`                       | Webhook vs timeout                                                                          | Espera qualquer um dos dois completar primeiro.             |
| `DelayMinutes` / `DelaySeconds` | Timeout e polling                                                                           | Hibernação durável — não é Thread.Sleep.                    |
| `timeout.IsCompleted`           | Loop de polling                                                                             | Verifica se o tempo esgotou sem bloquear.                   |
| `fire-event`                    | Webhook service + BFF                                                                       | Acorda uma função hibernada via REST.                       |
