> 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/pedido-com-estoque-aprovacao-e-faturamento.md).

# Pedido com estoque, aprovação e faturamento

Processo de pedido de ponta a ponta: registro, reserva de estoque, aprovação do gerente com lembrete e prazo, faturamento e envio ao ERP do cliente.

Este exemplo implementa o processo de confirmação de pedido de um sistema de gestão: o pedido é registrado, o estoque é reservado, pedidos acima de R$ 50 mil esperam a aprovação do gerente, a fatura é emitida e o ERP do cliente recebe o pedido. O código está completo e pode ser copiado para o seu projeto ou entregue ao seu assistente de IA como especificação.

## O processo

Quando o vendedor confirma um pedido, o sistema precisa:

1. Registrar o pedido no banco.
2. Reservar os itens no serviço de estoque. Sem estoque, o pedido é cancelado.
3. Se o total passar de R$ 50 mil, avisar o gerente e esperar a decisão. Sem resposta em 24 horas, mandar um lembrete. Sem resposta em 72 horas, a aprovação expira.
4. Se o gerente reprovar ou a aprovação expirar, liberar a reserva e cancelar o pedido com o motivo.
5. Emitir a fatura no serviço de faturamento.
6. Enviar o pedido com o número da fatura ao ERP do cliente. Se o ERP recusar (cliente bloqueado, cadastro divergente), cancelar a fatura, liberar a reserva e cancelar o pedido com o motivo da recusa.
7. Marcar o pedido como concluído.

O processo pode levar segundos ou três dias, precisa sobreviver a reinícios da aplicação e depende de quatro serviços externos (estoque, faturamento, ERP do cliente e notificações) que podem ficar fora do ar.

<figure><img src="https://1898946878-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsJmS5ILPG5x2x5j0WyiV%2Fuploads%2FL80RbNlCYZo95ZFM0jQp%2Fap-pedido-fluxo.svg?alt=media" alt="ProcessarPedido de cima para baixo, na ordem do código: RegistrarPedido, ReservarEstoque, a function filha AguardarAprovacaoDoGerente quando o total passa de R$ 50 mil, EmitirFatura, EnviarAoErpDoCliente e ConcluirPedido. Sem estoque, aprovação reprovada ou expirada e recusa do ERP levam à function filha DesfazerPedido, que roda CancelarFatura se há fatura, LiberarEstoque se há reserva e sempre CancelarPedido com o motivo."><figcaption><p>Fluxo do pedido</p></figcaption></figure>

## Como o processo vira código no skail

| Parte do processo                                 | No skail                                                                                  |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| O processo inteiro, do registro à conclusão       | A function `ProcessarPedido`                                                              |
| A espera pela decisão do gerente                  | `AguardarAprovacaoDoGerente`, uma function filha                                          |
| Cada passo que grava no banco ou chama um serviço | Um command                                                                                |
| O cancelamento, desfazendo o que já foi feito     | `DesfazerPedido`, outra function filha                                                    |
| A recusa do ERP do cliente                        | Resultado do command `EnviarAoErpDoCliente`, sem exceção                                  |
| O prazo de 24 e de 72 horas                       | Um delay, combinado com esperar o primeiro que terminar                                   |
| A decisão do gerente                              | Evento `PEDIDO_DECIDIDO`, recebido por uma espera por evento com payload `DecisaoGerente` |
| O início do processo                              | Chamada à API HTTP do skail (trigger) feita pela API que confirma o pedido                |

### Contratos

| Item                           | Valor                                                                                                           |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Workload                       | `pedidos:v1.0.0`                                                                                                |
| Function iniciada pelo trigger | `ProcessarPedido`, com um argumento `PedidoConfirmado`                                                          |
| Rota do trigger                | `POST /trigger/pedidos/v1.0.0/ProcessarPedido/{pedidoId}` com corpo `[pedido]`                                  |
| Id da execução                 | `pedidoId`                                                                                                      |
| Evento da decisão              | `PEDIDO_DECIDIDO`                                                                                               |
| instanceId do evento           | `pedidoId`                                                                                                      |
| Payload do evento              | `{ "Aprovado": true, "DecididoPor": "...", "Motivo": null }`, com os nomes das propriedades de `DecisaoGerente` |
| Limite sem aprovação           | R$ 50.000,00                                                                                                    |
| Lembrete e prazo final         | 24 horas e mais 48 horas                                                                                        |

## 1. Partes da solução e tipos do domínio

A solução tem três partes:

* `Pedidos.Contratos` (os tipos abaixo);
* `Pedidos.Fluxo` (a aplicação com o runtime do skail);
* `Pedidos.Api` (a API que confirma o pedido).

As duas últimas usam os tipos de `Pedidos.Contratos`; só `Pedidos.Fluxo` usa o skail. Em C#, cada parte é um projeto: `Pedidos.Contratos` é uma biblioteca de classes, `Pedidos.Fluxo` usa os pacotes `Skail.Platform.Runtime`, `Microsoft.Extensions.Http` e `Npgsql.EntityFrameworkCore.PostgreSQL`, e `Pedidos.Api` é uma API ASP.NET; em TypeScript, cada parte é um conjunto de arquivos (`contratos.ts`, `fluxo.ts`, `api.ts` e os demais indicados no topo de cada trecho), e `Pedidos.Fluxo` usa os pacotes `@skail/runtime` e `pg`.

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

