> 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/construa/casos-de-uso/emissao-de-nota-fiscal.md).

# Emissão de nota fiscal

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:

* 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.
* Rodando com Docker Compose.

{% hint style="info" %}
O fluxo principal está em `src/Worker.cs`. É nele que fica a lógica de orquestração.
{% endhint %}

\ <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

{% tabs %}
{% tab title="C#" %}
{% code lineNumbers="true" %}

```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);
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code lineNumbers="true" %}

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";
import {
  db,
  emitidaOuNegada,
  notaGatewayClient,
  type NfseEmissaoRequest,
  type NfseResponse,
} from "./nfse-integracoes.js";

export class Worker {
  @SkailFunction()
  async EmitirNfseAsync(request: NfseEmissaoRequest): Promise<void> {
    let nfse = await this.EnviarNotaGateway(request);

    // Cria o evento para aguardar o retorno do NotaGateway
    const retornoNotaGateway = Skail.waitForEvent<NfseResponse>(
      "EVENTO_AGUARDANDO_RETORNO",
      request.IdExterno,
    );

    // Configura um limite de tempo para aguardar o retorno
    const timeout = Skail.delayMinutes(3);

    // Aguarda o retorno OU o timeout, o que ocorrer primeiro.
    const retornoOuTimeout = await Skail.whenAny(retornoNotaGateway, timeout);

    if (retornoOuTimeout !== undefined) {
      nfse = retornoOuTimeout;
    } else {
      nfse = await this.ConsultarNfseNotaGateway(request.IdExterno);
    }

    await this.SalvarNotaAsync(nfse);

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

  @SkailFunction()
  async ConsultarNfseNotaGateway(idExterno: string): Promise<NfseResponse> {
    const tentativas = 20;
    let nfse: NfseResponse = { IdExterno: idExterno, Status: "Pendente" };

    for (let tentativa = 0; tentativa < tentativas; tentativa++) {
      nfse = await this.ConsultarNfseAsync(idExterno);

      if (emitidaOuNegada(nfse)) return nfse;

      await Skail.delaySeconds(30);
    }

    return nfse;
  }

  @SkailFunction()
  async AguardarRevisaoNfseAsync(nfse: NfseResponse): Promise<void> {
    await this.RegistrarPedidoRevisaoAsync(nfse);

    const nfseCorrigida = await Skail.waitForEvent<NfseEmissaoRequest>(
      "EVENTO_REVISAO_NFSE",
      nfse.IdExterno,
    );

    await this.EnviarNotaGateway(nfseCorrigida);

    await Skail.delayMinutes(3);
    const retornoNfse = await this.ConsultarNfseNotaGateway(nfse.IdExterno);

    await this.SalvarNotaAsync(retornoNfse);
  }

  @SkailCommand()
  async EnviarNotaGateway(request: NfseEmissaoRequest): Promise<NfseResponse> {
    const result = await notaGatewayClient.EmitirNfseAsync(request);
    return result;
  }

  @SkailCommand()
  async ConsultarNfseAsync(idExterno: string): Promise<NfseResponse> {
    const result = await notaGatewayClient.ConsultarNfsePorIdExternoAsync(idExterno);
    return result;
  }

  @SkailCommand()
  async SalvarNotaAsync(nfse: NfseResponse): Promise<void> {
    await db.SalvarNotaAsync(nfse);
    await db.RegistrarArquivoNotaAsync(nfse);
  }

  @SkailCommand()
  async RegistrarPedidoRevisaoAsync(nfse: NfseResponse): Promise<void> {
    await db.RegistrarPedidoRevisaoAsync(nfse);
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

Só de ler o código você já entende facilmente todas as etapas da emissão, 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 espera por evento e delay** 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](/construa/conceitos/hibernacao-e-retomada.md) significa que a execução “pausa de verdade”.\
\
Ela não fica rodando em background, não consome CPU nem mantém thread ativa.\
\ <mark style="background-color:cyan;">O skail registra em que ponto a execução está e 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:

{% tabs %}
{% tab title="C#" %}
{% code lineNumbers="true" %}

```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;
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code lineNumbers="true" %}

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";
import { emitidaOuNegada, notaGatewayClient, type NfseResponse } from "./nfse-integracoes.js";

export class Worker {
  @SkailFunction()
  async ConsultarNfseNotaGateway(idExterno: string): Promise<NfseResponse> {
    const tentativas = 20;
    let nfse: NfseResponse = { IdExterno: idExterno, Status: "Pendente" };

    for (let tentativa = 0; tentativa < tentativas; tentativa++) {
      nfse = await this.ConsultarNfseAsync(idExterno);

      if (emitidaOuNegada(nfse)) return nfse;

      await Skail.delaySeconds(30);
    }

    return nfse;
  }

