> 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/aprender/exemplos-completos/estoque.md).

# Estoque

Controle de estoque costuma ter vários processos que começam, param e continuam depois.<br>

Por exemplo:

* reservar um item enquanto o cliente decide
* esperar confirmação do fornecedor antes de considerar uma reposição
* aguardar a chegada física da mercadoria para atualizar o saldo

No modelo tradicional, cada uma dessas etapas vira:

* uma mensagem em fila
* um job agendado
* ou uma tabela de controle

\
Tudo com seu próprio ponto de falha.

<mark style="background-color:$success;">No skail, esse tipo de fluxo vira um método simples e linear.</mark>

### Reserva com expiração no checkout

Quando o cliente adiciona um produto ao carrinho, você precisa reservar a quantidade correspondente para que outro cliente não compre o mesmo item.

Se o checkout não for concluído em 15 minutos, a reserva expira e a quantidade volta para o estoque.

```csharp
[SkailFunction]
public async SkailTask ReservarItem(Guid reservaId, Guid produtoId, int quantidade)
{
    await MarcarComoReservado(produtoId, quantidade, reservaId);

    var checkout = SkailTask.WaitForEvent<ResultadoCheckout>(
        "CHECKOUT_CONCLUIDO",
        reservaId.ToString()
    );
    var expiracao = SkailTask.DelayMinutes(15);

    var resultado = await SkailTask.WhenAny(checkout, expiracao);

    if (resultado == expiracao)
    {
        await LiberarReserva(produtoId, quantidade, reservaId);
        return;
    }

    var dados = await checkout;
    await RegistrarPedido(dados);
    await ConfirmarBaixaDeEstoque(produtoId, quantidade, dados.PedidoId);
}
```

A função marca o produto como reservado, aguarda o que vier primeiro (a confirmação do checkout ou 15 minutos) e segue a vida.<br>

<mark style="background-color:$success;">Durante esse período, a execução fica hibernada ("pausada") sem consumir recurso:</mark>

* não há thread ocupada
* não há timer rodando
* não há polling
* não há registro em memória

<mark style="background-color:$success;">Quando o tempo passa ou o evento chega, o skail retoma a execução do ponto exato onde parou.</mark>

Do lado do checkout, quando o cliente conclui a compra, o endpoint que fecha o pedido dispara o evento na [API HTTP de eventos do skail](/construir/api-http/post-api-v1-fire-eventname-instanceid.md), usando o mesmo `reservaId`. Ele não grava nada; os dados do pedido vão no payload do evento:

```csharp
// No endpoint que conclui o checkout (código ASP.NET comum, fora do skail)
// _http já tem os headers skail-key e Skail-namespace configurados
await _http.PostAsJsonAsync(
    $"{skailBaseUrl}/api/v1/fire/CHECKOUT_CONCLUIDO/{reservaId}",
    new ResultadoCheckout { PedidoId = pedidoId, ClienteId = clienteId, Itens = itens });
```

Qualquer execução que estiver aguardando esse evento continua automaticamente, e é ela que registra o pedido (`RegistrarPedido`) e dá baixa no estoque, já com o payload em mãos. Se o endpoint gravasse o pedido e só depois disparasse o evento, uma falha entre os dois passos deixaria o pedido no banco e a reserva correndo até expirar e devolver o item ao estoque. Dentro da execução, a gravação é um command: se o banco falhar, o skail retenta; se a aplicação cair, a execução retoma com o evento já recebido. Quem dispara pode ser qualquer sistema, em qualquer linguagem: é só um POST.

{% hint style="info" %}

### Garantindo execução única

Os métodos que alteram o estoque e o pedido (`MarcarComoReservado`, `LiberarReserva`, `RegistrarPedido`, `ConfirmarBaixaDeEstoque`) são [`[SkailCommand]`](/construir/sdk-.net/skailcommand.md).

Um command que já gravou resultado não roda de novo quando a função principal é retomada, por mais vezes que isso aconteça ao longo da execução.
{% endhint %}

### Reposição automática

A mesma ideia se estende para fluxos longos que duram dias.

Quando o estoque de um produto cai abaixo do mínimo, você dispara o fluxo de reposição:

```csharp
[SkailFunction]
public async SkailTask ReporEstoque(Guid produtoId, int quantidade)
{
    var ordemId = await CriarOrdemDeCompra(produtoId, quantidade);

    var confirmacao = await SkailTask.WaitForEvent<ConfirmacaoFornecedor>(
        "FORNECEDOR_CONFIRMOU",
        ordemId.ToString()
    );

    if (!confirmacao.Aceito)
    {
        await CancelarOrdem(ordemId, confirmacao.MotivoRecusa);
        return;
    }

    var entrega = await SkailTask.WaitForEvent<NotaFiscalEntrada>(
        "MERCADORIA_RECEBIDA",
        ordemId.ToString()
    );

    await AtualizarSaldo(produtoId, entrega.QuantidadeRecebida);
    await ConciliarNota(ordemId, entrega.NumeroNF);
}
```

\
Esse processo pode levar horas ou dias:

* entre criar a ordem e receber a confirmação
* entre a confirmação e a entrega física

\
Mesmo assim, o código continua simples e linear.

\
Em cada ponto, o fluxo:

* espera um evento (`WaitForEvent`)
* hiberna (desliga e não consome mais recurso) até esse evento acontecer
* e continua automaticamente depois (como se nunca tivesse parado)

### Limite de espera pelo fornecedor

Se o fornecedor não responder em 48 horas, você provavelmente quer escalar para o time de compras em vez de deixar a ordem pendurada.

Basta combinar o `WaitForEvent` com um `Delay` de 48 horas dentro de um `WhenAny`:

```csharp
var confirmacaoTask = SkailTask.WaitForEvent<ConfirmacaoFornecedor>(
    "FORNECEDOR_CONFIRMOU",
    ordemId.ToString()
);
var limiteTask = SkailTask.Delay(TimeSpan.FromHours(48));

var resultado = await SkailTask.WhenAny(confirmacaoTask, limiteTask);

if (resultado == limiteTask)
{
    await EscalarParaTimeDeCompras(ordemId);
    return;
}

var confirmacao = await confirmacaoTask;
```

O fluxo está aguardando duas coisas ao mesmo tempo:

* a resposta do fornecedor (`WaitForEvent`)
* ou o prazo de 48 horas (`Delay`)

\
O [`WhenAny`](/construir/sdk-.net/whenall-e-whenany.md) faz o sistema reagir ao que acontecer primeiro.

### Observabilidade

No monitor do portal do skail, cada reserva e cada ordem de reposição aparece como uma execução própria.<br>

Você consegue, por exemplo:<br>

* filtrar por TaskId
* ver quanto tempo cada reserva ficou aguardando
* identificar quantas expiraram e quantas viraram pedido
* acompanhar ordens de reposição que estão há dias esperando o fornecedor

\ <mark style="background-color:$success;">Tudo isso vem direto do estado das execuções, sem precisar adicionar nada além do código acima. Sem precisar adicionar logs, métricas, ferramentas adicionais ou código extra.</mark>