```csharp
// Pedidos.Contratos
namespace Pedidos.Contratos;

public static class Eventos
{
    public const string PedidoDecidido = "PEDIDO_DECIDIDO";
}

public record ItemPedido(string Sku, int Quantidade);
public record PedidoConfirmado(Guid PedidoId, Guid ClienteId, decimal Total, ItemPedido[] Itens);
public record DecisaoGerente(bool Aprovado, string DecididoPor, string? Motivo);
public record Fatura(string Numero);

public enum ResultadoAprovacao { Aprovado, Reprovado, Expirado }
```

{% endcode %}
{% endtab %}

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

```typescript
// contratos.ts
// Pedidos.Contratos
export const Eventos = {
  PedidoDecidido: "PEDIDO_DECIDIDO",
} as const;

export interface ItemPedido {
  Sku: string;
  Quantidade: number;
}

export interface PedidoConfirmado {
  PedidoId: string;
  ClienteId: string;
  Total: number;
  Itens: ItemPedido[];
}

export interface DecisaoGerente {
  Aprovado: boolean;
  DecididoPor: string;
  Motivo: string | null;
}

export interface Fatura {
  Numero: string;
}

export type ResultadoAprovacao = "Aprovado" | "Reprovado" | "Expirado";
```

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

{% hint style="info" %}
Os argumentos e retornos levam ids e valores de decisão. Tudo o que entra e sai de um command aparece na linha do tempo da execução no Monitor, então o cadastro completo do cliente fica no seu banco e é carregado dentro do command que precisa dele.
{% endhint %}

## 2. O fluxo

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

```csharp
// Pedidos.Fluxo
using Pedidos.Contratos;
using Skail.Platform.Runtime.Standard;
using Skail.Platform.Runtime.Standard.Threading;

[assembly: VisibleToSkailPlatform]

public partial class FluxoDoPedido
{
    private const decimal LimiteSemAprovacao = 50_000m;

    [SkailFunction]
    public async SkailTask ProcessarPedido(PedidoConfirmado pedido)
    {
        await RegistrarPedido(pedido);

        var reservado = await ReservarEstoque(pedido.PedidoId, pedido.Itens);
        if (!reservado)
        {
            await DesfazerPedido(pedido.PedidoId, estoqueReservado: false, numeroFatura: null, "sem estoque");
            return;
        }

        if (pedido.Total > LimiteSemAprovacao)
        {
            var aprovacao = await AguardarAprovacaoDoGerente(pedido.PedidoId, pedido.Total);
            if (aprovacao != ResultadoAprovacao.Aprovado)
            {
                await DesfazerPedido(pedido.PedidoId, estoqueReservado: true, numeroFatura: null,
                    aprovacao == ResultadoAprovacao.Reprovado ? "reprovado pelo gerente" : "aprovação expirou");
                return;
            }
        }

        var fatura = await EmitirFatura(pedido.PedidoId, pedido.Total);

        var erp = await EnviarAoErpDoCliente(pedido, fatura.Numero);
        if (!erp.Aceito)
        {
            await DesfazerPedido(pedido.PedidoId, estoqueReservado: true, fatura.Numero,
                $"recusado pelo ERP do cliente: {erp.Motivo}");
            return;
        }

        await ConcluirPedido(pedido.PedidoId, fatura.Numero);
    }

    // Desfaz, na ordem inversa, só o que chegou a ser feito.
    [SkailFunction]
    public async SkailTask DesfazerPedido(Guid pedidoId, bool estoqueReservado, string? numeroFatura, string motivo)
    {
        if (numeroFatura is not null)
            await CancelarFatura(pedidoId, numeroFatura);

        if (estoqueReservado)
            await LiberarEstoque(pedidoId);

        await CancelarPedido(pedidoId, motivo);
    }

    [SkailFunction]
    public async SkailTask<ResultadoAprovacao> AguardarAprovacaoDoGerente(Guid pedidoId, decimal total)
    {
        await NotificarGerente(pedidoId, total);

        // Uma espera só, reaproveitada nos dois prazos.
        var decisao = SkailTask.WaitForEvent<DecisaoGerente>(Eventos.PedidoDecidido, pedidoId.ToString());

        var lembrete = SkailTask.Delay(TimeSpan.FromHours(24));
        if (await SkailTask.WhenAny(decisao, lembrete) == lembrete)
        {
            await LembrarGerente(pedidoId);

            var prazoFinal = SkailTask.Delay(TimeSpan.FromHours(48));
            if (await SkailTask.WhenAny(decisao, prazoFinal) == prazoFinal)
            {
                await RegistrarExpiracao(pedidoId);
                return ResultadoAprovacao.Expirado;
            }
        }

        var resposta = await decisao;
        await RegistrarDecisao(pedidoId, resposta);
        return resposta.Aprovado ? ResultadoAprovacao.Aprovado : ResultadoAprovacao.Reprovado;
    }
}
```

{% endcode %}
{% endtab %}

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