  @SkailCommand()
  async ConsultarNfseAsync(idExterno: string): Promise<NfseResponse> {
    const result = await notaGatewayClient.ConsultarNfsePorIdExternoAsync(idExterno);
    return result;
  }

}
```

{% endcode %}
{% endtab %}
{% endtabs %}

\
A lógica é simples:

* o fluxo consulta a nota a cada 30 segundos, por até 10 minutos
* 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 nada fica rodando na espera entre uma consulta e outra.</mark>

A execução [hiberna](/construa/conceitos/hibernacao-e-retomada.md), 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)

<figure><img src="https://1898946878-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsJmS5ILPG5x2x5j0WyiV%2Fuploads%2FHt5r0Dm6BUnNdKbQI1bv%2Fap-nfse-revisao.svg?alt=media" alt="AguardarRevisaoNfseAsync de cima para baixo: RegistrarPedidoRevisaoAsync registra o pedido de revisão e a function espera o evento EVENTO_REVISAO_NFSE com a nota corrigida, que alguém dispara pelo frontend ou pela API com POST /api/v1/fire/EVENTO_REVISAO_NFSE/{idExterno}. Em seguida, EnviarNotaGateway reenvia a nota, um delay de 3 minutos vem antes da function filha ConsultarNfseNotaGateway, que repete o ciclo de consulta, e SalvarNotaAsync salva a nota."><figcaption></figcaption></figure>

Se a nota não for autorizada, o fluxo [hiberna](/construa/conceitos/hibernacao-e-retomada.md) esperando uma correção manual:

{% tabs %}
{% tab title="C#" %}
{% code lineNumbers="true" %}

```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);
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code lineNumbers="true" %}

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";
import {
  db,
  emitidaOuNegada,
  notaGatewayClient,
  type NfseEmissaoRequest,
  type NfseResponse,
} from "./nfse-integracoes.js";

export class Worker {
  @SkailFunction()
  async AguardarRevisaoNfseAsync(nfse: NfseResponse): Promise<void> {
    await this.RegistrarPedidoRevisaoAsync(nfse);

    const nfseCorrigida = await Skail.waitForEvent<NfseEmissaoRequest>(
      "EVENTO_REVISAO_NFSE",
      nfse.IdExterno,
    );

    await this.EnviarNotaGateway(nfseCorrigida);

    await Skail.delayMinutes(3);
    const retornoNfse = await this.ConsultarNfseNotaGateway(nfse.IdExterno);

    await this.SalvarNotaAsync(retornoNfse);
  }

  @SkailFunction()
  async ConsultarNfseNotaGateway(idExterno: string): Promise<NfseResponse> {
    const tentativas = 20;
    let nfse: NfseResponse = { IdExterno: idExterno, Status: "Pendente" };

    for (let tentativa = 0; tentativa < tentativas; tentativa++) {
      nfse = await this.ConsultarNfseAsync(idExterno);

      if (emitidaOuNegada(nfse)) return nfse;

      await Skail.delaySeconds(30);
    }

    return nfse;
  }

  @SkailCommand()
  async EnviarNotaGateway(request: NfseEmissaoRequest): Promise<NfseResponse> {
    const result = await notaGatewayClient.EmitirNfseAsync(request);
    return result;
  }

  @SkailCommand()
  async ConsultarNfseAsync(idExterno: string): Promise<NfseResponse> {
    const result = await notaGatewayClient.ConsultarNfsePorIdExternoAsync(idExterno);
    return result;
  }

  @SkailCommand()
  async SalvarNotaAsync(nfse: NfseResponse): Promise<void> {
    await db.SalvarNotaAsync(nfse);
    await db.RegistrarArquivoNotaAsync(nfse);
  }