```typescript
// fluxo.ts
// Pedidos.Fluxo
import { Skail, SkailFunction } from "@skail/runtime";
import { ComandosDoPedido } from "./comandos.js";
import { Eventos, type DecisaoGerente, type PedidoConfirmado, type ResultadoAprovacao } from "./contratos.js";

const LimiteSemAprovacao = 50_000;
const HoraEmMs = 60 * 60 * 1000;

export class FluxoDoPedido {
  private readonly cmd = new ComandosDoPedido();

  @SkailFunction()
  async ProcessarPedido(pedido: PedidoConfirmado): Promise<void> {
    await this.cmd.RegistrarPedido(pedido);

    const reservado = await this.cmd.ReservarEstoque(pedido.PedidoId, pedido.Itens);
    if (!reservado) {
      await this.DesfazerPedido(pedido.PedidoId, false, null, "sem estoque");
      return;
    }

    if (pedido.Total > LimiteSemAprovacao) {
      const aprovacao = await this.AguardarAprovacaoDoGerente(pedido.PedidoId, pedido.Total);
      if (aprovacao !== "Aprovado") {
        await this.DesfazerPedido(
          pedido.PedidoId,
          true,
          null,
          aprovacao === "Reprovado" ? "reprovado pelo gerente" : "aprovação expirou",
        );
        return;
      }
    }

    const fatura = await this.cmd.EmitirFatura(pedido.PedidoId, pedido.Total);

    const erp = await this.cmd.EnviarAoErpDoCliente(pedido, fatura.Numero);
    if (!erp.Aceito) {
      await this.DesfazerPedido(pedido.PedidoId, true, fatura.Numero, `recusado pelo ERP do cliente: ${erp.Motivo}`);
      return;
    }

    await this.cmd.ConcluirPedido(pedido.PedidoId, fatura.Numero);
  }

  // Desfaz, na ordem inversa, só o que chegou a ser feito.
  @SkailFunction()
  async DesfazerPedido(
    pedidoId: string,
    estoqueReservado: boolean,
    numeroFatura: string | null,
    motivo: string,
  ): Promise<void> {
    if (numeroFatura !== null) await this.cmd.CancelarFatura(pedidoId, numeroFatura);

    if (estoqueReservado) await this.cmd.LiberarEstoque(pedidoId);

    await this.cmd.CancelarPedido(pedidoId, motivo);
  }

  @SkailFunction()
  async AguardarAprovacaoDoGerente(pedidoId: string, total: number): Promise<ResultadoAprovacao> {
    await this.cmd.NotificarGerente(pedidoId, total);

    // Uma espera só, reaproveitada nos dois prazos.
    const decisao = Skail.waitForEvent<DecisaoGerente>(Eventos.PedidoDecidido, pedidoId);

    const lembrete = Skail.delay(24 * HoraEmMs);
    if ((await Skail.whenAny(decisao, lembrete)) === undefined) {
      await this.cmd.LembrarGerente(pedidoId);

      const prazoFinal = Skail.delay(48 * HoraEmMs);
      if ((await Skail.whenAny(decisao, prazoFinal)) === undefined) {
        await this.cmd.RegistrarExpiracao(pedidoId);
        return "Expirado";
      }
    }

    const resposta = await decisao;
    await this.cmd.RegistrarDecisao(pedidoId, resposta);
    return resposta.Aprovado ? "Aprovado" : "Reprovado";
  }
}
```

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

A function só decide: compara o total, que veio como argumento, e lê o resultado dos commands. Ela não lê relógio, banco nem rede, porque o skail reexecuta o corpo dela a cada retomada e precisa chegar sempre no mesmo ponto.

A espera pelo gerente está numa function filha para o fluxo principal ficar legível. Enquanto a filha espera, a function pai também hiberna. A espera pelo evento é criada uma vez e participa das duas esperas com prazo, então uma decisão que chega depois do lembrete é atendida pela mesma espera.

Os três motivos de cancelamento (sem estoque, aprovação negada ou expirada, recusa do ERP) passam por `DesfazerPedido`. Ela recebe o que já foi feito e desfaz na ordem inversa: primeiro a fatura, depois a reserva, por último o status do pedido. Se o processo ganhar um passo novo, a reversão dele entra nesse mesmo lugar.

## 3. Os commands

Cada command faz uma operação e usa o `PedidoId` como chave. O skail não executa de novo um command que já registrou o resultado, mas se o processo cair entre a chamada a um serviço e o registro do resultado, o command roda outra vez. A chave faz o serviço reconhecer a repetição: o faturamento recebe `fatura-{pedidoId}` duas vezes e emite uma fatura só.

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

```csharp
// Pedidos.Fluxo
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Pedidos.Contratos;
using Skail.Platform.Runtime.Standard;
using Skail.Platform.Runtime.Standard.Threading;

public class PedidosDb(DbContextOptions<PedidosDb> options) : DbContext(options) { }

public record RespostaReserva(bool Reservado);
public record RespostaErp(bool Aceito, string? Motivo);
public record RecusaErp(string Motivo);

public partial class FluxoDoPedido
{
    private readonly PedidosDb _db;
    private readonly IHttpClientFactory _http;

    public FluxoDoPedido(PedidosDb db, IHttpClientFactory http)
    {
        _db = db;
        _http = http;
    }

    [SkailCommand]
    public async SkailTask RegistrarPedido(PedidoConfirmado pedido)
    {
        await _db.Database.ExecuteSqlInterpolatedAsync($@"
            INSERT INTO pedidos (id, cliente_id, total, status)
            VALUES ({pedido.PedidoId}, {pedido.ClienteId}, {pedido.Total}, 'confirmado')
            ON CONFLICT (id) DO NOTHING");
    }

    [SkailCommand]
    public async SkailTask<bool> ReservarEstoque(Guid pedidoId, ItemPedido[] itens)
    {
        using var resposta = await Enviar("estoque", "/reservas", $"reserva-{pedidoId}", new { pedidoId, itens });
        var corpo = await resposta.Content.ReadFromJsonAsync<RespostaReserva>();
        return corpo!.Reservado;
    }

    [SkailCommand]
    public async SkailTask LiberarEstoque(Guid pedidoId)
    {
        using var resposta = await Enviar("estoque", $"/reservas/{pedidoId}/liberar", $"liberacao-{pedidoId}", new { pedidoId });
    }

    [SkailCommand(retryCount: 8)]
    public async SkailTask<Fatura> EmitirFatura(Guid pedidoId, decimal total)
    {
        using var resposta = await Enviar("faturamento", "/faturas", $"fatura-{pedidoId}", new { pedidoId, total });
        return (await resposta.Content.ReadFromJsonAsync<Fatura>())!;
    }

    [SkailCommand]
    public async SkailTask CancelarFatura(Guid pedidoId, string numeroFatura)
    {
        using var resposta = await Enviar("faturamento", $"/faturas/{numeroFatura}/cancelar", $"cancelamento-{pedidoId}", new { pedidoId });
    }

    [SkailCommand]
    public async SkailTask<RespostaErp> EnviarAoErpDoCliente(PedidoConfirmado pedido, string numeroFatura)
    {
        using var resposta = await Postar("erp", "/pedidos", $"erp-{pedido.PedidoId}",
            new { referenciaExterna = pedido.PedidoId, pedido.ClienteId, pedido.Itens, numeroFatura });

        // 422 é a recusa de negócio documentada pelo ERP (cliente bloqueado, cadastro divergente):
        // volta como resultado, sem nova tentativa. Qualquer outro erro lança exceção e segue para o retry.
        if (resposta.StatusCode == HttpStatusCode.UnprocessableEntity)
        {
            string? motivo = null;
            try { motivo = (await resposta.Content.ReadFromJsonAsync<RecusaErp>())?.Motivo; }
            catch (JsonException) { }   // corpo fora do formato esperado: a recusa vale do mesmo jeito
            return new RespostaErp(false, motivo ?? "sem motivo informado");
        }

        resposta.EnsureSuccessStatusCode();
        return new RespostaErp(true, null);
    }

    [SkailCommand]
    public async SkailTask NotificarGerente(Guid pedidoId, decimal total)
    {
        using var resposta = await Enviar("notificacoes", "/gerente", $"notificacao-{pedidoId}",
            new { pedidoId, total, link = $"https://erp.exemplo/aprovacoes/{pedidoId}" });
    }

    [SkailCommand]
    public async SkailTask LembrarGerente(Guid pedidoId)
    {
        using var resposta = await Enviar("notificacoes", "/gerente/lembrete", $"lembrete-{pedidoId}", new { pedidoId });
    }

    [SkailCommand]
    public async SkailTask RegistrarDecisao(Guid pedidoId, DecisaoGerente decisao)
    {
        await _db.Database.ExecuteSqlInterpolatedAsync($@"
            INSERT INTO aprovacoes (pedido_id, aprovado, decidido_por, motivo, decidido_em)
            VALUES ({pedidoId}, {decisao.Aprovado}, {decisao.DecididoPor}, {decisao.Motivo}, now())
            ON CONFLICT (pedido_id) DO NOTHING");
    }

    [SkailCommand]
    public async SkailTask RegistrarExpiracao(Guid pedidoId)
    {
        await _db.Database.ExecuteSqlInterpolatedAsync($@"
            INSERT INTO aprovacoes (pedido_id, aprovado, decidido_por, motivo, decidido_em)
            VALUES ({pedidoId}, false, 'sistema', 'prazo de 72 horas sem resposta', now())
            ON CONFLICT (pedido_id) DO NOTHING");
    }

    [SkailCommand]
    public async SkailTask CancelarPedido(Guid pedidoId, string motivo)
    {
        await _db.Database.ExecuteSqlInterpolatedAsync($@"
            UPDATE pedidos SET status = 'cancelado', motivo_cancelamento = {motivo}
            WHERE id = {pedidoId}");
    }

    [SkailCommand]
    public async SkailTask ConcluirPedido(Guid pedidoId, string numeroFatura)
    {
        await _db.Database.ExecuteSqlInterpolatedAsync($@"
            UPDATE pedidos SET status = 'concluido', numero_fatura = {numeroFatura}
            WHERE id = {pedidoId}");
    }

    // Métodos comuns, sem atributo: reaproveitados pelos commands, não são passos do fluxo.
    private async Task<HttpResponseMessage> Postar(string servico, string rota, string chave, object corpo)
    {
        using var requisicao = new HttpRequestMessage(HttpMethod.Post, rota) { Content = JsonContent.Create(corpo) };
        requisicao.Headers.Add("Idempotency-Key", chave);
        return await _http.CreateClient(servico).SendAsync(requisicao);
    }

    private async Task<HttpResponseMessage> Enviar(string servico, string rota, string chave, object corpo)
    {
        var resposta = await Postar(servico, rota, chave, corpo);
        resposta.EnsureSuccessStatusCode();
        return resposta;
    }
}
```

{% endcode %}
{% endtab %}

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

```typescript
// db.ts
import { connectionStringPedidos } from "./config.js";

interface Pool {
  query(text: string, values?: readonly unknown[]): Promise<unknown>;
}

export class PedidosDb {
  static #pool: Promise<Pool> | undefined;

  private static pool(): Promise<Pool> {
    PedidosDb.#pool ??= (async () => {
      const driver = "pg";
      const { Pool } = (await import(driver)) as { Pool: new (config: { connectionString: string }) => Pool };
      return new Pool({ connectionString: connectionStringPedidos });
    })();
    return PedidosDb.#pool;
  }

  async execute(sql: string, ...values: unknown[]): Promise<void> {
    await (await PedidosDb.pool()).query(sql, values);
  }
}
```

{% endcode %}

{% code lineNumbers="true" %}