  @SkailCommand()
  async RegistrarPedidoRevisaoAsync(nfse: NfseResponse): Promise<void> {
    await db.RegistrarPedidoRevisaoAsync(nfse);
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

**O que importa aqui:**

* **A espera pelo evento `EVENTO_REVISAO_NFSE`**: a função [hiberna](/construa/conceitos/hibernacao-e-retomada.md) até alguém (via frontend ou API) disparar o evento pela [API HTTP de eventos do skail](/referencia/api-http/post-fire.md) 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 commands (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>

Ele não reprocessa o que já foi feito, não duplica a execução e não exige lógica extra no seu código.

{% tabs %}
{% tab title="C#" %}
{% code lineNumbers="true" %}

```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);
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code lineNumbers="true" %}

```typescript
import { SkailCommand } from "@skail/runtime";
import {
  db,
  notaGatewayClient,
  type NfseEmissaoRequest,
  type NfseResponse,
} from "./nfse-integracoes.js";

export class Worker {
  @SkailCommand()
  async EnviarNotaGateway(request: NfseEmissaoRequest): Promise<NfseResponse> {
    const result = await notaGatewayClient.EmitirNfseAsync(request);
    return result;
  }

  @SkailCommand()
  async ConsultarNfseAsync(idExterno: string): Promise<NfseResponse> {
    const result = await notaGatewayClient.ConsultarNfsePorIdExternoAsync(idExterno);
    return result;
  }

  @SkailCommand()
  async SalvarNotaAsync(nfse: NfseResponse): Promise<void> {
    await db.SalvarNotaAsync(nfse);
    await db.RegistrarArquivoNotaAsync(nfse);
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

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

* Tudo que faz operação de I/O (HTTP, banco, etc.) fica em [commands](/referencia/sdk-csharp/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 (retomando a função)

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

{% tabs %}
{% tab title="C#" %}
{% code lineNumbers="true" %}

```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
    );
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code lineNumbers="true" %}

```typescript
import { emitidaOuNegada, NfseWebhookMapper, type NfseWebhookDto } from "./nfse-integracoes.js";

export class SkailEventFirer {
  constructor(
    private readonly baseUrl: string,
    private readonly key: string,
    private readonly namespace: string,
  ) {}

  async FireAsync(evento: string, idExterno: string, payload: unknown, signal?: AbortSignal): Promise<void> {
    const resposta = await fetch(`${this.baseUrl}/api/v1/fire/${evento}/${idExterno}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "skail-key": this.key,
        "Skail-namespace": this.namespace,
      },
      body: JSON.stringify(payload),
      signal,
    });
    if (!resposta.ok) throw new Error(`Falha ao disparar ${evento}: HTTP ${resposta.status}`);
  }
}

const skailEventFirer = new SkailEventFirer(
  process.env.SKAIL_BASE_URL!,
  process.env.SKAIL_KEY!,
  process.env.SKAIL_NAMESPACE!,
);

export async function HandleNfseAsync(dto: NfseWebhookDto, signal?: AbortSignal): Promise<void> {
  const response = NfseWebhookMapper.Map(dto);

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

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

{% endcode %}
{% endtab %}
{% endtabs %}

**O que importa aqui:**

* **O webhook não decide nada**: ele só traduz o formato do NotaGateway para o formato do skail (fire-event).
* **`skailEventFirer` é um wrapper fino do próprio projeto** (pasta `src/Skail/`) sobre a [API HTTP de eventos](/referencia/api-http/post-fire.md): um `POST` em `/api/v1/fire/{evento}/{idExterno}` com os headers `skail-key` e `Skail-namespace`.
* **O nome do evento é o contrato.** `"EVENTO_AGUARDANDO_RETORNO"` precisa bater com a espera por evento 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:

{% code lineNumbers="true" %}

```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',
      'skail-key': key,
      'Skail-namespace': namespace,
    },
    body: JSON.stringify([request]),
  });
}
```

{% endcode %}

**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:** `skail-key` e `Skail-namespace` são obrigatórios.

## Bootstrap

{% tabs %}
{% tab title="C#" %}
{% code lineNumbers="true" %}

```csharp
using Skail.Platform.Runtime.Hosting;
using Skail.Platform.Runtime.Standard;

[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();
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code lineNumbers="true" %}

```typescript
import { createSkailPlatform } from "@skail/runtime";

const controller = new AbortController();
for (const signal of ["SIGINT", "SIGTERM"] as const) {
  process.once(signal, () => controller.abort());
}

const platform = createSkailPlatform({
  workload: process.env.SKAIL_WORKLOAD!,
  bundle: new URL("../.skail/skail-functions.mjs", import.meta.url),
  manifest: new URL("../.skail/manifest.json", import.meta.url),
  sidecar: {
    baseUrl: process.env.SKAIL_SIDECAR,
    namespace: process.env.SKAIL_NAMESPACE,
    key: process.env.SKAIL_KEY,
  },
});

await platform.run(controller.signal);
```

{% endcode %}
{% endtab %}
{% endtabs %}

**O que importa aqui:**

* **As functions ficam visíveis para o runtime.** Em C#, isso é feito por `[assembly: VisibleToSkailPlatform]`; em TypeScript, o `skail build` gera o bundle (`skail-functions.mjs`) e o `manifest.json` que o host carrega.
* **O host conecta o runtime do skail.** Em C#, com `builder.UseSkail()`; em TypeScript, com `createSkailPlatform({ ... })` e `platform.run(signal)`. A partir daqui, durabilidade, retry e hibernação funcionam.
* **O restante é código comum da aplicação.** Em C#, são os registros de dependências, os repositórios e os clients HTTP.

## 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                                                   |
| ------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Function                        | `EmitirNfseAsync`, `ConsultarNfseNotaGateway`, `AguardarRevisaoNfseAsync`                   | Funções duráveis. Se o processo cair, retoma de onde parou. |
| Command                         | `EnviarNotaGateway`, `ConsultarNfseAsync`, `SalvarNotaAsync`, `RegistrarPedidoRevisaoAsync` | Operações individuais duráveis e retriáveis.                |
| Espera por evento               | Webhook do NotaGateway + revisão manual                                                     | Hiberna a execução até o evento externo chegar. Zero custo. |
| Esperar o primeiro que terminar | Webhook vs timeout                                                                          | Espera qualquer um dos dois completar primeiro.             |
| Delay                           | Timeout e polling                                                                           | Hibernação durável, sem pausa nem timer comum da linguagem. |
| Limite do polling               | Loop de polling                                                                             | Para de consultar depois de 10 minutos, sem bloquear.       |
| `fire-event`                    | Webhook service + BFF                                                                       | Retoma uma função hibernada via REST.                       |