```typescript
// comandos.ts
// Pedidos.Fluxo
import { SkailCommand } from "@skail/runtime";
import { servicos, type Servico } from "./config.js";
import type { DecisaoGerente, Fatura, ItemPedido, PedidoConfirmado } from "./contratos.js";
import { PedidosDb } from "./db.js";

export interface RespostaReserva {
  Reservado: boolean;
}
export interface RespostaErp {
  Aceito: boolean;
  Motivo: string | null;
}
export interface RecusaErp {
  Motivo: string;
}

export class ComandosDoPedido {
  private readonly db = new PedidosDb();

  @SkailCommand()
  async RegistrarPedido(pedido: PedidoConfirmado): Promise<void> {
    await this.db.execute(
      `INSERT INTO pedidos (id, cliente_id, total, status)
       VALUES ($1, $2, $3, 'confirmado')
       ON CONFLICT (id) DO NOTHING`,
      pedido.PedidoId,
      pedido.ClienteId,
      pedido.Total,
    );
  }

  @SkailCommand()
  async ReservarEstoque(pedidoId: string, itens: ItemPedido[]): Promise<boolean> {
    const resposta = await this.Enviar("estoque", "/reservas", `reserva-${pedidoId}`, { pedidoId, itens });
    const corpo = (await resposta.json()) as RespostaReserva;
    return corpo.Reservado;
  }

  @SkailCommand()
  async LiberarEstoque(pedidoId: string): Promise<void> {
    await this.Enviar("estoque", `/reservas/${pedidoId}/liberar`, `liberacao-${pedidoId}`, { pedidoId });
  }

  @SkailCommand({ retryCount: 8 })
  async EmitirFatura(pedidoId: string, total: number): Promise<Fatura> {
    const resposta = await this.Enviar("faturamento", "/faturas", `fatura-${pedidoId}`, { pedidoId, total });
    return (await resposta.json()) as Fatura;
  }

  @SkailCommand()
  async CancelarFatura(pedidoId: string, numeroFatura: string): Promise<void> {
    await this.Enviar("faturamento", `/faturas/${numeroFatura}/cancelar`, `cancelamento-${pedidoId}`, { pedidoId });
  }

  @SkailCommand()
  async EnviarAoErpDoCliente(pedido: PedidoConfirmado, numeroFatura: string): Promise<RespostaErp> {
    const resposta = await this.Postar("erp", "/pedidos", `erp-${pedido.PedidoId}`, {
      referenciaExterna: pedido.PedidoId,
      ClienteId: pedido.ClienteId,
      Itens: pedido.Itens,
      numeroFatura,
    });

    // 422 é a recusa de negócio documentada pelo ERP (cliente bloqueado, cadastro divergente):
    // volta como resultado, sem nova tentativa. Qualquer outro erro lança exceção e segue para o retry.
    if (resposta.status === 422) {
      let motivo: string | null = null;
      try {
        motivo = ((await resposta.json()) as RecusaErp | null)?.Motivo ?? null;
      } catch {
        // corpo fora do formato esperado: a recusa vale do mesmo jeito
      }
      return { Aceito: false, Motivo: motivo ?? "sem motivo informado" };
    }

    this.garantirSucesso(resposta);
    return { Aceito: true, Motivo: null };
  }

  @SkailCommand()
  async NotificarGerente(pedidoId: string, total: number): Promise<void> {
    await this.Enviar("notificacoes", "/gerente", `notificacao-${pedidoId}`, {
      pedidoId,
      total,
      link: `https://erp.exemplo/aprovacoes/${pedidoId}`,
    });
  }

  @SkailCommand()
  async LembrarGerente(pedidoId: string): Promise<void> {
    await this.Enviar("notificacoes", "/gerente/lembrete", `lembrete-${pedidoId}`, { pedidoId });
  }

  @SkailCommand()
  async RegistrarDecisao(pedidoId: string, decisao: DecisaoGerente): Promise<void> {
    await this.db.execute(
      `INSERT INTO aprovacoes (pedido_id, aprovado, decidido_por, motivo, decidido_em)
       VALUES ($1, $2, $3, $4, now())
       ON CONFLICT (pedido_id) DO NOTHING`,
      pedidoId,
      decisao.Aprovado,
      decisao.DecididoPor,
      decisao.Motivo,
    );
  }

  @SkailCommand()
  async RegistrarExpiracao(pedidoId: string): Promise<void> {
    await this.db.execute(
      `INSERT INTO aprovacoes (pedido_id, aprovado, decidido_por, motivo, decidido_em)
       VALUES ($1, false, 'sistema', 'prazo de 72 horas sem resposta', now())
       ON CONFLICT (pedido_id) DO NOTHING`,
      pedidoId,
    );
  }

  @SkailCommand()
  async CancelarPedido(pedidoId: string, motivo: string): Promise<void> {
    await this.db.execute(
      `UPDATE pedidos SET status = 'cancelado', motivo_cancelamento = $1
       WHERE id = $2`,
      motivo,
      pedidoId,
    );
  }

  @SkailCommand()
  async ConcluirPedido(pedidoId: string, numeroFatura: string): Promise<void> {
    await this.db.execute(
      `UPDATE pedidos SET status = 'concluido', numero_fatura = $1
       WHERE id = $2`,
      numeroFatura,
      pedidoId,
    );
  }

  // Métodos comuns, sem decorator: reaproveitados pelos commands, não são passos do fluxo.
  private async Postar(servico: Servico, rota: string, chave: string, corpo: unknown): Promise<Response> {
    return fetch(new URL(rota, servicos[servico]), {
      method: "POST",
      headers: { "Content-Type": "application/json", "Idempotency-Key": chave },
      body: JSON.stringify(corpo),
    });
  }

  private async Enviar(servico: Servico, rota: string, chave: string, corpo: unknown): Promise<Response> {
    const resposta = await this.Postar(servico, rota, chave, corpo);
    this.garantirSucesso(resposta);
    return resposta;
  }

  private garantirSucesso(resposta: Response): void {
    if (!resposta.ok) throw new Error(`HTTP ${resposta.status} em ${resposta.url}`);
  }
}
```

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

Falta de estoque volta como `false` e a recusa do ERP (HTTP 422) volta como `RespostaErp` com `Aceito` igual a `false`, sem exceção. Qualquer exceção num command gera nova tentativa até o `retryCount`, e repetir a reserva ou o envio não mudaria a resposta de quem recusou. Os outros erros, inclusive 401, 403, 404 e 409, continuam como exceção, porque indicam credencial, rota, conflito ou bug a corrigir e não podem cancelar uma venda válida. Depois de esgotar as tentativas, a execução fica como falha no Monitor; você corrige e retoma. Use o código que o seu ERP documenta para recusa de negócio. O exemplo supõe que o estoque reserva tudo ou nada.

{% hint style="info" %}
O `retryCount: 8` do faturamento é para uma integração que oscila; os outros commands ficam no padrão de 5 tentativas. O SQL usa a sintaxe do PostgreSQL (`ON CONFLICT`, `now()`); no SQL Server, troque por `MERGE` ou `IF NOT EXISTS` e `SYSUTCDATETIME()`. Se o ERP do cliente não aceita chave de idempotência, use a tabela de efeitos aplicados de [Como escrever commands idempotentes](/construa/como-fazer/escrever-commands-idempotentes.md).
{% endhint %}

## 4. A aplicação que roda o fluxo

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

```csharp
// Pedidos.Fluxo, Program.cs
using Microsoft.EntityFrameworkCore;
using Skail.Platform.Runtime.Hosting;

var builder = Host.CreateApplicationBuilder(args);
builder.UseSkail();

builder.Services.AddDbContext<PedidosDb>(o =>
    o.UseNpgsql(builder.Configuration.GetConnectionString("Pedidos")));
builder.Services.AddHttpClient("estoque", c => c.BaseAddress = new Uri("https://estoque.interno"));
builder.Services.AddHttpClient("faturamento", c => c.BaseAddress = new Uri("https://faturamento.interno"));
builder.Services.AddHttpClient("erp", c => c.BaseAddress = new Uri("https://erp-do-cliente.exemplo"));
builder.Services.AddHttpClient("notificacoes", c => c.BaseAddress = new Uri("https://notificacoes.interno"));

await builder.Build().RunAsync();
```

{% endcode %}
{% endtab %}

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

```typescript
// config.ts
export type Servico = "estoque" | "faturamento" | "erp" | "notificacoes";

export const servicos: Record<Servico, string> = {
  estoque: "https://estoque.interno",
  faturamento: "https://faturamento.interno",
  erp: "https://erp-do-cliente.exemplo",
  notificacoes: "https://notificacoes.interno",
};

export const connectionStringPedidos = process.env.PEDIDOS_DB ?? "";
```

{% endcode %}

{% code lineNumbers="true" %}

```typescript
// main.ts
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 ?? "pedidos:v1.0.0",
  bundle: ".skail/skail-functions.mjs",
  manifest: ".skail/manifest.json",
});

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

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

Configure `SKAIL_WORKLOAD=pedidos:v1.0.0` e as outras três variáveis obrigatórias como no [Quickstart](/comece-aqui/quickstart.md). Para processar mais pedidos ao mesmo tempo, suba mais instâncias da mesma aplicação; duas instâncias nunca processam a mesma execução ao mesmo tempo.

{% hint style="warning" %}
Uma execução sempre continua no código da versão do workload em que começou. Se você mudar a ordem ou a quantidade de `await` de `ProcessarPedido`, `AguardarAprovacaoDoGerente` ou `DesfazerPedido`, publique uma versão nova e mantenha a anterior rodando até as execuções dela terminarem. Ver [Versionamento de código com execuções em andamento](/construa/conceitos/versoes-com-execucoes-em-andamento.md).
{% endhint %}

## 5. Iniciar o processo quando o pedido é confirmado

A API que recebe a confirmação monta o argumento e chama o trigger com o id do pedido como id da execução. Ela não grava o pedido: quem grava é o command `RegistrarPedido`, dentro da execução.

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

```csharp
// Pedidos.Api, Program.cs (trecho)
builder.Services.AddHttpClient("skail", c =>
{
    c.BaseAddress = new Uri(builder.Configuration["skailApi:baseUrl"]!);
    c.DefaultRequestHeaders.Add("skail-key", builder.Configuration["skailApi:key"]!);
    c.DefaultRequestHeaders.Add("Skail-namespace", builder.Configuration["skailApi:namespace"]!);
});
```

{% endcode %}
{% endtab %}

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

```typescript
// api.ts
const baseUrl = process.env.SKAIL_API_BASE_URL!;
const skailHeaders = {
  "Content-Type": "application/json",
  "skail-key": process.env.SKAIL_API_KEY!,
  "Skail-namespace": process.env.SKAIL_API_NAMESPACE!,
};

export function skailPost(path: string, body: unknown, signal?: AbortSignal): Promise<Response> {
  return fetch(new URL(path, baseUrl), {
    method: "POST",
    headers: skailHeaders,
    body: JSON.stringify(body),
    signal,
  });
}
```

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

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

```csharp
// Pedidos.Api
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Pedidos.Contratos;

[ApiController]
public class PedidosController : ControllerBase
{
    // Serializa com os nomes das propriedades como estão no C# (Aprovado, DecididoPor),
    // iguais aos do exemplo em cURL.
    private static readonly JsonSerializerOptions Json = new();

    private readonly HttpClient _skail;

    public PedidosController(IHttpClientFactory http) => _skail = http.CreateClient("skail");

    [HttpPost("pedidos/{pedidoId:guid}/confirmar")]
    public async Task<IActionResult> Confirmar(Guid pedidoId, NovoPedido dto, CancellationToken ct)
    {
        var pedido = new PedidoConfirmado(pedidoId, dto.ClienteId, dto.Total, dto.Itens);

        var resposta = await _skail.PostAsJsonAsync(
            $"/trigger/pedidos/v1.0.0/ProcessarPedido/{pedidoId}",
            new object[] { pedido },
            Json,
            ct);

        if (!resposta.IsSuccessStatusCode)
            return StatusCode(503);   // a tela tenta de novo com o mesmo pedidoId

        return Accepted(new { pedidoId });
    }
}

public record NovoPedido(Guid ClienteId, decimal Total, ItemPedido[] Itens);
```

{% endcode %}
{% endtab %}

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

```typescript
// confirmar.ts
// Pedidos.Api
import { skailPost } from "./api.js";
import type { ItemPedido, PedidoConfirmado } from "./contratos.js";

export interface NovoPedido {
  ClienteId: string;
  Total: number;
  Itens: ItemPedido[];
}

export async function confirmar(pedidoId: string, dto: NovoPedido, signal?: AbortSignal) {
  const pedido: PedidoConfirmado = {
    PedidoId: pedidoId,
    ClienteId: dto.ClienteId,
    Total: dto.Total,
    Itens: dto.Itens,
  };

  const resposta = await skailPost(`/trigger/pedidos/v1.0.0/ProcessarPedido/${pedidoId}`, [pedido], signal);

  if (!resposta.ok) return { status: 503 as const }; // a tela tenta de novo com o mesmo pedidoId

  return { status: 202 as const, body: { pedidoId } };
}
```

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

Essa API é uma aplicação comum, separada da aplicação do fluxo, e não usa o runtime do skail. A chave vem do cofre de segredos, e o padrão com retry no chamador está em [Como disparar uma função pela API HTTP](/construa/como-fazer/disparar-uma-funcao-pela-api.md). O `pedidoId` vem de quem cria o pedido, para que uma nova tentativa use o mesmo id. A API devolve 202, e a tela mostra o pedido como em processamento até `RegistrarPedido` gravar.

## 6. Receber a decisão do gerente

A tela de aprovação chama um endpoint do seu backend (trecho do mesmo controller de pedidos), que repassa a decisão ao skail pela API de eventos. O endpoint não grava a decisão: quem grava é o command `RegistrarDecisao`, dentro da execução, com o payload do evento. Se o endpoint gravasse e o disparo falhasse, a decisão estaria no banco e a execução continuaria esperando até expirar.

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

```csharp
[HttpPost("pedidos/{pedidoId:guid}/decisao")]
public async Task<IActionResult> Decidir(Guid pedidoId, DecisaoGerente decisao, CancellationToken ct)
{
    var resposta = await _skail.PostAsJsonAsync(
        $"/api/v1/fire/{Eventos.PedidoDecidido}/{pedidoId}", decisao, Json, ct);

    return resposta.IsSuccessStatusCode ? Accepted() : StatusCode(503);
}
```

{% endcode %}
{% endtab %}

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

```typescript
// decidir.ts
import { skailPost } from "./api.js";
import { Eventos, type DecisaoGerente } from "./contratos.js";

export async function decidir(pedidoId: string, decisao: DecisaoGerente, signal?: AbortSignal) {
  const resposta = await skailPost(`/api/v1/fire/${Eventos.PedidoDecidido}/${pedidoId}`, decisao, signal);

  return resposta.ok ? { status: 202 as const } : { status: 503 as const };
}
```

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

O mesmo disparo por cURL, para testar:

```bash
curl -X POST "https://SEU_SKAIL_BASE_URL/api/v1/fire/PEDIDO_DECIDIDO/8f2c3a10-0000-4000-8000-000000004107" \
  -H "Content-Type: application/json" \
  -H "skail-key: SUA_SKAIL_KEY" \
  -H "Skail-namespace: SEU_NAMESPACE" \
  -d '{ "Aprovado": true, "DecididoPor": "marina.souza", "Motivo": null }'
```

O nome do evento e o id do pedido ligam o disparo à espera. Se o gerente responder logo depois da notificação, antes de a execução chegar à espera pelo evento, o evento é entregue assim que a espera começar. Se a decisão chegar depois que a aprovação expirou, a execução já seguiu sem ela; consulte o status do pedido antes de disparar e mostre na tela que a aprovação expirou.

<figure><img src="https://1898946878-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsJmS5ILPG5x2x5j0WyiV%2Fuploads%2FFi4kXxjSm8RHsw6Zd9KH%2Fap-pedido-gerente.svg?alt=media" alt="AguardarAprovacaoDoGerente: NotificarGerente em 0 h, LembrarGerente em 24 h sem decisão e RegistrarExpiracao em 72 h, que retorna Expirado. Uma espera só pelo evento PEDIDO_DECIDIDO participa dos dois prazos; quando a decisão chega, RegistrarDecisao grava e a function retorna Aprovado ou Reprovado."><figcaption><p>Aprovação do gerente com lembrete e prazo</p></figcaption></figure>

## O que acontece quando algo falha

<figure><img src="https://1898946878-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsJmS5ILPG5x2x5j0WyiV%2Fuploads%2F2I26j9H4ijOGMxbAgfoN%2Fap-pedido-reinicio.svg?alt=media" alt="Pedido até R$ 50 mil com reinício da aplicação depois de ReservarEstoque. Na primeira entrega, RegistrarPedido e ReservarEstoque executam. Na segunda, o replay devolve os dois resultados registrados e EmitirFatura, EnviarAoErpDoCliente e ConcluirPedido executam até a execução ser concluída."><figcaption><p>Reinício no meio do fluxo e retomada</p></figcaption></figure>

**Reinício no meio do processo.** Um deploy da mesma versão, ou uma queda, reinicia a aplicação depois de `ReservarEstoque` e antes de `EmitirFatura`. Quando ela volta, o skail entrega a execução de novo, `RegistrarPedido` e `ReservarEstoque` devolvem o resultado já registrado sem chamar banco nem estoque, e `EmitirFatura` roda pela primeira vez.

**Serviço fora do ar.** Se o faturamento responder 503, `EmitirFatura` lança exceção e o skail tenta o mesmo command de novo, até 8 vezes. Se esgotar, a exceção chega ao `await EmitirFatura(...)`; como o exemplo não a captura, a function é reentregue até o limite de tentativas dela e depois a execução fica como falha no Monitor, com a exceção e o passo em que parou. Depois que o faturamento volta, você retoma a execução e ela continua de `EmitirFatura`, sem reservar estoque de novo. Ver [Retomada manual de execuções falhas](/opere/retomada-manual-de-execucoes-falhas.md).

**ERP do cliente recusa o pedido.** O ERP responde 422 porque o cliente está bloqueado. `EnviarAoErpDoCliente` devolve `Aceito` igual a `false` na primeira tentativa, e `ProcessarPedido` chama `DesfazerPedido`, que cancela a fatura, libera a reserva e grava o pedido como cancelado com o motivo da recusa. A execução termina sem erro, e a linha do tempo mostra a recusa e cada passo da reversão.

**ERP do cliente fora do ar.** Um 503 ou um timeout lança exceção, e o skail tenta o command de novo até 5 vezes. Se esgotar, a exceção chega ao `await` sem captura, a function é reentregue até o limite dela e depois a execução fica como falha no Monitor, sem desfazer nada: a venda continua válida e só falta o envio. Quando o ERP volta, você retoma e a execução continua de `EnviarAoErpDoCliente`. Se o seu negócio prefere cancelar nesse caso, envolva o `await EnviarAoErpDoCliente(...)` num `try`/`catch` que trate só as falhas de rede e de timeout (em C#, `catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)`, porque o timeout do `HttpClient` chega como `TaskCanceledException`) e chame `DesfazerPedido` no `catch`. Num timeout, o ERP pode ter gravado o pedido mesmo sem responder; confira lá antes de cancelar, para os dois sistemas não ficarem divergentes.

**Falha durante a reversão.** `CancelarFatura` e `LiberarEstoque` usam chave de idempotência e têm retry como qualquer command. Se um deles esgotar as tentativas, a execução fica como falha no Monitor parada nesse passo; ao retomar, os passos de reversão que já concluíram não rodam de novo.

**Espera longa.** Durante as 24 horas, ou os três dias, a execução hiberna e nenhuma thread nem processo fica ocupado. Um reinício ou um deploy da mesma versão no meio da espera não muda nada; quando a decisão chega, a execução retoma com as variáveis de antes.

## O que observar no Monitor

Cada pedido é uma execução, identificada pelo id do pedido. Enquanto o gerente não responde, a execução aparece aguardando o evento `PEDIDO_DECIDIDO`, com o id esperado. Um pedido cancelado mostra `DesfazerPedido` como function filha, com os passos de reversão que rodaram. Uma execução que falhou mostra a exceção, o command em que parou e as tentativas. A linha do tempo mostra cada passo com argumentos, resultado e duração.

## Para implementar com um assistente de IA

Entregue esta página ao assistente junto com o pedido abaixo, ajustando nomes e serviços ao seu sistema:

```
Implemente em <C# ou TypeScript> o processo de confirmação de pedido com o skail,
seguindo a página "Pedido com estoque, aprovação e faturamento" da documentação do skail.
Regras: a function só decide e não faz I/O; cada chamada a banco ou serviço
é um command com chave de idempotência derivada do PedidoId; a espera pelo
gerente usa uma única espera por evento, reaproveitada em duas esperas com prazo (24 h e mais 48 h);
recusa de negócio (sem estoque, recusa do ERP) volta como resultado do command, sem exceção;
todo cancelamento passa pela function DesfazerPedido, que desfaz na ordem inversa só o que
foi feito; quem dispara o trigger e o evento não grava nada no banco.
Meus serviços: estoque em <URL>, faturamento em <URL>, ERP do cliente em <URL>,
notificações em <URL>. Banco: PostgreSQL, tabelas pedidos e aprovacoes.
Tipos compartilhados em Pedidos.Contratos; só Pedidos.Fluxo usa o skail.
```

## Próximos passos

[Human-in-the-loop](/construa/como-fazer/padroes-e-antipadroes/human-in-the-loop.md) traz aprovação em cadeia e escalonamento. [Como esperar com timeout](/construa/como-fazer/esperar-um-evento-com-prazo.md) detalha prazo e lembrete. [Como tratar erros e compensar](/construa/como-fazer/tratar-erros-e-desfazer-passos.md) e [Saga](/construa/como-fazer/padroes-e-antipadroes/saga.md) aprofundam a reversão. [Falhas, retries e idempotência](/construa/conceitos/retries-e-idempotencia.md) explica o que o skail garante em cada tentativa.
